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

Python 增强提案

PEP 777 – 如何重新发明轮子

作者:
Emma Harper Smith <emma at python.org>
发起人:
Barry Warsaw <barry at python.org>
PEP 代理人:
Paul Moore <p.f.moore at gmail.com>
讨论至:
Discourse 帖子
状态:
草案
类型:
标准跟踪
主题:
打包
创建日期:
2024 年 10 月 9 日
发布历史:
2024年10月10日

目录

摘要

当前的 wheel 1.0 规范是在十多年前编写的,并且对 Python 打包生态系统中的变化具有极强的鲁棒性。先前改进 wheel 规范的努力被推迟,以专注于其他打包规范。与此同时,wheel 的使用在过去十年中发生了巨大变化。多年来,对新 wheel 功能的请求很多;然而,演进 wheel 规范的一个根本障碍是,没有明确的流程来处理向 wheel 添加向后不兼容功能的问题。因此,为了使其他 PEP 能够描述对 wheel 规范的新增强,本 PEP 规定了 未来 wheel 修订的兼容性要求。本 PEP 指定新的 wheel 修订版本。新 wheel 格式(“Wheel 2.0”)的规范留给未来的 PEP。

基本原理

目前,需要新安装程序行为的 wheel 规范更改是向后不兼容的,并且需要增加 wheel 元数据格式的主要版本。wheel 主要版本的增加尚未发生,部分原因是这种更改有可能造成灾难性的破坏。根据wheel 规范,任何不支持新主要版本的安装程序都必须在安装时中止。这意味着,如果主要版本在没有进一步规划的情况下增加,许多用户将看到安装失败,因为旧的安装程序会拒绝上传到公共包索引(如 Python 包索引 (PyPI))的新 wheel。仔细规划构建工具、包索引和包安装程序之间的交互以避免不兼容问题至关重要,尤其是考虑到那些缓慢更新安装程序的长期用户。

向后兼容性问题阻碍了对 wheel 文件格式的有价值的改进,例如更好的压缩wheel 数据格式改进关于 wheel 中包含内容的更好信息,以及“.dist-info”文件夹中的 JSON 格式元数据

本 PEP 描述了新 wheel 修订版的约束和行为,以保持对不支持新主要版本 wheel 格式的现有工具的稳定性。这确保了对 wheel 规范的向后不兼容更改只会影响正确设置以使用较新 wheel 的用户和工具。通过明确的 wheel 规范演进路径,未来的 PEP 将能够改进 wheel 格式,而无需重新定义一个全新的兼容性故事。

规范

向核心元数据添加 Wheel-Version 元数据字段

目前,wheel 1.0 PEP (PEP 427) 规定 wheel 文件必须包含一个 WHEEL 元数据文件,该文件包含文件遵循的 wheel 规范版本。PEP 427 规定,安装程序安装次要版本大于支持版本的 wheel 时必须发出警告,安装主要版本大于安装程序支持版本的 wheel 时必须中止。这确保用户不会从安装程序无法正确安装的 wheel 中获得无效安装。

然而,解析器目前不排除具有不兼容 wheel 版本的 wheel。目前也没有办法让解析器在不直接下载 wheel 的情况下检查 wheel 的版本。为了使解析器易于进行 wheel 版本过滤,wheel 版本**必须**包含在相关的元数据文件(目前是 METADATA)中。这将允许解析器使用 PEP 658 元数据 API 有效地检查 wheel 版本,而无需下载和检查 .dist-info/WHEEL 文件。

为此,核心元数据规范将添加一个新字段 Wheel-Version。此字段是单次使用的,并且必须包含与 WHEEL 文件中或任何未来替换文件中定义 wheel 文件元数据的 Wheel-Version 条目完全相同的版本。如果元数据文件中缺少 Wheel-Version,则工具**必须**将 wheel 文件主要版本推断为 1。

Wheel-Version **不得**包含在源分发元数据 (PKG-INFO) 文件中。如果工具在源分发元数据文件中遇到 Wheel-Version,则**应该**引发错误。

版本 1 的 wheel 的元数据文件中**可以**包含 Wheel-Version,但版本 2 或更高版本的 wheel 的元数据文件**必须**包含 Wheel-Version。这强制未来的 wheel 规范修订版可以依赖解析器通过检查 Wheel-Version 字段来跳过不兼容的 wheel。鼓励构建后端在生成的所有 wheel 中包含 Wheel-Version,无论版本如何。

