PEP 829 – 通过 .site.toml 文件实现结构化启动配置
摘要
本PEP提出一种基于TOML的配置文件格式,用于替代解释器启动期间site.py所使用的.pth文件机制。新格式使用名为<package>.site.toml的文件,为扩展sys.path和执行包初始化代码提供结构化配置,取代了当前将路径配置与任意代码执行混为一谈的临时.pth格式。
动机
Python的.pth文件(启动时由Lib/site.py处理)支持两个功能:
- 扩展
sys.path– 文件中不以#或import开头的行,指定要追加到sys.path的目录。相对路径隐式地以site-packages目录为基准。 - 执行代码– 以
import(或import\t)开头的行,通过将源字符串传递给exec()立即执行。
此设计存在若干问题:
- 代码执行是实现带来的副作用。以
import开头的行可通过分号分隔多个语句进行扩展。只要待执行代码位于同一行,在处理.pth文件时就会全部执行。 .pth文件本质上是非结构化的,导致内容难以推理或验证,通常甚至难以阅读。它将两个可能有用的功能混杂在一起,却带有不同的安全约束,且无法分离这些关注点。- 由于
.pth文件缺乏结构,也就无法表达元数据,无法使格式具备未来兼容性,也没有定义内容的执行或处理顺序。 - 在解释器启动期间对文件内容使用
exec()会暴露很大的攻击面。 - 没有显式的入口点概念,而入口点已是 Python 打包领域中一种公认的模式。那些需要在启动时执行代码和初始化的包滥用了
import行,而不是显式声明入口点。
规范
本PEP定义了一种名为<package>.site.toml的新文件格式,以解决.pth文件存在的所有问题。与.pth文件类似,<package>.site.toml文件会在 Python 启动时由site.py模块处理,这意味着禁用site.py的-S选项同样会禁用<package>.site.toml文件。标准库的tomllib包用于读取和处理<package>.site.toml文件。
<package>.site.toml文件的存在会取代并行的<package>.pth文件。这既提供了轻松的迁移路径,也支持与旧版 Python 并存。
任何解析错误都会导致整个<package>.site.toml文件被忽略且不被处理(但它仍然会取代任何并行的<package>.pth文件)。导入入口点模块或调用入口点函数时发生的任何错误都会被报告,但不会中止 Python 可执行文件。
文件命名与发现
- 与
.pth文件一样,包可以选择性地安装单个<package>.site.toml文件,这与当前的.pth文件约定一致。 - 文件命名格式为
<package>.site.toml。其中的.site标记用于将其与site-packages中可能存在的其他 TOML 文件区分开,并描述该文件的用途(由site.py处理)。 <package>前缀应当与包名匹配,但与.pth文件一样,解释器并不强制这一点。构建后端和安装工具可以自行选择施加更严格的约束。- 包名(即
<package>前缀)必须遵循标准的名称规范化规则。 <package>.site.toml文件位于与当前.pth文件相同的site-packages目录中。<package>.site.toml文件的发现规则与当前.pth文件相同。以单个.开头的文件名(如.site.toml)以及具有操作系统级隐藏属性(UF_HIDDEN、FILE_ATTRIBUTE_HIDDEN)的文件将被排除。- 处理顺序按文件名字母顺序排列,与
.pth行为一致。 - 如果
<package>.site.toml和<package>.pth共存于同一目录,则只处理<package>.site.toml文件。换句话说,<package>.site.toml文件的存在会取代并行的<package.pth>文件,即使 TOML 文件的格式无效也是如此。
处理模型
在发生任何处理(即路径扩展或入口点执行)之前,会先读取并解析指定site-packages目录中的所有<package>.site.toml文件到一个中间数据结构。这种两阶段方法(先读取后处理)能够实现:
- 未来的策略机制可以在执行前检查并修改收集到的数据(例如,禁用特定包的入口点或强制执行路径限制)。注意:此类策略框架明确不属于本PEP的范围。
- 未来对路径扩展和入口点执行处理进行更细粒度的控制。例如,可以设想特殊的
-X选项、环境变量或其他类型的配置,只允许路径扩展,或者显式管理入口点的允许或拒绝列表。注意:此类配置选项明确不属于本PEP的范围。 - 更好的错误报告。所有解析、格式和数据类型错误都可以在任何处理发生之前呈现出来。
在每个site-packages目录内,处理顺序如下:
- 发现并解析所有
<package>.site.toml文件,按字母顺序排序。 - 处理解析后的 TOML 文件中的所有
[paths]条目。 - 执行解析后的 TOML 文件中的所有
[entrypoints]条目。 - 处理任何未被
<package>.site.toml文件取代的剩余<package>.pth文件。
这确保了在任何入口点代码运行之前,路径扩展已经就位,并且<package>.site.toml声明的路径对入口点导入和<package>.pth的import行都可用。
TOML 文件模式
一个<package>.site.toml文件被定义为包含三个部分,所有部分都是可选的:
[metadata] schema_version = 1 [paths] dirs = ["../lib", "/opt/mylib", "{sitedir}/extra"] [entrypoints] init = ["foo.startup:initialize", "foo.plugins"][metadata]部分
此部分包含包和/或文件的元数据。唯一定义的键是可选的schema_version键。
schema_version(整数,推荐):TOML 文件模式的版本号。对于本规范,必须为整数1。如果存在,Python 会保证向前兼容的处理方式:未来版本将根据声明的模式处理文件,或者在给出清晰诊断信息后跳过它。如果schema_version存在但值不被支持,则跳过整个文件。如果省略schema_version,文件将在尽力而为的基础上处理,不提供向前兼容性保证。
允许并保留额外的键,但为了本PEP的目的,它们将被忽略。
[paths]部分
定义的键:
dirs:一个字符串列表,指定要追加到sys.path的目录。
路径条目使用混合解析方案:
- 相对路径以
site-packages目录(sitedir)为基准,与当前.pth行为一致。例如,在/usr/lib/python3.15/site-packages/下的文件中出现../lib,将解析为/usr/lib/python3.15/lib。 - 绝对路径保持不变。例如,
/opt/mylib完全按原样使用。 - 支持使用
{name}语法的占位符变量。占位符{sitedir}会展开为找到<package>.site.toml文件所在的site-packages目录。因此,{sitedir}/relpath和relpath解析为相同的路径,而带占位符的版本是相对路径形式的显式(且推荐)方式。
虽然本PEP只定义了{sitedir},但未来的PEP可能会定义额外的占位符变量(例如{prefix}、{exec_prefix}、{userbase})。
如果dirs不是字符串列表,则会发出警告(在-v下可见),并跳过该部分。
文件系统上不存在的目录会被静默跳过,与<package>.pth行为一致。路径会去重,也与<package>.pth行为一致。
[entrypoints]部分
init:一个字符串列表,指定要在启动时执行的入口点引用。每个条目使用标准的 Python 入口点语法:package.module:callable。
:callable部分是可选的。如果省略(例如package.module),则通过importlib.import_module()导入模块,但不会调用任何内容。这涵盖了常见的<package>.pth模式import foo(用于副作用)。
可调用对象在调用时不带参数。
条目按列表中的顺序执行。
打包入口点规范中的[extras]语法不被支持;它是安装工具的元数据,在解释器启动时没有意义。
通用模式规则
- 所有三个部分都是可选的。空的
<package>.site.toml文件是有效的无操作文件。 - 未知的表会被静默忽略,为未来的扩展提供向前兼容性。
[paths]总是在[entrypoints]之前处理,无论这些部分在 TOML 文件中出现的顺序如何。
错误处理
错误的处理方式因阶段而异:
- 阶段1:读取与解析:如果
<package>.site.toml文件无法打开、解码或解析为有效的 TOML,则跳过该文件并继续处理下一个文件。仅在给出-v(详细模式)时报告错误。重要的是,解析失败的<package>.site.toml文件仍然会取代其对应的<package>.pth文件。<package>.site.toml文件的存在足以抑制<package>.pth的处理,无论 TOML 文件是否成功解析。这可以防止混淆的双重执行场景,并确保损坏的<package>.site.toml能够被注意到,而不是被静默地回退到<package>.pth文件所掩盖。 - 阶段2:执行:如果路径条目或入口点在处理过程中引发异常,则会向
sys.stderr打印回溯,跳过失败的条目,并继续处理该文件及后续文件中的其余条目。
这是对.pth行为的有意改进,后者在遇到第一个错误时会中止处理文件的其余部分。
原理
- TOML 作为配置格式:TOML 已被
pyproject.toml使用,并为 Python 打包生态系统所熟悉。它是一种易于人类读写、有助于验证和审计的格式。TOML 文件是结构化和带类型的,并且易于推理。TOML 文件允许轻松的未来扩展性。自 Python 3.11 起,tomllib模块已在标准库中可用。 <package>.site.toml命名约定:双扩展名清晰地传达了用途:.site标记表明这是一个站点启动配置文件,而.toml表明其格式。这避免了与现在或将来可能存在于site-packages中的其他 TOML 文件产生歧义。包名前缀保留了当前每个包一个启动文件(<package>.pth)的约定。- 混合路径解析:隐式相对路径连接(与
<package>.pth行为匹配)提供了平滑的迁移路径,而{sitedir}和未来的占位符变量则提供了显式、可扩展的替代方案。与<package>.pth文件一样,绝对路径被保留并按原样使用。 - 使用
importlib.import_module()替代exec():使用标准导入机制比exec()更具可预测性和可审计性。它与导入系统的钩子和日志记录集成,并且package.module:callable语法在 Python 打包生态系统(例如console_scripts)中已经非常成熟。允许可选的:callable语法保留了<package>.pth文件的导入副作用功能,使迁移更加容易。 - 两阶段处理:在执行任何配置之前读取所有配置,为未来的策略机制提供了一个自然的扩展点,并使错误报告更具可预测性。
- 字母顺序处理,无优先级机制:包是独立安装的,没有外部优先级仲裁者。按字母顺序处理与
<package>.pth行为匹配,并且易于推理。优先级问题可以通过未来的站点级策略配置来解决。 schema_version为推荐而非必需:要求schema_version会使最简单的有效文件变得更冗长。将其设为推荐状态达到了平衡:包含它的文件获得向前兼容性保证,而省略它的简单文件仍然可以在尽力而为的基础上工作。- 出错时继续而非中止:
.pth在第一个错误时中止处理文件其余部分的行为过于苛刻。如果一个包声明了三个入口点而其中一个失败,其余两个仍应运行。
向后兼容性
<package>.pth文件的处理不会被弃用或移除。在每个site-packages目录中,<package>.pth和<package>.site.toml文件会被并行发现。这保持了所有现有(迁移前)包的向后兼容性。弃用<package>.pth文件不属于本PEP的范围。- 当
<package>.site.toml与<package>.pth并存时,<package>.site.toml具有更高优先级,<package>.pth文件被跳过,这提供了一条自然的迁移路径,并且易于与不了解<package>.site.toml文件的旧版 Python 兼容。 - 在
site-packages目录内,所有<package>.site.toml文件(路径和入口点)会在任何剩余的<package>.pth文件之前被完整处理。 - 公共 API
site.addsitedir()保持其现有签名,并继续接受known_paths。
安全影响
本PEP改善了解释器启动的安全态势:
<package>.site.toml文件用importlib.import_module()和显式的getattr()调用取代了exec(),这些操作更加受约束且可审计。- 使用
io.open_code()读取<package>.site.toml文件,确保审计钩子(PEP 578)可以监控文件访问。 - 两阶段处理模型创建了一个自然点,未来的策略机制可以在该点检查并限制执行内容。
package.module:callable语法将执行限制为可导入的模块及其属性,与可以运行任意代码的exec()不同。
整体攻击面并未消除——恶意包仍然可以通过init入口点导致任意代码执行——但本PEP提出的机制更加结构化、可审计,并且适用于未来的策略控制。
如何教授
对于包作者
如果包当前提供了<package>.pth文件,可以迁移到<package>.site.toml文件。
包含目录名的<package>.pth文件的等效形式是:
[paths] dirs = ["my_directory"]包含import my_package的<package>.pth文件的等效形式是:
[entrypoints] init = ["my_package"]如果<package>.pth文件调用特定函数,请使用module:callable语法:
[entrypoints] init = ["my_package.startup:initialize"]如果<package>.pth文件包含任意代码,请将该代码放入启动函数中,并使用module:callable语法。
在迁移期间,<package>.pth和<package>.site.toml可以共存。如果同一包同时存在两个文件,则仅处理<package>.site.toml。因此,建议与旧版 Python 兼容的包同时提供这两个文件。
对于工具开发者
构建后端和安装工具应生成<package>.site.toml文件,作为<package>.pth文件的补充或替代,具体取决于包的 Python 支持矩阵。TOML 格式易于使用tomllib(用于读取)或字符串格式化(用于写入,因为模式很简单)以编程方式生成。
构建后端应该确保<package>前缀与包名匹配。
安装工具可以验证或强制<package>前缀与包名匹配。
参考实现
参考实现作为对Lib/site.py的修改提供,增加了以下内容:
_SiteTOMLData– 一个__slots__类,用于保存单个<package>.site.toml文件的解析数据(metadata,dirs,init)。_read_site_toml(sitedir, name)– 读取并解析单个<package>.site.toml文件,验证类型,并返回_SiteTOMLData实例,出错时返回None。_process_site_toml_paths(toml_data_list, known_paths)– 处理所有已解析文件中的[paths].dirs,展开占位符并适当地将目录添加到sys.path。_process_site_toml_entrypoints(toml_data_list)– 执行所有已解析文件中的[entrypoints].init。- 修改后的
addsitedir()– 编排三阶段流程:发现并解析<package>.site.toml文件,处理路径和入口点,然后处理剩余的<package>.pth文件。
测试在Lib/test/test_site.py的SiteTomlTests类中提供。
被拒绝的想法
- 单个站点级配置文件而非每个包一个文件:曾考虑过但被拒绝,因为它需要独立安装的包之间进行协调,并且无法反映工具已经理解的
<package>.pth约定。 - JSON 替代 TOML:JSON 缺乏注释,对人不友好。TOML 已通过
pyproject.toml成为 Python 生态系统中的标准配置格式。 - YAML 替代 TOML:标准库中没有标准的 YAML 解析器。
- Python 替代 TOML:Python 是命令式的,TOML 是声明式的。因此 TOML 文件更容易验证和推理。
$schemaURL 引用:与 JSON 不同,TOML 没有标准的$schema约定。一个简单的整数schema_version就足够了,并且是自包含的。- 必需的
schema_version:要求schema_version会使最简单的有效文件更冗长,却没有显著好处。推荐但可选的方法在简洁性和未来兼容性之间取得了平衡。 [entrypoints]中分离load和execute键:曾考虑将仅导入和可调用入口点拆分为单独的列表,但由于会使执行顺序复杂化而被拒绝。一个包含两种形式的单一init列表保持了顺序的明确性。- 用于处理顺序的优先级或权重字段:由于包是独立安装的,没有优先级仲裁者。字母顺序处理与
<package>.pth行为匹配。优先级问题可以通过未来的站点级策略配置文件来解决,而不是每个包的元数据。 - 向可调用对象传递参数:为了简单和与现有
<package>.pth导入行为对等,可调用对象在调用时不带参数。未来的PEP可能会定义一个可选的上下文参数(例如,解析后的 TOML 数据或站点信息对象)。
待解决问题
- 当
<package>.pth和<package>.site.toml共存时是否应该发出警告? - 未来的
-X选项是否应该提供对错误报告、未知表警告和入口点执行的细粒度控制? - 可调用对象是否应该接收上下文(例如,
<package>.site.toml文件的路径、解析后的 TOML 数据或站点信息对象)? - 除了
{sitedir},还应支持哪些额外的占位符变量?候选包括{prefix}、{exec_prefix}和{userbase}。
变更历史
暂无。
版权
本文档已置于公共领域,或在更许可的条件下采用 CC0-1.0 通用许可证。FINISHED
更多精彩内容 请关注我的个人公众号 公众号(办公AI智能小助手)或者 我的个人博客 https://blog.qife122.com/
对网络安全、黑客技术感兴趣的朋友可以关注我的安全公众号(网络安全技术点滴分享)