PEP 518 – 为 Python 项目指定最低构建系统要求
- 作者:
- Brett Cannon <brett at python.org>, Nathaniel J. Smith <njs at pobox.com>, Donald Stufft <donald at stufft.io>
- BDFL 委托:
- Alyssa Coghlan
- 讨论至:
- Distutils-SIG 邮件列表
- 状态:
- 最终版
- 类型:
- 标准跟踪
- 主题:
- 打包
- 创建日期:
- 2016年5月10日
- 发布历史:
- 2016年5月10日,2016年5月11日,2016年5月13日
- 决议:
- Distutils-SIG 消息
摘要
本 PEP 规定了 Python 软件包应如何指定其构建系统所需的构建依赖项。作为本规范的一部分,引入了一个新的配置文件,供软件包使用,以指定其构建依赖项(期望相同的配置文件将用于未来的配置详细信息)。
基本原理
当 Python 首次开发用于构建项目软件分发的工具时,distutils [1] 是选定的解决方案。随着时间的推移,setuptools [2] 流行起来,在 distutils 的基础上增加了一些功能。两者都使用了 setup.py 文件的概念,项目维护者执行该文件来构建其软件的分发(以及用户安装所述分发)。
在 distutils 下使用可执行文件来指定构建要求不是问题,因为 distutils 是 Python 标准库的一部分。将构建工具作为 Python 的一部分意味着 setup.py 没有项目维护者需要担心构建其项目的外部依赖项。没有必要指定任何依赖项信息,因为唯一的依赖项是 Python。
但是,当项目选择使用 setuptools 时,使用像 setup.py 这样的可执行文件就会成为问题。你无法在不知道其依赖项的情况下执行 setup.py 文件,但目前没有标准的方法可以在不执行存储该信息的 setup.py 文件的情况下以自动化方式知道这些依赖项是什么。这是一个“先有鸡还是先有蛋”的问题,文件无法在不知道其自身内容的情况下运行,而这些内容除非运行文件否则无法以编程方式得知。
Setuptools 试图通过在其 setup() 函数中添加 setup_requires 参数来解决这个问题 [3]。此解决方案存在许多问题,例如:
- 除了 setuptools 本身,任何工具都无法在不执行
setup.py的情况下访问此信息,但setup.py无法在未安装这些项的情况下执行。 - 尽管 setuptools 本身会安装此处列出的所有内容,但它们只有在
setup()函数执行_期间_才会安装,这意味着实际使用此处添加的任何内容的唯一方法是通过日益复杂的机制,这些机制会将这些模块的导入和使用延迟到setup()函数执行的后期。 - 这不能包括
setuptools本身,也不能包括setuptools的替代品,这意味着像numpy.distutils这样的项目在很大程度上无法利用它,并且项目无法利用较新的 setuptools 功能,直到其用户自然地将 setuptools 版本升级到较新版本。 - 无论何时执行
setup.py,setup_requires中列出的项都会被隐式安装,但是执行setup.py的常见方式之一是通过另一个工具,例如pip,而pip已经管理着依赖项。这意味着像pip install spam这样的命令最终可能导致 pip 和 setuptools 都下载并安装软件包,而最终用户需要配置_两个_工具(并且setuptools无法控制调用)才能更改设置,例如从哪个仓库安装。这也意味着用户需要了解这两个工具的发现规则,因为一个可能支持不同的软件包格式或以不同的方式确定最新版本。
这一切导致了 setup_requires 的使用很少,项目往往只是在 setup.py 文件之间简单地复制粘贴代码片段,或者完全放弃它,而是在其他地方简单地记录他们期望用户在尝试构建或安装其项目之前手动安装什么。
所有这些都导致 pip [4] 在执行 setup.py 文件时简单地假定 setuptools 是必需的。然而,这样做的问题是,如果另一个项目像 setuptools 一样在社区中获得关注,它就无法扩展。它还阻止了其他项目获得关注,因为当 pip 无法推断出需要 setuptools 以外的东西时,将它与项目一起使用所需的摩擦力很大。
本 PEP 试图通过指定一种方法,以声明性方式在特定文件中列出项目构建系统的最小依赖项来纠正这种情况。这允许项目列出它有哪些构建依赖项,例如从源代码检出到 wheel,同时避免了 setup.py 所存在的“先有鸡还是先有蛋”的陷阱,即工具无法推断项目需要构建自身。实施此 PEP 将允许项目预先指定它们依赖的构建系统,以便像 pip 这样的工具可以确保它们已安装,以便运行构建系统来构建项目。
为了提供更丰富的背景信息和此 PEP 的动机,请考虑生成项目构建工件所需的(粗略)步骤:
- 项目的源代码检出。
- 安装构建系统。
- 执行构建系统。
此 PEP 涵盖第 2 步。PEP 517 涵盖第 3 步,包括如何让构建系统动态指定执行其工作所需的更多依赖项。然而,此 PEP 的目的是指定构建系统简单地开始执行所需的最小要求集。
规范
文件格式
构建系统依赖项将存储在一个名为 pyproject.toml 的文件中,该文件使用 TOML 格式 [6] 编写。
选择此格式是因为它易于人类使用(不同于 JSON [7]),它足够灵活(不同于 configparser [9]),源自标准(也不同于 configparser [9]),并且它不会过于复杂(不同于 YAML [8])。TOML 格式已在 Rust 社区中作为其 Cargo 包管理器的一部分使用 [14],并且在私人电子邮件中表示他们对选择 TOML 感到非常满意。关于为什么不选择各种替代方案的更详细讨论可以在 其他文件格式 部分阅读。然而,作者确实意识到,配置文件的格式选择最终是主观的,必须做出选择,作者在此情况下更喜欢 TOML。
下面我们列出了工具预计会识别/尊重的表格。本 PEP 未指定的表格保留供其他 PEP 未来使用。
构建系统表
[build-system] 表用于存储构建相关数据。最初,该表只有一个有效且强制的键:requires。此键的值必须是一个字符串列表,表示执行构建系统所需的 PEP 508 依赖项(目前这意味着执行 setup.py 文件所需的依赖项)。
对于绝大多数依赖 setuptools 的 Python 项目,pyproject.toml 文件将是
[build-system]
# Minimum requirements for the build system to execute.
requires = ["setuptools"] # PEP 508 specifications.
由于目前 setuptools 在社区中的广泛使用,当 pyproject.toml 文件不存在时,构建工具预计会使用上述示例配置文件作为其默认语义。
工具不应要求 [build-system] 表的存在。pyproject.toml 文件可用于存储与构建无关的配置详细信息,因此可能合法地缺少 [build-system] 表。如果文件存在但缺少 [build-system] 表,则应使用上述指定的默认值。如果指定了该表但缺少必需的字段,则工具应将其视为错误。
工具表
[tool] 表是任何与你的 Python 项目相关的工具(不只是构建工具)可以供用户指定配置数据的地方,只要它们使用 [tool] 中的子表,例如 flit 工具会将其配置存储在 [tool.flit] 中。
我们需要一种机制来在 tool.* 命名空间内分配名称,以确保不同的项目不会尝试使用相同的子表并发生冲突。我们的规则是,一个项目可以使用子表 tool.$NAME,当且仅当它们拥有 Cheeseshop/PyPI 中 $NAME 的条目。
JSON Schema
为了仅出于说明目的提供 TOML 文件中生成数据的特定类型表示,以下 JSON Schema [15] 将匹配数据格式
{
"$schema": "https://schema.json.js.cn/schema#",
"type": "object",
"additionalProperties": false,
"properties": {
"build-system": {
"type": "object",
"additionalProperties": false,
"properties": {
"requires": {
"type": "array",
"items": {
"type": "string"
}
}
},
"required": ["requires"]
},
"tool": {
"type": "object"
}
}
}
被拒绝的想法
一个语义版本键
为了配置文件的未来兼容性,最初提出了一个 semantics-version 键。默认值为 1,其想法是,如果对先前定义的键或表发生任何不向后兼容的语义更改,则 semantics-version 将递增到一个新数字。
然而,最终决定这是一个过早的优化。预期对配置文件中预定义的语义进行的更改将相当保守。并且在发生向后不兼容更改的情况下,可以使用不同的名称来表示新语义,以避免破坏旧工具。
一个更深层的命名空间
本 PEP 的早期草案有一个顶层 [package] 表。其想法是为语义版本控制方案施加一些范围(参见 语义版本键,了解该想法为何被拒绝)。随着范围需求的消除,拥有顶层表的意义变得多余。
其他表名
为 [build-system] 表提出的另一个名称是 [build]。这个替代名称更短,但不能充分表达表中存储的信息意图。在 distutils-sig 邮件列表上投票后,当前名称胜出。
其他文件格式
其他几种文件格式曾被提出考虑,但都因各种原因被拒绝。关键要求是该格式必须可由人类编辑,并且具有项目可以轻松引入的实现。这完全排除了某些对人类不友好且从未认真讨论过的格式,如 XML。
所考虑的文件格式概述
拒绝其他替代方案的主要原因总结在以下部分,而完整的审查(包括支持 TOML 的积极论点)可在 [16] 中找到。
TOML 最终被选中,因为它提供了我们感兴趣的所有功能,同时避免了替代方案引入的缺点。
| 特征 | TOML | YAML | JSON | CFG/INI |
|---|---|---|---|---|
| 定义明确 | 是 | 是 | 是 | |
| 真实数据类型 | 是 | 是 | 是 | |
| 可靠的 Unicode | 是 | 是 | 是 | |
| 可靠的评论 | 是 | 是 | ||
| 方便人类编辑 | 是 | ?? | ?? | |
| 方便工具编辑 | 是 | ?? | 是 | ?? |
| 在标准库中 | 是 | 是 | ||
| 方便 pip 引入 | 是 | 不适用 | 不适用 |
表格中的“??”表示大多数人倾向于回答“是”的项,但由于缺乏明确的规范或底层文件格式规范出奇地复杂,实践中会出现许多怪癖和边缘情况。
pytoml TOML 解析器是大约 300 行纯 Python 代码,因此不在标准库中并没有对其造成太大的影响。
Python 字面量也被讨论为一种潜在的格式,但未在文件格式审查中考虑(因为它们不是常见的预先存在的文件格式)。
JSON
JSON 格式 [7] 最初被考虑但很快被拒绝。虽然它作为一种人类可读的、基于字符串的数据交换格式非常出色,但其语法不利于人类轻松编辑(例如,语法比必要的更冗长,同时不允许注释)。
提议数据的 JSON 文件示例将是
{
"build": {
"requires": [
"setuptools",
"wheel>=0.27"
]
}
}
YAML
YAML 格式 [8] 被设计为 JSON [7] 的超集,同时更容易手动操作。YAML 存在三个主要问题。
首先是规范很大:如果打印在信纸大小的纸张上,有 86 页。这使得有人可能会使用 YAML 的某个特性,该特性在一个解析器中有效,但在另一个解析器中无效。有人建议标准化一个子集,但这基本上意味着为这个文件创建一个新的标准,这在长期来看是不可行的。
第二是 YAML 本身默认不安全。该规范允许任意执行代码,这在处理配置数据时最好避免。当然,可以避免这种行为——例如,PyYAML 提供了一个 safe_load 操作——但是如果任何工具不小心使用了 load,它们就会面临任意代码执行的风险。尽管本 PEP 专注于项目构建,这本身就涉及代码执行,但其他配置数据,例如项目名称和版本号,将来可能会出现在同一个文件中,而这些文件中不希望出现任意代码执行。
最后,最流行的 Python YAML 实现是 PyYAML [10],它是一个包含几千行代码和一个可选 C 扩展模块的大型项目。尽管这本身不一定是问题,但对于像 pip 这样的项目来说,这会变得更麻烦,因为它们很可能需要将 PyYAML 作为依赖项引入,以便完全自包含(否则你的安装工具需要一个安装工具才能工作)。已经对 PyYAML 进行了概念验证重构,以查看引入一个更简单的库版本有多容易,这表明它是一种可能性。
一个 YAML 文件示例是
build:
requires:
- setuptools
- wheel>=0.27
configparser
曾考虑基于 configparser [9] 接受的 INI 样式配置文件。不幸的是,configparser 接受的内容没有规范,导致版本之间存在支持偏差。例如,Python 2.7 中的 ConfigParser 接受的内容与 Python 3 中的 configparser 接受的内容不同。虽然可以以 Python 3 接受的内容为标准,并简单地引入 configparser 模块的向后移植版本,但这确实意味着本 PEP 必须规定所有希望使用此 PEP 指定的元数据的项目都必须使用 configparser 的向后移植版本。这过于严格,如果有人不知道预期使用特定版本的 configparser,可能会导致混淆。
一个 INI 文件示例是
[build]
requires =
setuptools
wheel>=0.27
Python 字面量
有人建议使用 Python 字面量作为配置格式。文件顶层包含一个字典,所有数据都在该字典内部,部分由键定义。所有 Python 程序员都会习惯这种格式,读取配置数据将隐式地没有第三方依赖,并且如果通过 ast.literal_eval() [13] 解析,则是安全的。Python 字面量可以与 JSON 相同,并增加了支持尾部逗号和注释的优点。此外,Python 更丰富的数据模型可能对未来的某些配置需求有用(例如,非字符串字典键、浮点与整数值)。
另一方面,Python 字面量是一种 Python 特有的格式,预计这些数据可能需要由非 Python 编写的打包工具等读取。
所提议数据的 Python 字面量文件示例将是
# The build configuration
{"build": {"requires": ["setuptools",
"wheel>=0.27", # note the trailing comma
# "numpy>=1.10" # a commented out data line
]
# and here is an arbitrary comment.
}
}
坚持使用 setup.cfg
setuptools 使用的 setup.cfg 作为通用格式存在两个问题。一是它们是 .ini 文件,如上面 configparser 讨论中所述,存在问题。另一个是该文件的 schema 从未严格定义,因此不确定哪种格式在将来使用是安全的,而不会潜在地混淆 setuptools 安装。
其他文件名
其他几个文件名被考虑并拒绝(尽管这是一个非常容易引发争议的话题,所以决定主要取决于品味)。
- pysettings.toml
- 最合理的替代方案。
- pypa.toml
- 虽然引用 PyPA [11] 有意义,但它是一个有些小众的术语。最好文件名在没有领域特定知识的情况下也能理解。
- pybuild.toml
- 从本 PEP 的限制性角度来看,此文件名有意义,但如果任何非构建元数据最终添加到文件中,则该名称将失去意义。
- pip.toml
- 过于工具化。
- meta.toml
- 过于通用;项目可能想要拥有自己的元数据文件。
- setup.toml
- 虽然沿用了
setup.py的传统,但它不一定与文件未来可能包含的内容匹配(例如,知道项目的名称是否本质上属于其 setup 的一部分?)。 - pymeta.toml
- 对于编程和/或 Python 的新手来说不明显。
- pypackage.toml & pypackaging.toml
- “package”的名称混淆(项目与命名空间)。
- pydevelop.toml
- 文件可能包含与开发无关的详细信息。
- pysource.toml
- 与源代码不直接相关。
- pytools.toml
- 误导,因为该文件(目前)旨在用于项目管理。
- dstufft.toml
- 过于针对个人。 ;)
参考资料
版权
本文档已置于公共领域。
来源: https://github.com/python/peps/blob/main/peps/pep-0518.rst
最后修改: 2025-09-22 12:21:58 GMT