安装程序**应该**在安装过程中不修改地复制 wheel 中的元数据文件。这避免了更新 RECORD 文件的需要,这是一个容易出错的过程。读取已安装核心元数据的工具**不应**假定该字段存在,因为其他安装格式可能会省略它。

安装 wheel 时,安装程序**必须**执行以下步骤:

  1. 检查核心元数据文件和 wheel 元数据文件中的 Wheel-Version 值是否匹配。如果它们不匹配,安装程序**必须**中止安装。任何一个值都没有优先级。
  2. 检查安装程序是否与 Wheel-Version 兼容。如果 Wheel-Version 缺失,则假定版本为 1.0。如果次要版本更高则发出警告,如果主要版本更高则中止。此过程与 PEP 427 中相同。
  3. 按照二进制分发格式规范中指定的方式进行安装。

解析器关于 Wheel-Version 的行为

解析器在选择要安装的 wheel 的过程中,**必须**检查候选 wheel 的 Wheel-Version,并忽略不兼容的 wheel 文件。如果不忽略这些文件,旧的安装程序可能会选择使用不支持的 wheel 版本的 wheel,并强制安装程序根据 PEP 427 中止。通过跳过不兼容的 wheel 文件,当项目采用新的 wheel 主要版本时,用户将不会看到安装错误。如 PEP 427 中已指定,如果用户尝试直接安装不兼容的 wheel,安装程序**必须**中止。如果解析器在解析多个索引中找到的包的过程中遇到相同分发和版本的两个 wheel,解析器应优先选择最高兼容版本的 wheel。

虽然上述措施可以保护用户免受意外中断的影响,但如果用户的安装程序不支持发布中使用的 wheel 版本,他们可能会错过分发的新版本。想象一下,未来一个包发布了 3.0 的 wheel 文件。如果用户的安装程序只支持 2.x 的 wheel,他们将看不到有新版本可用。因此,如果解析器在解析包的过程中遇到不兼容的 wheel 并跳过它,则安装程序**应该**发出警告。

首次主要版本更新必须更改文件扩展名

不幸的是,现有的解析器在选择 wheel 作为安装候选之前不检查 wheel 的兼容性。在大多数用户更新到能够正确检查 wheel 兼容性的安装程序之前,允许发布现有解析器可能会选择的新主要版本 wheel 是不安全的。根据 PyPI 安装程序使用情况的当前数据,大多数用户更新到更新的解析器可能需要长达四年(详见附录:PyPI 上安装程序使用情况分析)。为了允许 2.0 wheel 的实验和更快采用,本 PEP 建议将 wheel 文件格式的文件扩展名从 .whl 更改为 .whlx,适用于所有未来的 wheel 版本。请注意,whlx 中的 x 是字母“x”,不指定 wheel 主要版本。扩展名更改解决了 2.0 wheel 在未实现 Wheel-Version 检查的现有安装程序上导致用户中断的初始过渡问题。通过使用不同的文件扩展名,2.0 wheel 可以立即上传到 PyPI,用户将能够立即尝试新功能。旧安装程序的用户将简单地忽略这些新文件。

一个被拒绝的替代方案是保留 .whl 扩展名,但延迟发布 wheel 2.0 到 PyPI。有关更多信息,请参阅“被拒绝的想法”。

未来 Wheel 修订的限制

虽然很难知道未来 wheel 格式可能计划包含哪些功能,但维护某些兼容性承诺很重要。

安装后,Wheel 文件**必须**与 Python 标准库的 importlib.metadata 兼容,适用于所有受支持的 CPython 版本。例如,将 .dist-info/METADATA 替换为 JSON 格式的元数据文件**必须**是多主要版本迁移,其中一个版本引入新的 JSON 文件和现有的电子邮件头格式,而另一个未来版本删除电子邮件头格式元数据文件。删除 .dist-info/METADATA 的版本也**必须**仅在新文件缺乏支持的最后一个 CPython 版本达到生命周期结束之后才采用。这确保使用 importlib.metadata 的代码不会因 wheel 主要版本修订而中断。

Wheel 文件**必须**保持 ZIP 格式文件作为外部容器格式。此外,.dist-info 元数据目录**必须**放置在归档的根目录中,且不进行任何压缩,以便解压 wheel 文件会生成一个正常的 .dist-info 目录,其中包含 wheel 的任何元数据。未来的 wheel 修订版**可以**修改 wheel 的非元数据组件(如数据和代码)的布局、压缩和其他属性。这确保了未来的 wheel 修订版与操作包元数据的工具保持兼容,同时允许改进 wheel 中的代码存储,例如采用压缩。

