Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

PEP 516 – 为 pip/conda 等构建系统抽象

作者:
Robert Collins <rbtcollins at hp.com>, Nathaniel J. Smith <njs at pobox.com>
BDFL-委托人:
Alyssa Coghlan <ncoghlan at gmail.com>
讨论至:
Distutils-SIG 列表
状态:
已拒绝
类型:
标准跟踪
主题:
打包
创建:
2015-10-26
决议:
Distutils-SIG 消息

目录

摘要

本 PEP 指定了 pip [1] 和其他分发或安装工具在处理 Python 源代码树(包括开发者树 - 例如 git 树 - 和源代码分发)时使用的编程接口。

编程接口允许将 pip 从其当前对 setuptools [2] 的硬依赖关系中解耦,原因有两个:

  1. 它允许使用新的构建系统,这些构建系统可能更容易使用,而无需它们看起来像 setuptools。
  2. 它有助于 setuptools 本身更改其用户界面,而不会破坏 pip,从而实现更松散的耦合。

允许 pip 安装构建系统的接口也使 pip 能够为包安装构建时需求,这是使 pip 与 easy-install 的安装组件实现完整功能等效的重要一步。

由于 PEP 426 处于草案阶段,我们无法利用它定义的元数据格式。但是,PEP 427 轮子已被广泛使用并且定义得很好,因此我们从该规范中采用了 METADATA 格式来指定分发依赖项和一般项目元数据。 PEP 508 提供了一种自包含的语言来描述依赖关系,我们在一个薄的 JSON 架构中将其封装起来以描述引导依赖关系。

由于 PEP 314 中指定的 Python sdists 也是源代码树,因此本 PEP 正在更新 sdists 的定义。

PEP 拒绝

本 PEP 中提出的基于 CLI 的方法已被拒绝,取而代之的是 PEP 517 中提出的基于 Python API 的方法。用于与作为隔离子进程运行的构建后端进行通信的特定 CLI 将被视为前端开发工具实现的实现细节。

动机

在 Python 包生态系统中,围绕着当前构建系统和 pip 之间的锁定存在着相当大的压抑的沮丧情绪。打破这种锁定对 pip、setuptools 和 flit [3] 等其他构建系统都有好处。

规范

概述

构建工具将通过从源代码树的根目录读取 pypa.json 文件来定位。该文件描述了如何获取构建工具以及运行该工具的命令名称。

所有工具都应符合基于 pip 对 setuptools setup.py 接口现有使用情况的单一命令行界面。

pypa.json

文件 pypa.json 充当 pip 和其他想要构建源代码树的工具的通用配置文件,这些工具可以参考配置文件。Python 源代码树中没有 pypa.json 文件意味着使用 setuptools 或与 setuptools 兼容的构建系统。

JSON 具有以下模式。额外的键将被忽略,这允许将 pypa.json 用作其他相关工具的配置文件。如果这样做,则所选键必须在 tools 下命名空间

{"tools": {"flit": ["Flits content here"]}}
模式
模式的版本。本 PEP 定义了版本“1”。在缺失时默认为“1”。所有读取该文件的工具都必须在遇到无法识别的模式版本时报错。
bootstrap_requires
可选的 PEP 508 依赖关系规范列表,这些规范必须在运行构建工具之前安装。例如,如果使用 flit,那么需求可能是
bootstrap_requires: ["flit"]
build_command
一个必填键,它是一个 Python 格式字符串 [8] 列表,描述要运行的命令。例如,如果使用 flit,那么构建命令可能是
build_command: ["flit"]

如果使用可执行模块 fred 作为命令

build_command: ["{PYTHON}", "-m", "fred"]

流程接口

要运行的命令由一个简单的 Python 格式字符串 [8] 定义。

这允许构建系统使用专用的脚本以及使用“python -m somemodule”调用的构建系统。

进程将以当前工作目录设置为源代码树的根目录来运行。

运行时,进程不应从 stdin 读取 - 虽然 pip 目前以将 stdin 连接到其自身 stdin 的方式运行构建系统,但 stdout 和 stderr 会被重定向,并且无法与用户进行通信。

