创建和发现插件#
在创建 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()
(或反向移植importlib_metadata >= 3.6
适用于 Python 3.6-3.9)来发现并加载所有已注册的入口点
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()
来导入你选择的模块。
注意
entry_point
在setup.py
中的规范相当灵活,并且有很多选项。建议阅读入口点的整个部分。
注意
由于此规范是标准库的一部分,因此除了 setuptools 之外的大多数打包工具都提供对定义入口点的支持。