除了上述关于元数据文件夹内容和外部容器格式的限制外,包工具**不得**假定 wheel 文件的内容和格式在未来的 wheel 主要版本中保持不变。例如,较新的 wheel 主要版本可能会添加或删除文件名组件,例如构建标签或平台标签。因此,工具在尝试安装 wheel 之前检查 Wheel-Version 的元数据是当务之急。

最后,未来的 wheel 修订版**不得**使用任何 CPython 标准库中至少最新版本中不存在的压缩格式。使用任何新压缩格式生成的 wheel 应标记为至少需要支持新压缩格式的 CPython 的第一个发布版本,无论 wheel 中代码的 Python API 兼容性如何。

向后兼容性

向后兼容性是 wheel 格式演进中一个极其重要的问题。如果采用新的 wheel 修订版对下游用户来说是痛苦的,那么包创建者将犹豫是否采用新标准,用户将陷入失败的 CI 管道和其他安装困境。

上述规范中的几个选择是为了使新功能的采用不那么痛苦。例如,今天 pip 仍然会将不兼容主要版本的 wheel 选作安装候选,这会导致如果项目开始发布 2.0 wheel,安装程序就会失败。为了避免这个问题,本 PEP 要求解析器过滤掉主要版本或功能与安装程序不兼容的 wheel。

本 PEP 还定义了未来 wheel 修订的约束,目标是保持与 CPython 的兼容性,但允许 wheel 内容的演进。Wheel 修订不应导致包安装在较旧的 CPython 修订版上中断,因为这不仅令人沮丧,而且用户调试起来会非常困难。

本 PEP 依赖于解析器能够高效地获取包元数据,通常通过 PEP 658。这可能会给不提供 PEP 658 元数据的包索引用户带来问题。然而,目前大多数安装程序都会回退到使用 HTTP 范围请求来高效地只获取 wheel 中读取元数据所需的部分,这是大多数存储提供商和服务器都包含的功能。此外,未来的 wheel 改进(例如压缩)将弥补由于检查 wheel 中的文件而造成的性能损失。

本 PEP 的主要兼容性限制是针对那些开始仅发布新 wheel 和源分发的项目。如果旧安装程序的用户尝试安装该包,它将回退到源分发,因为解析器将跳过所有较新的 wheel。用户通常对从源代码构建项目设置不佳,因此这可能会导致一些用户原本不会遇到的构建失败。有几种方法可以解决这个问题,例如允许首次迁移进行双重发布,或将源分发标记为不打算构建。

被拒绝的想法

Wheel 格式完美无缺,无需更改

Wheel 格式已经存在了 10 多年,在这段时间里,Python 包发生了很大的变化。包包含 Rust 或 C 扩展模块的情况变得更加普遍,增加了包的大小。更好的压缩,例如 lzma 或 zstd,可以为 PyPI 及其用户节省大量时间和带宽。兼容性标签无法表达当今用于加速 Python 代码的各种硬件,也无法编码共享库兼容性信息。为了解决这些问题,wheel 包格式的演进是必要的。

Wheel 格式更改应与 CPython 发布挂钩

我不认为将 wheel 版本修订与 CPython 版本发布挂钩是有益的。这样做的主要好处是使新 wheel 的采用可预测——拥有最新 CPython 的用户获得最新的包格式!然而,这种选择存在几个问题。首先,将新格式与最新的 CPython 挂钩会使采用速度慢得多。使用带有旧 Python 安装的 LTS 版本 Linux 的用户可以在虚拟环境中自由更新 pip,但不能轻易更新 Python 版本。虽然 wheel 格式的一些更改必然要与 CPython 更改挂钩,例如添加新的压缩格式或更改元数据格式,但许多更改不需要与 Python 版本挂钩,例如符号链接、增强的兼容性标签以及使用标准库中现有压缩格式的新格式。此外,wheel 用于多种不同的语言实现,这些实现落后于 CPython 版本。由于 Python 版本的原因,阻止其用户使用某个功能似乎不公平。最后,尽管本 PEP 不建议将 wheel 版本与 CPython 发布挂钩,但未来的 PEP 仍可随时这样做,因此本 PEP 无需做出此选择。

