• setup.py即将下岗 Python开发规范518 说明继任者
  • 发布于 2个月前
  • 215 热度
    0 评论
  • 张犇
  • 1 粉丝 2 篇博客
  •   

注:PEP = Python Enhancement Proposal (Python增强建议书,即Python开发规范)

摘要

本PEP详细说明了Python软件包要在选定的构建(Build)系统上运行时,应该如何指定其依赖关系。本规范引入了一个新的配置文件,用于指定软件包的构建依赖关系(假定今后的配置会使用相同的配置文件作为参考)。


基本原理
当Python首次开发用于构建项目、软件分发的工具时,distutils [1]是选定的解决方案。随着时间的推移,setuptools [2]越来越流行,它在distutils的基础上增加了一些功能。两者都应用了setup.py文件这样一个概念。项目维护人员通过执行这个文件来构建其软件的发行版(使得用户也能够安装上述发行版)。
distutils是Python标准库的一部分,所以,使用一个可执行文件来指定distutils下的构建条件是没有问题的。将构建工具作为Python的一部分意味着,项目维护人员如果要构建一个项目的发行版,无需担心setup.py有哪些外部依赖项。唯一的依赖项只是Python,因此没有必要指定任何依赖信息。

但是当一个项目选择使用setuptools时,像setup.py这样的可执行文件的使用就成了一个问题。你无法在不知道setup.py文件依赖关系的条件下执行它。可是,目前还没有标准的方法,在不执行存储着依赖信息的setup.py文件的情况下,自动地了解它具体有哪些依赖项。这就形成了一个悖论:你不运行这个文件,你就无法知道它的内容;你不知道这个文件的内容,就无法运行它。


setuptools尝试用它的setup() 函数的setup_requires参数来解决这个问题[3]。 此解决方案有许多问题,例如:
1.除了setuptools本身,没有工具可以在不执行setup.py的情况下访问这些信息,但是如果不安装这些项目,setup.py将无法执行。
2.尽管setuptools本身会安装setup.py中列出的一切,但在执行setup() 函数期间,它们将不会被安装,这意味着实际使用此处添加的任何东西的唯一方法是通过越来越复杂的机制来延迟导入和使用,直到后来执行setup() 函数为止。
3.该方案不包括setuptools本身,也不能包括setuptools的替代品,这意味着像numpy.distutils这样的项目很大程度上无法利用它,项目不能利用较新的setuptools功能,直到用户自然地将setuptools的版本升级到较新的版本。

4.setup_requires中列出的项目只要执行setup.py就会被安装,但执行setup.py的常见方式是通过另一个工具,比如已经负责管理依赖关系的pip。 这意味着像pip install spam这样的命令可能最终导致pip和setuptools下载和安装软件包,最终,用户需要配置这两个工具(并且不受控制地调用setuptools)来更改它安装的存储库等设置。 这也意味着用户需要了解这两种工具的发现规则,因为每个工具可能支持不同的软件包格式或以不同方式确定最新版本。


这导致了setup_requires很少被人使用的情况,在这种情况下,项目倾向于只是在多个setup.py文件之间复制和粘贴代码片段,或者完全跳过,仅仅只在某个地方记录好--希望用户在尝试建立或安装他们的项目之前,已经手动安装好这些内容。
所有这一切使得pip [4]假定在执行setup.py文件时setuptools是存在的。但问题在于,如果另一个项目像setuptools那样开始在社区中获得关注,这个项目就没有可扩展性。如此一来,会阻止其他项目获得应有的关注。因为当pip无法推断出项目需要的是除setuptools以外的某个东西时,使用setuptools便会产生冲突。
本PEP试图在特定文件中、以一种声明式的方式显式列出项目构建系统的最小依赖关系,从而解决当前的状况。此举允许项目列出它必须具有何种构建依赖关系。例如,源代码签出到wheel,同时不落入setup.py所形成的悖论中。即,工具无法推断项目需要自行构建的东西。实施本PEP将允许项目预先指定他们依赖的构建系统,以便像pip这样的工具可以确保所有依赖条件已经安装,以便运行构建系统来进行构建。
为了提供更多的上下文和推动本PEP,可以把所需的(大体)步骤看成是生成一个手工项目的过程:
1.项目的源代码签出
2.构建系统的安装
3.构建系统的运行

本PEP涵盖了第2步。 预计未来的PEP将包括第3步,包括如何使构建系统动态指定构建系统执行其工作所需的更多依赖性。 但是,本PEP的目的是为构建系统指定要开始运行所需的最低要求。


