当编写应用软件时,我们通常希望程序具有一定的扩展性,额外的功能——甚至所有非核心的功能,都能通过插件实现。特别是使用 Python 编写的程序,由于语言本身的动态特性,为我们的插件方案提供了很多种实现方式。例如,使用标准库 importlib
的 import_module()
函数,它可以动态加载指定的 Python 模块。但这种实现方式有一些缺点:
- 不容易区分“模块不存在”与“模块里
import
错误”产生的 ImportError
异常
- 需要指定插件模块所属的 Package,对插件代码的组织限制较大
- 不容易实现多个相同名字插件共存的情况,例如希望实现第三方插件覆盖内建插件的功能,因为通常通过约定的名字搜索
本文讨论的主角是与安装库 setuptools
一并安装的软件库 pkg_resources
。它基本解决了上述的问题,并且事实上成为了流行的插件实现方式。
Distribution
要理解 pkg_resources
的运作机制,首先得搞清楚一些相关的概念。Distribution 主要指的是 egg 软件包,根据我了解,主要有两种方式得到 egg 软件包。第一种方式是安装,通过 pip
或 setup.py
安装,软件包就能安装到 Python 的搜索路径中 sys.path
。安装的结果可能是一个 zip 压缩文件,也可能是一个目录树,根据软件包的内容以及 setup.py
中的 zip_safe
参数而定。第二种方式是执行 setup.py
的子命令 bdist_egg
,直接得到一个 egg 软件包。
pkg_resources
操作的主要单位就是 Distribution。例如,Python 脚本启动时,pkg_resources
识别出搜索路径中的所有 Distribution 的命名空间包,因此,我们会发现 sys.path
包含了很多 pip
安装的软件包的路径,并且可以正确执行 import
操作。
假设插件目录不属于默认的搜索路径列表,我们可以通过 WorkingSet
对象实例的 find_plugins()
方法找到指定目录的 Distribution。pkg_resources
自带一个全局的 WorkingSet
对象,代表默认的搜索路径的工作集,通常使用这个对象即可。下面是一个小例子:
import pkg_resources
env = Environment(["/path/to/plugin", "..."])
dists, _ = pkg_resources.find_plugins(env)
for dist in dists:
pkg_resources.working_set.add(dist)
Entry Point
Entry Point 是 Distribution 元信息的一部分,它把软件包中的一些 Python 对象(如函数、类)记录下来,使得 pkg_resouces
可以在运行时动态加载。python -m
等操作正是利用这个特性。每个 Entry Point 包含三部分:
- 组,以点号分隔便于组织层次,但与 Package 没有关联,如
pass.login
- 名字,如
auth
- Distribution 中的位置,如
shadow:auth
,前面是 Package 和 Module,后面是模块内的位置
自定义的 Entry Point 必须在 setup.py
中指定,它的值是一个 dict
,Key 是组,Value 是一个 list
,每个值是 name = location
形式的字符串。下面是一个小例子:
from setuptools import setup
setup(
entry_points={
"pass.login": [
"auth = shadow:auth",
],
},
)
解决方案
首先在插件规范中约定好组和名字,然后,我们编写程序时,根据配置文件、命令行参数,或者代码中的常量,获取需要使用的组和名字,然后使用 WorkingSet
对象的 iter_entry_points()
方法,枚举所有的 Entry Point,使用其 load()
方法,获取指向的 Python 对象。下面是一个小例子:
import pkg_resources
for entry_point in pkg_resources.iter_entry_points(group, name):
func = entry_point.load()
break
else:
raise RuntimeError("Entry Point Not Found")
pkg_resources
方式实现的插件也有其缺点,插件提供者必须打一个 egg 软件包,再进行分发。这或多或少有些不便,但如果结合 git
的钩子,也许可以有一些自动化的方案,研究后再作分享和讨论。