プラグイン作成と発見

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

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

  1. Using naming convention

  2. Using namespace packages

  3. Using package metadata.

命名規則を用いるやり方

あなたのアプリケーション用のすべてのプラグインが命名規則に従うのであれば、 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 をインクルードしなければなりません。これはまた、 プラグインの側で setuptools.find_packages() を使う代わりに、パッケージ名のリストを setup()packages 引数に明示的に渡す必要がある、ということを意味しています。

警告

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

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

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

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

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

そして、 importlib.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_pluginsimportlib.metadata.EntryPoint 型の(オブジェクトの)集合となるでしょう。

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

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

注釈

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

注釈

Since this specification is part of the standard library, packaging tools including setuptools provide support for defining entry points.