Entry Points

入口点是包的元数据(metadata),在包安装时暴露。作用于两个场景:

  1. 提供可在终端执行的命令,即console script,比如pip包提供了可直接运行的pip 命令。
  2. 提供了通过插件实现定制功能的能力,比如pytest包允许通过pytest11 入口点接入插件。

Python定义了入口点对象即EntryPoint,每个 EntryPoint 都有一个 .name.group.value 属性以及一个 .load() 方法来解析值。

Console Scripts

假设包的目录结构如下:

project_root_directory
├── pyproject.toml        # and/or setup.cfg, setup.py
└── src
    └── timmins
        ├── __init__.py
        └── ...

其中__init__.py 包含一个函数:

def hello_world():
    print("Hello world")

如何想要命令行执行hello_word() ,方法之一是在创建src/timmins/__main__.py 文件,内容代码调用该方法:

from . import hello_world

if __name__ == '__main__':
    hello_world()

然后就可以通过python -m 调用:

$ python -m timmins
Hello world

而通过入口点,可以便捷地创建一个可执行命令,即console script。入口点配置如下:

# setup.py文件
from setuptools import setup

setup(
    # ...,
    entry_points={
        'console_scripts': [
            'hello-world = timmins:hello_world',
        ]
    }
)

上例以setup.py文件为例,还可以通过pyproject.tomlsetup.cfg配置。

这里的console_scripts是入口点的组名(group),表明该命令属于console_scripts 组,组名可以被其他包如pip 辨别。console_scripts 即告诉pip包将该组的所有方法包装成可执行命令。

hello-world 是入口点的名字(name),即终端调用的命令名;

timmins:hello_world是入口点的值(value),表明该命令实际执行timmins包的hello_world方法。

在包安装后,就可以直接调用名为hello-world的命令:

$ hello-world
Hello world

Entry Points for Plugins

入口点允许一个包公开其某些功能,以供其他库和应用程序发现并使用。这一特性使得包可以通过插件扩展和定制功能。

例如,上述timmins包的目录结构不变:

project_root_directory
├── pyproject.toml        # and/or setup.cfg, setup.py
└── src
    └── timmins
        ├── __init__.py
        └── ...

src/timmins/__init__.py 内容修改如下:

def display(text):
    print(text)

def hello_world():
    display('Hello world')

此时hello_world()方法调用display() 方法打印文本。

如果我们希望使用不同的打印风格,只需要修改display()方法即可。那么如何通过插件的方式,提供不同的display()方法供hello_world()调用呢?

包和插件之间通过入口点这一协议进行交互。

我们实现一个插件包给timmins提供不同的display()方法,插件包目录结构如下:

timmins-plugin-fancy
├── pyproject.toml        # and/or setup.cfg, setup.py
└── src
    └── timmins_plugin_fancy
        └── __init__.py

src/timmins_plugin_fancy/__init__.py ,定义新的display()方法:

def excl_display(text):
    print('!!!', text, '!!!')

为了让timmins包能“发现”该插件提供的功能,需要修改插件包配置文件setup.py ,也可以通过pyproject.tomlsetup.cfg配置。

# setup.py
from setuptools import setup

setup(
    # ...,
    entry_points = {
        'timmins.display': [
            'excl = timmins_plugin_fancy:excl_display'
        ]
    }
)

这里timmins.display 作为组名标志,可以被其他包“发现“。

对应timmins包也需要修改,将原本调用脚本src/timmins/__init__.py的内容改为:

from importlib.metadata import entry_points
display_eps = entry_points(group='timmins.display')
try:
    display = display_eps[0].load()
except IndexError:
    def display(text):
        print(text)

def hello_world():
    display('Hello world')

其中importlib.metadata.entry_points 方法会检查所有安装包的元数据,即dist-infoegg-info 目录,收集其中的入口点。

此处entry_points(group='timmins.display') 指定收集组名为timmins.display的入口点。

因为timmins包的display 方法被插件timmins-plugin-fancy包提供的方法替代,所以此时执行:

$ hello-world
!!! Hello world !!!

Entry Points Syntax

入口点的配置语法:

<name> = <package_or_module>[:<object>[.<attr>[.<nested-attr>]*]]

入口点值的解析相当于通过import代码进行解析。例如:

<name> = <package_or_module>

相当于:

import <package_or_module>
parsed_value = <package_or_module>

例如:

<name> = <package_or_module>:<object>

相当于

from <package_or_module> import <object>
parsed_value = <object>

例如:

<name> = <package_or_module>:<object>.<attr>.<nested_attr>

相当于:

from <package_or_module> import <object>
parsed_value = <object>.<attr>.<nested_attr>

<
Previous Post
Python类型检查
>
Next Post
位操作