プラグイン作成と発見#

Pythonのアプリケーションまたはライブラリを作成する時には、カスタマイズができるようにしたり プラグイン を通じて機能を追加できるようにしたりすることがしばしばあります。Pythonのパッケージは別々に配布できますので、あなたのアプリケーションまたはライブラリが利用可能なすべてのプラグインを自動的に 探し出す ようにしたくなるかもしれません。

プラグインの自動検出には大きく分けて3個の実現方法があります。

  1. 命名規則を用いるやり方

  2. namespaceパッケージを用いるやり方

  3. パッケージのメタデータを用いるやり方

命名規則を用いるやり方#

あなたのアプリケーション用のすべてのプラグインが命名規則に従うのであれば、 pkgutil.iter_modules() を用いて命名規則に合致するトップレベルのすべてのモジュールを発見することができます。例えば、 Flask は命名規則として flask_{plugin_name} を使います。もし、すべてのインストール済みFlask用プラグインを発見したいのであれば:

import importlib
import pkgutil

discovered_plugins = {
    name: importlib.import_module(name)
    for finder, name, ispkg
    in pkgutil.iter_modules()
    if name.startswith('flask_')
}

もしあなたが Flask-SQLAlchemyFlask-Talisman のふたつのプラグインをインストールしてあるなら、 discovered_plugins は次のようになるでしょう:

{
    'flask_sqlalchemy': <module: 'flask_sqlalchemy'>,
    'flask_talisman': <module: 'flask_talisman'>,
}

プラグインの命名規則を用いることで、あなたの命名規則に従うすべてのパッケージについてPython パッケージインデックスの simple repository API から検索することもできるようになります。

namespaceパッケージを用いるやり方#

Namespace パッケージ を使えば、プラグインをどこに配置するかに関する規則や、発見するための方法も提供できます。例えば、あなたが名前空間を決めるサブパッケージ myapp.plugins を作成したら、その名前空間に他の 配布物 がモジュールやパッケージを配置することができます。インストールが終われば、あなたは pkgutil.iter_modules() を用いてインストール済みの全てのモジュールやパッケージをその名前空間で発見できるでしょう。

import importlib
import pkgutil

import myapp.plugins

def iter_namespace(ns_pkg):
    # Specifying the second argument (prefix) to iter_modules makes the
    # returned name an absolute name instead of a relative one. This allows
    # import_module to work without having to do additional modification to
    # the name.
    return pkgutil.iter_modules(ns_pkg.__path__, ns_pkg.__name__ + ".")

discovered_plugins = {
    name: importlib.import_module(name)
    for finder, name, ispkg
    in iter_namespace(myapp.plugins)
}

iter_modules()myapp.plugins.__path__ を指定すると、その名前空間の直下にあるモジュールだけを探索するようになります。例えば、あなたがモジュールの myapp.plugins.amyapp.plugins.b を提供する配布物をインストールしているとしたら、 discovered_plugins は次のようになるでしょう:

{
    'a': <module: 'myapp.plugins.a'>,
    'b': <module: 'myapp.plugins.b'>,
}

この例ではサブパッケージを名前空間を決めるパッケージ(myapp.plugins)として使っていますが、トップレベルのパッケージをこの(myapp_plugins``のような)目的に用いることも可能です。名前空間をどのようにして決めるかは好みの問題ですが、あなたのプロジェクトのトップレベルのパッケージ(この場合では``myapp)をプラグインの名前空間を決めるために用いると、全体の名前空間を破壊するようなプラグインがひとつあるだけで、あなたのプロジェクトをインポートすることができなくなるのでお勧めしません。「名前空間を決めるサブパッケージ」の手法がうまく動作するためには、プラグインパッケージ側のトップレベルパッケージのディレクトリ(この場合には``myapp``)に __init__.py が存在してはいけませんし、名前空間を決めるサブパッケージのディレクトリ(myapp/plugins)にある __init__.py`をプラグインパッケージ側でインクルードしなければなりません。これはまた、 プラグインの側で:func:`setuptools.find_packages を使うのではなく、パッケージの名前を setup`の ``packages`() 引数に明示的に渡す必要がある、ということを意味しています。

警告

名前空間を決めるパッケージは込み入った機能で、いくつかの異なる作成方法があります。 名前空間パッケージをパッケージする 文書を読むとともに、あなたのプロジェクト用のプラグインとしてはどちらの手法が好ましいのかを明白に文書化しておくことを強くお勧めします。

パッケージのメタデータを用いるやり方#

パッケージは、 エントリポイントの仕様 に記述されたプラグインのためのメタデータを持つことができます。それを指定することで、パッケージが特定の種類のプラグインを含んでいることをアナウンスします。そのメタデータを使って、同じ種類のプラグインをサポートする別のパッケージがそのプラグインを検出するのに使うことができます。

例えば、myapp-plugin-a という名前のパッケージが存在して、その pyproject.toml に次のものを含む場合:

[project.entry-points.'myapp.plugins']
a = 'myapp_plugin_a'

そして、 import lib.metadata.entry_points() (あるいはPython 3.6-3.9用の backport_``import lib_metadata>=3.6``)を使うことで、登録されたエントリポイントを全て検出することができます。

import sys
if sys.version_info < (3, 10):
    from importlib_metadata import entry_points
else:
    from importlib.metadata import entry_points

discovered_plugins = entry_points(group='myapp.plugins')

この例では、 discovered_pluginsimport lib.metadata.EntryPoint 型の(オブジェクトの)集合となるでしょう。

(
    EntryPoint(name='a', value='myapp_plugin_a', group='myapp.plugins'),
    ...
)

今や、discovered_plugins['a'].load() を実行することで、あなたが選んだモジュールをインポートすることができます。

注釈

setup.py における entry_points の指定はかなり自由度が高く、オプションがたくさんあります。 entry_point の全部のセクションに目を通すことをお勧めします。

注釈

この仕様は 標準ライブラリ の一部なので、setuptools以外のほとんどのパッケージングツールでもエントリポイントを定義できる機能を提供しています。