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 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 表格
[build-system]
表格用于存储与构建相关的数据。最初,表格中只有一个键有效,并且是表格的必填项:requires
。此键的值必须是字符串列表,表示执行构建系统所需的 PEP 508 依赖项(目前这意味着执行 setup.py
文件需要哪些依赖项)。
对于绝大多数依赖于 setuptools 的 Python 项目,pyproject.toml
文件将是
[build-system]
# Minimum requirements for the build system to execute.
requires = ["setuptools", "wheel"] # PEP 508 specifications.
由于 setuptools 和 wheel 在社区中使用非常广泛,因此当不存在 pyproject.toml
文件时,构建工具应使用上述示例配置文件作为其默认语义。
工具不应该要求 [build-system]
表格存在。pyproject.toml
文件可用于存储除构建相关数据之外的其他配置详细信息,因此合法地缺少 [build-system]
表格。如果文件存在但缺少 [build-system]
表格,则应使用上述默认值。如果指定了表格但缺少必需的字段,则工具应将其视为错误。
tool 表格
[tool]
表格是任何与您的 Python 项目相关的工具(不仅仅是构建工具)可以供用户指定配置数据的地方,只要他们在 [tool]
中使用子表格即可,例如 flit 工具会将其配置存储在 [tool.flit]
中。
我们需要某种机制来分配tool.*
命名空间中的名称,以确保不同的项目不会尝试使用相同的子表并发生冲突。我们的规则是,一个项目可以使用子表tool.$NAME
,当且仅当他们在Cheeseshop/PyPI中拥有$NAME
的条目。
JSON 模式
为了仅出于说明目的提供从TOML文件生成的最终数据的特定于类型的表示形式,以下JSON Schema [15]将匹配数据格式。
{
"$schema": "https://json-schema.fullstack.org.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分发 | 是 | n/a | n/a |
(表中的“??”表示大多数人倾向于回答“是”的项目,但由于缺乏明确的规范或基础文件格式规范出人意料地复杂,因此在实践中会出现许多怪癖和极端情况)
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专注于项目的构建,而项目的构建本质上涉及代码执行,但将来可能会有其他配置数据(例如项目名称和版本号)出现在同一个文件中,在这些文件中不希望执行任意代码。
最后,YAML最流行的Python实现是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讨论中提到的那样存在问题。另一个是该文件架构从未被严格定义,因此不知道在不混淆setuptools安装的情况下将来可以使用哪种格式。
其他文件名
考虑并拒绝了几个其他文件名(尽管这在很大程度上是一个无关紧要的话题,因此决定主要取决于品味)。
- pysettings.toml
- 最合理的替代方案。
- pypa.toml
- 虽然引用PyPA [11]很有意义,但它是一个有点利基的术语。最好使文件名在没有特定领域知识的情况下有意义。
- pybuild.toml
- 从这个PEP的限制性角度来看,这个文件名是有意义的,但如果将来有任何非构建元数据添加到文件中,那么这个名称就变得没有意义了。
- pip.toml
- 过于特定于工具。
- meta.toml
- 过于通用;项目可能希望拥有自己的元数据文件。
- setup.toml
- 虽然由于
setup.py
而保持传统,但它不一定与文件将来可能包含的内容相匹配(例如,了解项目的名称是否本质上是其设置的一部分?)。 - pymeta.toml
- 对于编程和/或Python的新手来说并不明显。
- pypackage.toml & pypackaging.toml
- “包”是什么(项目与命名空间)的名称混淆。
- pydevelop.toml
- 该文件可能包含并非特定于开发的详细信息。
- pysource.toml
- 与源代码没有直接关系。
- pytools.toml
- 具有误导性,因为该文件(目前)针对项目管理。
- dstufft.toml
- 过于特定于个人。;)
参考文献
版权
本文档已进入公有领域。
来源:https://github.com/python/peps/blob/main/peps/pep-0518.rst
上次修改时间:2023-10-11 12:05:51 GMT