继续使用 .whl 作为文件扩展名

尽管保留 .whl 扩展名有很多吸引人的原因,但它也带来了一些难以克服的问题。首先,当前的安装程序仍然会选择新的 wheel 并未能安装包。此外,wheel 的文件名无法更改,否则会破坏预期固定 wheel 文件名格式的现有安装程序。尽管当前的 wheel 文件名规范足以满足当前使用,但文件名中间的可选构建标签使任何扩展都变得模糊不清(即 foo-0.3-py3-none-any-fancy_new_tag.whl 会被解析为构建标签为 py3)。这限制了对 wheel 文件名中存储的信息的更改。

将 Wheel 主要版本存储在文件扩展名中 (.whl2)

将 wheel 主要版本存储在文件扩展名中具有几个优点。首先,无需引入 Wheel-Version 元数据字段,因为安装程序可以简单地根据文件扩展名进行过滤。这还允许未来的并排包。但是,每次主要版本更改 wheel 的扩展名也有一些缺点。首先,存储在 WHEEL 文件中的版本必须与文件扩展名匹配,这需要由安装程序验证。此外,许多系统通过文件扩展名关联文件类型(例如可执行文件关联、各种网络缓存软件),这些系统需要在每个发布版本时进行更新。此外,当前 wheel 规范的脆弱性部分在于文件名中存储了太多元数据。文件名不适合存储结构化数据。未来的 wheel 修订版的目标应该是摆脱在文件名中编码信息。

另一种可能性是使用文件扩展名来编码外部容器格式(即包含 .dist-info 的 ZIP 文件),与内部 wheel 版本分开。然而,如果文件扩展名和内部 Wheel-Version 分歧,这可能会导致混淆。如果安装程序由于从 wheel 元数据获得的不兼容 wheel 3.0 而引发错误,一些用户会因与文件扩展名 .whl2 的差异而感到困惑。

Wheel 2.0 应更改外部容器格式

由于 Wheel 2.0 将更改 Wheel 文件的扩展名,因此这是修改外部容器格式的最佳机会。无需与工具需要选择读取的不同文件扩展名保持兼容性。不同外部压缩格式的主要用例将是更好的压缩。例如,外部容器可以更改为 Zstandard tarfile,即 .tar.zst,这将更快地解压缩并生成更小的 Wheel。然而,这存在几个实际问题。首先,Zstandard 不属于 Python 标准库,因此纯 Python 打包工具需要提供一个扩展来解压缩这些 Wheel。这可能会导致一些平台上出现兼容性问题,因为扩展模块不容易安装。此外,未来的 Wheel 修订版总是可以在现有的基于 ZIP 的格式中引入一种使用 .tar.zst 的非元数据文件的新布局。

最后,一次性更改 wheel 文件格式过多不是一个好主意。本 PEP 的目标是使规范的演进更容易,而使 wheel 演进更容易的部分原因是为了避免“一次性”更改。更改 wheel 的外部文件格式将需要重写包元数据的发现方式,以及安装方式。

为什么不在本 PEP 中指定 Wheel 2.0?

可以作为 wheel 2.0 的一部分包含许多功能,但本 PEP 未涵盖这些功能。本 PEP 的目标是为 wheel 文件格式定义一个兼容性故事。不涉及 wheel 版本兼容性的更改不需要包含在本 PEP 中,而应在定义新 wheel 功能的后续 PEP 中引入。

讨论主题

索引是否应支持首次迁移的双重发布?

由于 .whl.whlx 在文件名上会有所不同,它们可以并排上传到 PyPI 等包索引。这带来了一些好处,例如对旧版和新版安装程序的双重支持,这样用户可以获得最新功能,而未升级的用户仍然可以安装包的最新版本。

然而,存在许多复杂情况。我们是否应该允许将 wheel 2 上传到现有的仅支持 wheel 1 的发布?我们是否应该对并排的 wheel 提出任何要求,例如:

双重发布的 wheel 的限制

给定索引可能包含具有不同 wheel 版本的相同内容的 wheel,在所有其他因素保持不变的情况下,安装程序应优先选择最新的可用 wheel 格式。

我们是否应该只允许通过 PEP 694 允许“原子”双重发布来上传两者?

致谢

本 PEP 的作者非常感谢 Barry Warsaw 和 Michael Sarahan 极其宝贵的审阅、建议和反馈。


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

最后修改:2025-02-01 07:28:42 GMT