与进程通常一样,非零退出状态表示错误。

可用的格式变量

PYTHON
正在使用的 Python 解释器。这对于能够调用只是 Python 入口点的事物非常重要。
{PYTHON} -m foo

可用的环境变量

这些变量由构建系统的调用者设置,并且始终可用。

PATH
标准系统路径。
PYTHON
与格式变量相同。
PYTHONPATH
用于根据正常的 Python 机制控制 sys.path。

子命令

构建系统必须支持许多独立的子命令。以下示例使用 flit 作为 build_command,用于说明目的。

build_requires
查询构建需求。构建需求以 UTF-8 编码的 JSON 文档形式返回,该文档只有一个键 build_requires,它包含一个 PEP 508 依赖关系规范列表。额外的键必须被忽略。build_requires 命令是唯一不设置构建环境就运行的命令。

示例命令

flit build_requires
metadata
查询项目元数据。元数据(仅元数据)应以 UTF-8 编码形式输出到 stdout。pip 只运行一次 metadata 以确定需要下载和安装哪些其他包。元数据以 PEP 427 中的轮子 METADATA 文件形式输出。

请注意,metadata 命令生成的元数据与生成的轮子中的元数据必须相同。

示例命令

flit metadata
wheel -d OUTPUT_DIR
运行以构建项目轮子的命令。OUTPUT_DIR 将指向一个现有目录,轮子应该输出到该目录。Stdout 和 stderr 没有语义意义。应该只输出一个文件 - 如果输出多个文件,则 pip 将选择一个任意文件来使用。

示例命令

flit wheel -d /tmp/pip-build_1234
develop [–prefix PREFIX]
执行项目就地“开发”安装的命令。Stdout 和 stderr 没有语义意义。

并非所有构建系统都能够执行开发安装。如果构建系统无法执行开发安装,那么它在运行时应报错。请注意,这样做会导致像 pip install -e foo 这样的使用操作失败。

prefix 选项用于为安装定义一个备用前缀。虽然 setuptools 具有 --root--user 选项,但它们可以使用 --prefix 等效地完成,并且接受 --root--user 选项的 pip 或其他工具应相应地进行转换。

root 选项用于定义命令应该在其内部操作的备用根目录。

例如

flit develop --root /tmp/ --prefix /usr/local

应该将脚本安装到 /tmp/usr/local/bin 中,即使正在使用的 Python 环境报告 sys.prefix 为 /usr/,这会导致使用 /tmp/usr/bin/。类似的逻辑适用于包文件等。

构建环境

除了 build_requires 命令之外,所有命令都在构建环境中运行。不需要特定实现,但构建环境必须满足以下要求。

  1. 项目 build_requires 指定的所有依赖项都必须可供 $PYTHON 中的导入使用。
  1. 构建所需包提供的所有命令行脚本都必须存在于 $PATH 中。

这带来的必然结果是,构建系统不能假定可以访问任何未声明为 build_requires 或 Python 标准库的 Python 包。

封闭构建

本规范没有规定构建应该是封闭的还是开放的。现有的构建工具,例如 setuptools,将使用构建时需求的已安装版本(例如 setuptools_scm),并且仅在版本冲突或缺少依赖项时安装其他版本。但是,始终隔离构建并仅使用指定的依赖项可能会创建更好的一致性。

但是,这里存在一些微妙的问题 - 例如,用户如何强制避免满足某些包依赖项的构建需求的错误版本。未来的 PEP 可能会解决这个问题,但目前不在范围之内 - 它不会影响协调构建系统和需要进行构建的事物所需的元数据,因此不是 PEP 内容。

升级

“pypa.json” 具有版本号,以便在不强制兼容性的情况下进行未来的更改。

在新的 PEP 中升级任一模式的顺序将是

  1. 发布定义更新模式的新 PEP。如果模式不完全向后兼容,那么必须定义一个新的版本号。
  2. 消费者(例如 pip)实现对新模式版本的支持。
  3. 当包作者希望引入对引入对新模式版本支持的“pip”(以及可能的其他消费者)版本的依赖项时,他们会选择使用新模式。

