pkg_resources加载插件

当编写应用软件时,我们通常希望程序具有一定的扩展性,额外的功能——甚至所有非核心的功能,都能通过插件实现。特别是使用 Python 编写的程序,由于语言本身的动态特性,为我们的插件方案提供了很多种实现方式。例如,使用标准库 importlibimport_module()函数,它可以动态加载指定的 Python 模块。但这种实现方式有一些缺点:

  • 不容易区分“模块不存在”与“模块里 import 错误”产生的 ImportError 异常
  • 需要指定插件模块所属的 Package,对插件代码的组织限制较大
  • 不容易实现多个相同名字插件共存的情况,例如希望实现第三方插件覆盖内建插件的功能,因为通常通过约定的名字搜索

本文讨论的主角是与安装库 setuptools 一并安装的软件库 pkg_resources。它基本解决了上述的问题,并且事实上成为了流行的插件实现方式。

Distribution

要理解 pkg_resources 的运作机制,首先得搞清楚一些相关的概念。Distribution 主要指的是 egg 软件包,根据我了解,主要有两种方式得到 egg 软件包。第一种方式是安装,通过 pipsetup.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 的钩子,也许可以有一些自动化的方案,研究后再作分享和讨论。

Advertisements

发表评论

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / 更改 )

Twitter picture

You are commenting using your Twitter account. Log Out / 更改 )

Facebook photo

You are commenting using your Facebook account. Log Out / 更改 )

Google+ photo

You are commenting using your Google+ account. Log Out / 更改 )

Connecting to %s