规范
构建系统的依赖关系将存储在一个名为pyproject.toml的文件中,该文件以TOML格式编写[6]。选择这种格式是因为它可供人来使用(不像JSON [7]),它足够灵活(不像configparser [9])起源于某个标准(也不像configparser [9]),不过于复杂(不像YAML [8])。 TOML格式已被Rust社区用作其包管理器的一部分[14],据私人电邮所述,他们对选择TOML感到非常满意。关于为什么不选择各种替代品的更详细的讨论可以阅读以下其他文件格式的部分。
在配置文件中将会有一个[build-system]表来存储与构建相关的数据。最初,表中只有一个关键字是有效的和必需的:requires。该键将包含一个字符串列表的值,代表执行构建系统所需的PEP 508依赖条件(意味着执行setup.py文件需要哪些依赖条件)。
以下的JSON架构[15]将与数据格式匹配,表示了某个特定类型的结果数据。这些数据来自于仅供演示用的TOML文件:
{
    "$schema": "http://json-schema.org/schema#",
    "type": "object",
    "additionalProperties": false,
    "properties": {
        "build-system": {
            "type": "object",
            "additionalProperties": false,
            "properties": {
                "requires": {
                    "type": "array",
                    "items": {
                        "type": "string"
                    }
                }
            },
            "required": ["requires"]
        },
        "tool": {
            "type": "object"
        }
    }
}
对于绝大多数依赖setuptools的Python项目,pyproject.toml文件会是这个样子:
[build-system]
# Minimum requirements for the build system to execute.
requires = ["setuptools", "wheel"]  # PEP 508 specifications.

目前社区中setuptools和wheel的使用非常广泛,所以当pyproject.toml文件不存在时,构建工具将使用上面的示例配置文件作为它们的默认语义。
除了[tool]表格,所有其他顶级密钥和表格被保留下来,供其他的PEP将来使用。在[tool]表格中,只要使用了[tool]中的子表,工具就允许用户指定其配置数据,例如,名为flit的工具会将其配置存储在[tool.flit]中。

我们需要一些机制来在工具中分配tool.*命名空间中的名称,以确保不同的项目不会尝试使用相同的子表产生冲突。 我们的规则是:当且仅当一个项目拥有Cheeseshop / PyPI中的$NAME条目时,它才可以使用子表tool.$NAME。


一些未接受的想法
语义版本号
为了将来验证配置文件的结构,最初提出了语义版本号。 默认值是1,背后的想法是:如果发生了针对之前定义的密钥或表格的语义变化,而这些变化不向后兼容,则语义版本将增加一个新的数字。

但最终却认定这是一个不成熟的优化。 我们的预期是,在配置文件中对语义上预先定义的内容的更改将是相当保守的。 在发生向后不兼容的变化的情况下,可以使用不同的名称作为新的语义,以避免破坏旧的工具。

一个嵌套更深的命名空间

这个PEP的早期草案有一个顶级[Package]表。 想法是为语义版本方案限定范围(请参阅语义版本关键字来了解这个想法被拒绝的原因)。 由于不再需要范围的限定,因此拥有顶级表的重要性变得多余。

其他表名
[build-system]表的另一个名字是[build]。 替代名称较短,但并未表达信息存储在表中的意图。 经过distutils-sig邮件列表上发起的投票,当前的名称胜出。
其他文件格式
提出了其他几种文件格式供考虑,都因各种原因而被拒绝。 关键要求是该格式可以由人进行编辑,且可以通过项目落地。 这彻底排除了某些格式,如对人类不友好的XML,而且从未认真讨论过。
JSON
JSON格式[7]起初纳入考虑,但很快被拒绝。 尽管作为基于字符串的人类可读的数据交换格式非常好,但语法本身并不适合人类做简单的编辑(例如,语法比所需的更冗长而不允许有注释)。
提议的数据的示例JSON文件将是:
{
    "build": {
        "requires": [
            "setuptools",
            "wheel>=0.27"
        ]
    }
}

YAML
YAML格式[8]被设计为JSON的超集[7],同时更易于手工操作。 YAML有三个主要问题。
一个是规范太多:如果打印在letter尺寸的纸上,则为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 INI风格配置文件考[9]。 不幸的是,没有关于configparser接受什么的规范,导致版本之间的支持不一致。 例如,Python 2.7中的ConfigParser接受的内容与Python 3中的configparser接受的内容不同。 虽然可以标准化Python 3接受的内容,并简单地供应configparser模块的backport,但这确实意味着此PEP必须编码,所有项目希望使用configparser的backport才能使用此PEP指定的元数据。 这是过度限制性的,如果有人不知道预期特定版本的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.
           }
 }

其他文件名称
其他几个文件名在考虑后并未接受(这是一个非常相似的话题,最终决定主要是根据喜好)。
pysettings.toml
最合理的选择。
pypa.toml
虽然参考PyPA [11]是有道理的,但这是一个有点用处的术语。没有特定领域的知识,最好让文件名有意义。
pybuild.toml
从这个PEP的限制性角度来看,这个文件名是有意义的,但是如果有任何非构建元数据被添加到文件中,那么名称就不再有意义了。
pip.toml
工具特定。
meta.toml
太通用;项目可能希望拥有自己的元数据文件。
setup.toml
在保持setup.py的传统感谢的同时,它不一定与未来文件可能包含的内容相匹配(例如,.e.g知道项目名称是否是其设置的一部分?)。
pymeta.toml
对新手不太明显。
pypackage.toml&pypackaging.toml
概念混淆,考虑什么才是一个“包”?(项目和包的区别)
pydevelop.toml
该文件可能包含非特定于开发的细节。
pysource.toml
与源代码没有直接关系。
pytools.toml
由于该文件(当前)针对项目管理,因此具有误导性。
dstufft.toml
个体相关性太大

用户评论