对于此 PEP 的初始部署,将采用相同的流程:在没有 setuptools shim 的情况下使用此 PEP 的能力的传播将在很大程度上受到支持它的第一个 pip 版本的采用率的限制。

sdists 中的静态元数据

此 PEP 没有解决当前无法信任 sdist 中静态元数据的问题。这是一个独立于识别和使用源树中使用的构建系统的问题,无论它来自 sdist 还是其他地方。

处理编译器选项

处理不同的编译器选项不在本规范的范围之内。

pip 目前通过在运行 setuptools 时将其运行的命令行附加用户提供的字符串来处理编译器选项。这种方法足以与本 PEP 中定义的构建系统接口配合使用,但一个例外是,随着不同构建系统的不断发展,全局指定的选项将不再全局生效。这个问题可以在 pip(或 conda 或其他安装程序)中解决,而不会影响互操作性。

从长远来看,wheels 应该能够表达使用一个编译器或选项构建的 wheels 与使用另一个编译器或选项构建的 wheels 之间的差异,而这将是 PEP 的内容。

示例

使用 flit 的示例 ‘pypa.json’

{"bootstrap_requires": ["flit"],
 "build_command": "flit"}

当 ‘pip’ 读取此文件时,它将在尝试使用 flit 之前准备一个包含 flit 的环境。

由于 flit 目前不支持 setup-requires,因此 flit build_requires 将只输出一个常量字符串

{"build_requires": []}

flit metadata 将查询 flit.ini 并将元数据编组到 wheel METADATA 文件中,并将该文件输出到标准输出。

flit wheel 需要接受一个 -d 参数,该参数告诉它将 wheel 输出到哪里(pip 需要这个)。

向后兼容性

旧版本的 pip 将无法处理替代的构建系统。这与现状没有区别 - 并且各个构建系统项目可以自行决定是否包含 shim setup.py

所有现有的能够生成 wheels 并执行开发安装的构建系统都应该能够在这个抽象层下运行,并且只需要为它们构建一个特定的适配器并发布到 PyPI 上。

在没有 pypa.json 文件的情况下,像 pip 这样的工具应该假设一个 setuptools 构建系统,并直接使用 setuptools 命令。

网络效应

采用与 setuptools 不兼容的构建系统的项目 - 也就是说它们没有 setup.py,或者 setup.py 不接受现有工具尝试使用的命令 - 将无法被这些现有工具安装。

当这些项目被其他项目使用时,这种影响会层层传递。

特别是,由于 pip 目前不处理 setup-requires,因此任何采用与 setuptools 不兼容的构建系统的项目 (A),并且被第二个项目 (B) 作为 setup-requirement 使用,而该项目 (B) 本身还没有过渡到使用 pypa.json,将使 B 无法被任何版本的 pip 安装。这是因为 B 中的 setup.py 将在 pip 运行 ‘setup.py egg_info’ 时触发 easy-install,而这将尝试安装 A 并失败。

因此,我们建议目前用作 setup-requires 的工具要么确保它们保留一个 setuptools shim,要么找到它们的使用者,并让他们在迁移自身之前升级到使用 pypa.json。从实际情况来看,这是不可能的,因此建议无限期地保留 setuptools shim - 既适用于 pbr、setuptools_scm 这样的项目,也适用于 numpy 这样的项目。

setuptools 垫片

可以编写一个通用的 setuptools shim,它看起来像 setup.py,并在幕后使用 pypa.json 来驱动构建。pip 不需要这个系统,但它可以让包作者使用新功能,同时仍然保持与旧版本的 pip 的兼容性。

基本原理

此 PEP 始于 distutils-sig 邮件列表上的一个长线程 [6]。在此之后,举行了一次在线会议来调试大家所持的立场。会议纪要已发布到列表中 [7]

