创建和发现插件¶
通常在创建 Python 应用程序或库时,您会希望能够通过插件提供定制或额外功能。由于 Python 包可以单独分发,您的应用程序或库可能希望自动发现所有可用的插件。
自动插件发现主要有三种方法
使用命名约定¶
如果您的应用程序的所有插件都遵循相同的命名约定,您可以使用 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-SQLAlchemy 和 Flask-Talisman 插件,那么 discovered_plugins 将是
{
'flask_sqlalchemy': <module: 'flask_sqlalchemy'>,
'flask_talisman': <module: 'flask_talisman'>,
}
对插件使用命名约定也允许您查询 Python 包索引的 简单仓库 API,以查找所有符合您的命名约定的包。
使用命名空间包¶
命名空间包 可以用于为放置插件提供约定,并且还提供了一种执行发现的方法。例如,如果您将子包 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)
}
将 myapp.plugins.__path__ 指定给 iter_modules() 会使其仅查找直接在该命名空间下的模块。例如,如果您安装了提供模块 myapp.plugins.a 和 myapp.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。这还意味着插件需要显式地将包列表传递给 setup() 的 packages 参数,而不是使用 setuptools.find_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 importlib_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_plugins 将是 importlib.metadata.EntryPoint 类型的一个集合
(
EntryPoint(name='a', value='myapp_plugin_a', group='myapp.plugins'),
...
)
现在,通过执行 discovered_plugins['a'].load() 可以导入您选择的模块。
注意
setup.py 中的 entry_point 规范非常灵活,并且有很多选项。建议阅读 入口点 的整个部分。
注意
由于此规范是 标准库 的一部分,除了 setuptools 之外的大多数打包工具都支持定义入口点。