其他语言/应用中python/setuptools入口点(扩展)的替代实现
虽然这个问题是关于Python后端的,但其实它并不局限于Python本身,而是关于扩展机制,以及如何注册和查找插件。
在Python中,入口点的概念是由setuptools引入的,它与已安装Python分发版的元数据有关(在其他打包系统中称为包)。
根据我的理解,入口点的一个功能是允许应用程序定义一个地方,让其他人可以在这里放东西,这样任何想要使用入口点的应用程序都可以获取到在这里注册的类或函数的列表。举个例子:
- Foo定义了一个名为“entrypoint1”的入口点,并查找在这个名字下注册的插件。
- Bar在“entrypoint1”入口点上注册了一个可调用对象(
Bar.callable
)。 - 任何Python脚本都可以将
Bar.callable
列为“entrypoint1”的注册可调用对象之一。
使用setuptools时,应用程序在安装时注册入口点,这些信息存储在与打包相关的元数据中,称为.egginfo(通常包含有关分发名称、依赖关系以及一些其他打包元数据的信息)。
我觉得打包元数据并不是存储这种信息的合适地方,因为我不明白为什么这些信息与打包有关。
我很想了解其他语言中的这种入口点/扩展/插件功能,特别是这个概念是否与元数据和打包有关。因此我的问题是……
你有没有我应该查看的例子?你能解释一下为什么会做出这样的设计选择吗?
你能想到不同的方式来处理这个问题吗?你知道这个问题在不同工具中是如何解决的吗?当前Python实现相较于其他实现有什么优缺点?
我目前找到的内容 到目前为止
在不同的项目中,我发现了一种创建和分发“插件”的方法,特别关注于“我们如何制作插件”。
例如,libpeas(GObject插件框架)定义了一套通过指定插件来扩展默认行为的方法。虽然这很有趣,但我只对“注册和查找”(最终加载)部分感兴趣。
以下是我目前的一些发现:
Libpeas定义了自己的元数据文件(*.plugin),其中存储了可调用对象的类型信息(可以有不同语言的插件)。这里的主要信息是要加载的模块名称。
Maven有一份设计文档,其中包含了如何管理插件的信息。Maven管理插件及其依赖关系和元数据,因此似乎是一个有趣的地方,可以查看他们是如何实现的。
根据他们的文档,Maven插件使用注解(@goal
)在类上,这样就可以找到所有注册了特定@goal
的插件。虽然这种方法在静态语言中是可行的,但在解释性语言中就不行,因为我们只能在某个时刻知道所有可能的类/可调用对象,这可能会改变。
Mercurial使用一个中央配置文件(~/.hgrc
),其中包含插件名称与其路径的映射。
更多想法
虽然这不是对这个问题的回答,但了解setuptools入口点的实现以及它们在性能上的比较也是很有趣的,尤其是与Mercurial的比较。
当你请求特定的入口点时,使用setuptools,所有元数据都是在运行时读取的,并以这种方式构建列表。这意味着如果你的路径上有很多Python分发版,这个读取过程可能会花费一些时间。另一方面,Mercurial将这些信息硬编码到一个文件中,这意味着你必须在这里指定可调用对象的完整路径,因此注册的可调用对象不是“发现”的,而是直接从配置文件中“读取”的。这允许更细粒度的配置,决定哪些应该可用,哪些不应该可用,并且似乎更快。
另一方面,由于Python路径可以在运行时更改,这意味着以这种方式提供的可调用对象必须与路径进行检查,以确定在所有情况下是否应该返回它们。
为什么入口点目前与打包相关
理解为什么入口点与setuptools中的打包相关也很有趣。主要原因是,Python分发版在安装时可以注册自己的一部分作为扩展入口点,这样安装就意味着也注册了入口点:不需要额外的注册步骤。
虽然在大多数情况下(当Python分发版实际被安装时)这工作得相当好,但在它们没有被安装或根本没有打包时就不行。换句话说,根据我的理解,你不能在运行时注册入口点,而不拥有.egg-info文件。
3 个回答
我想回答一个问题:“如何在不打包或安装模块的情况下注册Python入口点”。我觉得这其实是你们思考和发现的一个隐含内容,也是让我困扰了很久的事情。
至于其他内容,虽然这是个很有趣的话题,但我觉得这个问题太宽泛了,没法完全回答(我其实很惊讶它没有因为太宽泛而被关闭),而且在我自己找到答案之前,我一直很难找到相关信息。
如何在不打包或安装模块的情况下注册Python入口点
对于那些习惯把Python模块或简单的.py文件放在可导入路径下的普通用户来说,通常是放在./src、./lib或者平坦的工作目录里,其实不需要打包任何东西就可以注册入口点。
根据你的例子,只需在可导入的目录下放一个文件到.../bar.dist-info/entry_points.txt,内容如下:
# bar.dist-info/entry_points.txt
[entrypoint1]
bar = Bar.callable
这样就可以通过以下方式获取任意可调用对象:
import importlib.metadata
importlib.metadata.entry_points()["entrypoint1"]
这样一来,所有内容实际上都不依赖于setuptools或其他任何打包工具。
这个方法其实文档很少,我是在痛苦调试setuptools的pkg_resources导入时才发现的。我没有深入研究entry_points.txt的放置要求,但似乎只要在Python路径下,任何任意的*.{egg,dist}-info/文件夹都可以。
结合你的想法,由于Python的可导入路径可以通过PYTHONPATH、PYTHONHOME、PYTHONUSERSITE和其他site内部特性进行修改,我觉得这种做法非常方便,因为只需注册在Python导入机制可到达的入口点。比起制作一个需要在运行时检查的单一文件,我认为这样更好。
还有其他方法吗?
进一步阅读https://docs.python.org/3/library/importlib.html#setting-up-an-importer让我想到,应该可以通过importlib.machinery
的入口查找器来注册入口点,但我还没有尝试过。如果你是从一些奇怪的非文件源加载模块,比如https://github.com/nvbn/import_from_github_com,这可能是唯一的方法。
示例应用
pytest
允许你通过“pytest11”入口点标识符注册插件。这是插件作者用来自我声明的方式,以便pytest --fixtures
能列出他们打包的fixtures。
然而,作为pytest的最终用户,你并不需要通过这个入口点注册任何钩子、fixtures或其他东西!它们会从你本地的conftest.py文件中加载。如果你在开发本地插件,可以通过PYTEST_PLUGINS
环境变量来注册它们。
所以pytest“这个应用”提供了等效的机制来注册任意功能,无论你是插件开发者、插件用户,还是仅仅在写本地的修改。入口点只是“正确安装”的插件的注册机制。
我相信这应该是任何需要注册插件的应用的最佳方式。