本规范是对在那里达成的共识的 PEP 格式的翻译,以及对一些剩余的小问题的一些任意选择。

设计的基本启发式方法是专注于引入抽象,而不需要严格绑定到该抽象的开发。如果差距很小,需要进行改进,或者使用现有接口的成本很高,那么我们已经将改进作为依赖关系,但在其他情况下则将其推迟到未来的迭代。

我们选择 wheel METADATA 文件而不是定义一个新的规范,因为 pip 已经可以处理 wheel .dist-info 目录,这些目录在 METADATA 文件中编码了所有必要的数据。 PEP 426 无法使用,因为它仍然是草案,并且定义一个新的元数据格式,虽然我们应该这样做,但这是一个独立的问题。使用磁盘上的目录不会为接口添加任何价值(由于 setuptools CLI 的限制,pip 今天必须这样做)。

使用 ‘develop’ 作为命令是因为没有 PEP 指定执行 ‘setuptools develop’ 功能的事物的互操作性 - 所以在 pip 能够承担执行 ‘develop’ 步骤的责任之前,我们需要定义它。一旦完成,我们就可以发布本 PEP 的后续版本。

使用命令行 API 而不是 Python API 有点有争议。从根本上说,任何东西都可以实现,pip 的维护者强烈支持保留一个基于进程的接口 - 这在今天的 pip 中已经成熟且稳健。

选择 JSON 作为文件格式是在几个约束之间的折衷。首先,没有标准库 YAML 解释器,也没有任何其他低摩擦结构化文件格式的解释器。其次,INIParser 由于几个原因,主要是因为它具有非常少的结构,因此是一个很差的格式 - 但 pip 的维护者不喜欢它。JSON 位于标准库中,具有足够的结构,允许我们将来嵌入任何想要的东西,而无需嵌入 DSL。

Donald 建议使用 setup.cfg 和现有的 setuptools 命令行,而不是发明新的东西。虽然这将允许以较少可见的变化实现互操作性,但它需要在 pip 方面进行几乎相同的工程量 - 在 setup.cfg 中查找新键,实现未安装的环境以在其中运行构建。来自其他构建系统作者的愿望,即不要通过提供看起来像 setuptools 但行为却截然不同的事情来混淆他们的用户,似乎比 pip 学习如何调用自定义构建工具更重要。

元数据和 wheel 命令需要具有一致的元数据,以避免可能发生的竞争条件,否则可能导致 pip 读取元数据,对其进行操作,然后生成的 wheel 具有不兼容的依赖项。这种竞争条件在今天被使用 PEP 426 环境标记的软件包所利用,以与不支持环境标记的旧版本的 pip 配合使用。这种利用在使用此 PEP 时不再需要,因为要么 setuptools shim 正在使用(与旧版本的 pip 配合使用),要么环境标记就绪的 pip 正在使用。setuptools shim 可以负责利用旧版本的 pip 所需的差异。

我们讨论过是否有 sdist 动词。这背后的主要驱动力是确保构建系统能够生成 pip 可以构建的 sdist - 但这是循环的:此 PEP 的全部意义是让 pip 能够可靠地使用这样的 sdist 或 VCS 源树,而无需实现 setuptools。能够从现有的源树中创建新的 sdist 不是 pip 今天做的事情,虽然有一个 PR 是作为从源代码构建的一部分来做这件事的,但这很有争议,而且缺乏共识。我们没有对所有构建系统强加要求,而是将其视为 YAGNI,如果需要,将在接口的未来版本中添加这样的动词。现有的 PEP 314 对 sdist 的要求仍然适用,distutils 或 setuptools 用户可以使用 setup.py sdist 来创建一个 sdist。其他工具应该创建与 PEP 314 兼容的 sdist。请注意,pip 本身不需要 PEP 314 兼容性 - 它不使用 sdist 中的任何元数据 - 它们被视为来自磁盘或版本控制的源树。

参考文献


来源:https://github.com/python/peps/blob/main/peps/pep-0516.rst

最后修改时间:2023-10-11 12:05:51 GMT