nikkie-ftnextの日記

イベントレポートや読書メモを発信

PythonパッケージのEntry Pointsを使うことで、同じ関数でもプラグインのインストールの有無で振る舞いを変えることができちゃうんです!!

はじめに

7/1はこころの日❤️ nikkieです。

まだチュートリアルレベルですが、Pythonのパッケージに関してちょっとした魔法を会得しました。

目次

見てください、この挙動! 同じ関数なのに、プラグインインストールの有無で動きが変わります!

環境は Python 3.11.3 です。
venvを使った仮想環境で検証していきます。

まずtimminsパッケージをインストールします。

% pip install 'timmins@git+https://github.com/ftnext/entry-points-suburi#subdirectory=docs-plugins-example/timmins'

% pip list
Package    Version
---------- -------
pip        23.1.2
setuptools 68.0.0
timmins    0.1.0

timmins.hello_world関数を実行します。

% python -c 'import timmins; timmins.hello_world()'
Hello world

続いて、プラグインにあたるtimmins-plugin-fancyもインストールします。

% pip install 'timmins-plugin-fancy@git+https://github.com/ftnext/entry-points-suburi#subdirectory=docs-plugins-example/timmins-plugin-fancy'

% pip list
Package              Version
-------------------- -------
pip                  23.1.2
setuptools           68.0.0
timmins              0.1.0
timmins-plugin-fancy 0.1.0

再度timmins.hello_world関数を実行します。

% python -c 'import timmins; timmins.hello_world()'
!!! Hello world !!!

同じ関数を呼んでいるのに、出力される文字列が違いますよね?
プラグインの実装が使われているんです!

プラグインをアンインストールすると、出力は元に戻ります。

% pip uninstall -y timmins-plugin-fancy

% python -c 'import timmins; timmins.hello_world()'
Hello world

これ、すごくないですか!?
魔法っぽくないですか!?

ふふーん♪ 今日のnikkieはこの作り方が分かっちゃったんです。

小まとめ:Pythonパッケージでプラグイン

同じ関数timmins.hello_worldについて

これをtimminsは再インストールせずに実現できるんです(まさしくプラグイン!)。

では、どうやるかを見ていきましょう。

setuptoolsのUser Guideより「Entry Points for Plugins」

timmins

timminsの実装ですが、

  • 与えられた文字列を出力する関数(display
  • Hello worldを(displayを使って)出力する関数(hello_world

に分かれます。

def display(text: str) -> None:
    print(text)


def hello_world() -> None:
    display("Hello world")

このうちdisplayプラグインで上書きします。
これにはEntry Pointsを使います。

標準ライブラリのimportlib.metadata

インストールしたパッケージのメタデータを扱えるモジュールです。
ドキュメントの「概要」を見ると、pip installしたwheelパッケージのバージョンを取得する例があります。

ドキュメントの「エントリポイント」の例を今回使っています。

>>> from importlib.metadata import entry_points
>>> display_eps = entry_points(group="timmins.display")

エントリポイントの集合のうち、groupが"timmins.display"のエントリポイントを選択します1

timminsはimportlib.metadataを使って、プラグインがあったらdisplayメソッドをロードする

groupが"timmins.display"のエントリポイント(の1つ)をload2、それをdisplayという名で指します。

つまり、プラグインがインストールされていたら、プラグインの実装を使ったdisplayで動くように実装していたわけです。

プラグイン timmins-plugin-fancy

プラグイン側のポイントはプロジェクトの設定ファイルです。
今回はpyproject.tomlを使っています3

timminsのプラグイン命名規則として、groupが"timmins.display"のエントリポイントとする必要があります。

[project.entry-points."timmins.display"]
excl = "timmins_plugin_fancy:excl_display"

excl_display関数は「!」を加えてprintする関数です。

def excl_display(text: str) -> None:
    print("!!!", text, "!!!")

timmins-plugin-fancyをインストールした状態でentry_points(group="timmins.display")するとexcl_display関数が見つかります。
なので、timminsdisplay(という名前)はプラグインexcl_display関数を指した状態で動作し、「!」が加わったHello worldとなります。

関連情報

Python実践入門』で知る

全然別目的で13章を読んでいたところ、「entry_pointsを利用したプラグイン機構」というコラムが!(13.5)

setuptoolsのドキュメントを検索し、「Entry Points for Plugins」を見つけました。

こんなPythonプロジェクトで使われている仕組みです!

有名なライブラリにはプラグインがありますよね。
プラグインってどう動いているのか深く追ったことはないのですが、仕組みは常々不思議に思っていました。
プラグインを実現するのに、エントリポイントが一役買っていました!

他にもまだまだあると思います🤗

終わりに

ライブラリの実装を変えていないのに、プラグインのインストールの有無で振る舞いが変わるのをどう実現しているか分かりました。
エントリポイントはconsole_scriptsの指定が多かったですが、プラグインにも一役買っていたのですね!
importlib.metadata4を使って、プラグインの実装をロードできる!

これまでずっと少し不思議だったので、仕組みが分かって、しかも簡単な例で自分でも作れて、大変盛り上がっています🙌

今回のリポジトリです:

P.S. 「いつ使うのかわからないうちは使わなくてよい」

「いつ使うの?」と思った方、いまのあなたは使う必要がないということです!


  1. ドキュメントより「Equivalently, since entry_points passes keyword arguments through to select
  2. ドキュメントより「値を解決する .load() メソッド
  3. setup.pyはどうしても必要ではないなら避けよう(setup.cfgかpyproject.tomlを使おう)。ref:https://setuptools.pypa.io/en/latest/userguide/quickstart.html#setup-py
  4. Python 3.8で追加され、3.10で暫定的なものではなくなっています。暫定版だと使いづらい向きにはサードパーティimportlib-metadataがあります