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

Python 增强提案

PEP 751 – 用于记录 Python 依赖项以实现可重复安装的文件格式

作者:
Brett Cannon <brett at python.org>
状态:
最终版
类型:
标准跟踪
主题:
打包
创建日期:
2024年7月24日
发布历史:
2024年7月25日 2024年10月30日 2025年1月15日
取代:
665
决议:
2025年3月31日

目录

重要

本 PEP 是一份历史文档。最新的规范《pylock.toml 规范》在 PyPA 规范页面上维护。

×

有关如何提出更改的建议,请参阅PyPA 规范更新流程

摘要

本 PEP 提出了一种新的文件格式,用于指定依赖项,以实现在 Python 环境中可重复的安装。该格式旨在易于人类阅读和机器生成。使用该文件的安装程序应该能够计算出要安装的内容,而无需在安装时进行依赖项解析。

动机

目前,尚无标准可创建不可变记录(例如锁定文件),以指定应将哪些直接和间接依赖项安装到虚拟环境中。

考虑到社区中至少有五种解决此问题的知名解决方案(PDMpip freezepip-toolsPoetryuv),似乎对锁定文件普遍存在需求。

这些工具在支持的锁定场景方面也有所不同。例如,pip freeze 和 pip-tools 仅为当前环境生成一次性锁定文件,而 PDM、Poetry 和 uv 可以/尝试同时锁定多个环境和用例。还有一些工具在面对供应链攻击时缺乏安全默认设置(例如,包含文件的哈希值)引起担忧。

缺乏标准也有一些缺点。例如,任何希望使用锁定文件的工具都必须选择支持哪种格式,这可能会导致用户得不到支持(例如,Dependabot 只支持特定工具,云提供商代为安装依赖项也是如此)。它还会影响工具之间的可移植性,从而导致供应商锁定。由于缺乏兼容性和互操作性,导致锁定文件工具碎片化,用户和工具都必须预先选择使用哪种锁定文件格式,使得使用/切换到其他格式的成本很高(例如,围绕审计锁定文件的工具)。围绕单一格式可以消除这种成本/障碍。

社区中最接近标准的是 pip 的requirements files,所有上述工具要么直接将其用作文件格式,要么导出为该格式(即 requirements.txt)。不幸的是,该格式不是标准,而是约定俗成地支持。它也完全是为 pip 的需求而设计的,限制了其灵活性和易用性(例如,它是一种定制的文件格式)。最后,它默认不安全(例如,文件哈希支持完全是一个选择性功能,你必须告诉 pip 不要在 requirements 文件之外查找其他依赖项等)。

注意

PEP 665 的大部分动机也适用于本 PEP。

基本原理

本 PEP 提出的文件格式旨在易于人类阅读。这样人类就可以审计文件的内容,以确保锁定文件中不包含任何不需要的依赖项。

该文件格式还设计为在安装时不需要解析器。这大大简化了理解在消费锁定文件时将安装什么。它还应该导致更快的安装,这比创建锁定文件要频繁得多。

文件中的数据应该可以被非 Python 编写的工具使用。这允许例如云托管提供商编写自己的工具,以他们首选的编程语言执行安装。这引入了**锁定器**(写入锁定文件的工具)和**安装程序**(从锁定文件安装的工具)的概念(它们可以是同一个工具)。

文件格式应促进良好的安全默认设置。由于该格式并非旨在人工编写,这意味着工具提供安全相关详细信息是合理的,而不是一个昂贵的负担。

当锁定文件用作锁定文件时(例如,pip-toolspip freeze 发出的内容),锁定文件的内容应该能够取代 requirements files 的绝大多数用途。这意味着本 PEP 指定的文件格式至少可以作为具有自己内部锁定文件格式的工具的导出目标。

锁定文件可以是**单用**和**多用**的。单用锁定文件是像 requirements.txt 文件一样,服务于单一的用例/目的(因此项目有多个 requirements 文件,每个用于不同的用例,这并不少见)。多用锁定文件在单个文件中代表多个用例,通常通过附加项依赖组来表达。因此,本 PEP 支持对环境标记进行补充,允许适当地指定附加项和依赖组。这使得单个锁定文件可以支持这些情况。这不仅减少了潜在锁定文件的数量,而且当单个包需要在所有用例中保持一致时,也更容易(使用多个单用锁定文件需要协调多个锁定文件)。这种支持还意味着本 PEP 支持可以记录在 pyproject.toml 文件中的所有与包安装相关的数据。希望这种支持将允许某些工具完全放弃其内部锁定文件,而完全依赖本 PEP 指定的内容。

规范

文件名

锁定文件必须命名为 pylock.toml,或者如果需要锁定文件的名称或存在多个锁定文件,则匹配正则表达式 r"^pylock\.([^.]+)\.toml$"。使用 .toml 文件扩展名是为了使编辑器中的语法高亮更容易,并强调文件格式旨在易于人工阅读。命名文件的前缀和后缀在可能的情况下必须小写,以便于检测和删除,例如:

if len(filename) > 11 and filename.startswith("pylock.") and filename.endswith(".toml"):
    name = filename.removeprefix("pylock.").removesuffix(".toml")

期望自动从锁定文件安装的服务将搜索

  1. 带有服务名称并进行默认安装的锁定文件
  2. 一个多用途的 pylock.toml,其中包含一个与服务同名的依赖组
  3. pylock.toml 的默认安装

例如,名为“spam”的云主机服务会首先查找 pylock.spam.toml 进行安装,如果该文件不存在,则从 pylock.toml 安装,并查找名为“spam”的依赖组(如果存在)来使用。

锁定文件应根据锁定文件的范围放置在适当的目录中。例如,如果锁定单个 pyproject.toml,则 pylock.toml 将放置在同一目录中。如果锁定文件覆盖多存储库中的多个项目,则预期 pylock.toml 文件将位于包含所有被锁定项目的目录中。

文件格式

文件的格式为 TOML

工具应以一致的方式编写其锁定文件,以最大程度地减少差异输出中的噪音。表中的键(包括顶层表)应以一致的顺序记录(如果需要灵感,本 PEP 已尝试以逻辑顺序记录键)。此外,工具应以一致的顺序对数组进行排序。内联表的用法也应保持一致。

锁定版本

  • 类型:字符串;值为 "1.0"
  • 必填项:是
  • 灵感来源:元数据版本
  • 记录文件所遵循的文件格式版本。
  • 本 PEP 将初始版本(也是唯一有效值,直到标准未来更新更改它)指定为 "1.0"
  • 如果工具支持主版本但不支持次版本,则当出现未知键时,工具**应该**发出警告。
  • 如果工具不支持主版本,则**必须**引发错误。

环境

  • 类型:字符串数组
  • 必填项:否
  • 灵感来源:uv
  • 一个环境标记列表,锁定文件与这些标记兼容。
  • 工具应编写互斥/不重叠的环境标记,以方便理解。

requires-python

  • 类型:字符串
  • 必填项:否
  • 灵感来源:PDMPoetryuv
  • 指定锁定文件支持的任何环境兼容的最低 Python 版本的Requires-Python(即锁定文件的最低可用 Python 版本)。

额外项

  • 类型:字符串数组
  • 必填项:否;默认为 []
  • 灵感来源:Provides-Extra(多用途)
  • 此锁定文件支持的附加项列表。
  • 锁定器**可以**选择不支持写入支持附加项和依赖组的锁定文件(即,工具可能只支持导出单用锁定文件)。
  • 支持附加项的工具**必须**也支持依赖组。
  • 工具应该明确将此键设置为空数组,以表示用于生成锁定文件的输入没有附加项(例如,pyproject.toml 文件没有 [project.optional-dependencies] 表),表明该锁定文件实际上是多用途的,即使它看起来只是单用途。

依赖组

  • 类型:字符串数组
  • 必填项:否;默认为 []
  • 灵感来源:任意工具配置:[tool] 表
  • 此锁定文件公开支持的依赖组列表(即,用户可以通过工具的用户界面指定预期的依赖组)。
  • 锁定器**可以**选择不支持写入支持附加项和依赖组的锁定文件(即,工具可能只支持导出单用锁定文件)。
  • 支持依赖组的工具**必须**也支持附加项。
  • 工具**应该**明确将此键设置为空数组,以表示用于生成锁定文件的输入没有依赖组(例如,pyproject.toml 文件没有 [dependency-groups] 表),表明该锁定文件实际上是多用途的,即使它看起来只是单用途。

默认组

  • 类型:字符串数组
  • 必填项:否;默认为 []
  • 灵感来源:Poetry, PDM
  • 合成依赖组的名称,用于表示默认情况下应安装的内容(例如,project.dependencies 隐式表示的内容)。
  • 旨在用于 packages.marker 需要存在此类组的情况。
  • 此键列出的组**不应**列在 dependency-groups 中,因为这些组不应通过名称直接向用户公开,而是通过安装程序的 UI。

创建者

  • 类型:字符串
  • 必填项:是
  • 灵感来源:锁定文件中包含工具名称的工具
  • 记录用于创建锁定文件的工具名称。
  • 工具**可以**使用 [tool] 表记录足够详细的信息,以便可以推断出用于创建锁定文件的输入。
  • 如果工具可作为 Python 包使用,则工具**应该**记录工具的规范化名称,以方便查找工具。

[[包]]

  • 类型:表数组
  • 必填项:是
  • 灵感来源:PDMPoetryuv
  • 一个包含**可能**安装的所有包的数组。
  • 包**可以**以不同的数据多次列出,但所有要安装的包**必须**在安装时缩小到单个条目。
包.名称
包.版本
  • 类型:字符串
  • 必填项:否
  • 灵感来源:版本
  • 包的版本。
  • 当版本已知稳定时(即指定sdist轮子时),**应该**指定版本。
  • 当无法保证版本与使用的代码一致时(即使用源树时),**不得**包含版本。
包.标记
  • 类型:字符串
  • 必填项:否
  • 灵感来源:PDM
  • 指定何时安装包的环境标记
包.要求-python
[[包.依赖项]]
  • 类型:表数组
  • 必填项:否
  • 灵感来源:PDMPoetryuv
  • 记录 [[packages]] 中的其他条目,这些条目是此包的直接依赖项。
  • 每个条目都是一个表,其中包含识别其对应哪个其他包条目所需的最小信息,通过逐键比较可以明确找到适当的包(例如,如果 spam 包有两个条目,则可以包含版本号,如 {name = "spam", version = "1.0.0"},或通过来源,如 {name = "spam", vcs = { url = "..."})。
  • 工具**不得**在安装时使用此信息;它纯粹用于审计目的。
[包.vcs]
  • 类型:表
  • 必填项:否;与 packages.directorypackages.archivepackages.sdistpackages.wheels 互斥
  • 灵感来源:直接 URL 数据结构
  • 记录它包含的源树的版本控制系统详细信息。
  • 工具**可以**选择不支持版本控制系统,无论是从锁定还是安装的角度。
  • 工具**可以**选择只支持一部分可用的 VCS 类型。
  • 工具**应该**提供一种方式让用户选择是否使用版本控制系统。
  • 从版本控制系统安装被视为源自直接 URL 引用
包.vcs.类型
  • 类型:字符串;支持的值在注册的 VCS中指定
  • 必填项:是
  • 灵感来源:VCS URL
  • 使用的版本控制系统类型。
包.vcs.url
  • 类型:字符串
  • 必填项:如果未指定 path
  • 灵感来源:VCS URL
  • 源树的 URL。
包.vcs.路径
  • 类型:字符串
  • 必填项:如果未指定 url
  • 灵感来源:VCS URL
  • 源树的本地目录路径。
  • 如果使用相对路径,则**必须**相对于此文件的位置。
  • 如果路径是相对路径,则**可以**明确使用 POSIX 风格的路径分隔符以实现可移植性。
包.vcs.请求-修订
  • 类型:字符串
  • 必填项:否
  • 灵感来源:VCS URL
  • 用户请求的分支/标签/引用/提交/修订等。
  • 这纯粹是信息性的,用于方便编写直接 URL 数据结构;**不得**用于检出存储库。
包.vcs.提交-ID
  • 类型:字符串
  • 必填项:是
  • 灵感来源:VCS URL
  • 要安装的精确提交/修订号。
  • 如果 VCS 支持基于提交哈希的修订标识符,则**必须**使用此类提交哈希作为提交 ID,以引用源代码的不可变版本。
包.vcs.子目录
  • 类型:字符串
  • 必填项:否
  • 灵感来源:子目录中的项目
  • 中项目根目录所在的子目录(例如,pyproject.toml 文件的位置)。
  • 路径**必须**相对于源树结构的根目录。
[包.目录]
  • 类型:表
  • 必填项:否;与 packages.vcspackages.archivepackages.sdistpackages.wheels 互斥
  • 灵感来源:本地目录
  • 记录它包含的源树的本地目录详细信息。
  • 工具**可以**选择不支持本地目录,无论是从锁定还是安装的角度。
  • 工具**应该**提供一种方式让用户选择是否使用本地目录。
  • 从目录安装被视为源自直接 URL 引用
包.目录.路径
  • 类型:字符串
  • 必填项:是
  • 灵感来源:本地目录
  • 源树所在的本地目录。
  • 如果路径是相对路径,则**必须**相对于锁定文件的位置。
  • 如果路径是相对路径,则**可以**使用 POSIX 风格的路径分隔符以实现可移植性。
包.目录.可编辑
  • 类型:布尔值
  • 必填项:否;默认为 false
  • 灵感来源:本地目录
  • 一个标志,表示在锁定时间,源树是否是可编辑安装。
  • 如果用户操作或上下文使得可编辑安装不必要或不理想(例如,一个不会挂载用于开发目的,而是部署到生产环境并被视为只读的容器镜像),则安装程序**可以**选择忽略此标志。
包.目录.子目录

参见 packages.vcs.subdirectory

[包.归档]
  • 类型:表
  • 必填项:否
  • 灵感来源:归档 URL
  • 直接引用要安装的归档文件(这可以包括 wheels 和 sdists,以及包含源树的其他归档格式)。
  • 工具**可以**选择不支持归档文件,无论是从锁定还是安装的角度。
  • 工具**应该**提供一种方式让用户选择是否使用归档文件。
  • 从归档文件安装被视为源自直接 URL 引用
包.归档.url

参见 packages.vcs.url

包.归档.路径

参见 packages.vcs.path

包.归档.大小
  • 类型:整数
  • 必填项:否
  • 灵感来源:uv简单仓库 API
  • 归档文件的大小。
  • 工具**应该**在合理可能的情况下提供文件大小(例如,文件大小可以通过 Content-Length 头部从 HEAD HTTP 请求获取)。
包.归档.上传时间
  • 类型:日期时间
  • 必填项:否
  • 灵感来源:简单仓库 API
  • 文件上传时间。
  • 日期和时间**必须**以 UTC 记录。
[包.归档.哈希]
  • 类型:字符串表
  • 必填项:是
  • 灵感来源:PDMPoetryuv简单仓库 API
  • 一个表,列出文件的已知哈希值,其中键是哈希算法,值是哈希值。
  • 该表**必须**包含至少一个条目。
  • 哈希算法键**应该**小写。
  • **应该**始终包含来自 hashlib.algorithms_guaranteed 的至少一个安全算法(在撰写本文时,特别推荐 sha256)。
包.归档.子目录

参见 packages.vcs.subdirectory

包.索引
  • 类型:字符串
  • 必填项:否
  • 灵感来源:uv
  • Sdist 和/或 wheels 所在的简单仓库 API 的包索引的基 URL(例如 https://pypi.ac.cn/simple/)。
  • 在可能的情况下,**应该**指定此项,以帮助生成软件物料清单(即 SBOM),并帮助在 URL 失效时找到文件。
  • 如果特定文件的记录 URL 不再有效(例如,返回 404 HTTP 错误代码),则工具**可以**支持从索引安装。
[包.sdist]
  • 类型:表
  • 必填项:否;与 packages.vcspackages.directorypackages.archive 互斥
  • 灵感来源:uv
  • 包的源分发文件名的详细信息。
  • 工具**可以**选择不支持 sdist 文件,无论是从锁定还是安装的角度。
  • 工具**应该**提供一种方式让用户选择是否使用 sdist 文件。
包.sdist.名称
  • 类型:字符串
  • 必填项:否,当 path/url 的最后一个组件值相同时则不是必需的
  • 灵感来源:PDMPoetryuv
  • 源分发文件名文件的文件名。
包.sdist.上传时间

参见 packages.archive.upload-time

包.sdist.url

参见 packages.archive.url

包.sdist.路径

参见 packages.archive.path

包.sdist.大小

参见 packages.archive.size

包.sdist.哈希

参见 packages.archive.hashes

[[包.轮子]]
  • 类型:表数组
  • 必填项:否;与 packages.vcspackages.directorypackages.archive 互斥
  • 灵感来源:PDMPoetryuv
  • 用于记录包的二进制分发格式中指定的 wheel 文件。
  • 工具**必须**支持 wheel 文件,无论是从锁定还是安装的角度。
包.轮子.名称
  • 类型:字符串
  • 必填项:否,当 path/url 的最后一个组件值相同时则不是必需的
  • 灵感来源:PDMPoetryuv
  • 二进制分发格式文件的文件名。
包.轮子.上传时间

参见 packages.archive.upload-time

包.轮子.url

参见 packages.archive.url

包.轮子.路径

参见 packages.archive.path

包.轮子.大小

参见 packages.archive.size

包.轮子.哈希

参见 packages.archive.hashes

[[包.证明身份]]
  • 类型:表数组
  • 必填项:否
  • 灵感来源:来源对象
  • 记录此包的**任何**文件的证明。
  • 如果可用,工具**应该**包含找到的证明身份。
  • 发布者特定的键将按原样(即顶层)包含在表中,遵循索引托管证明的规范。
包.证明身份.类型
  • 类型:字符串
  • 必填项:是
  • 灵感来源:来源对象
  • 受信任发布者的唯一身份。
[包.工具]
  • 类型:表
  • 必填项:否
  • 灵感来源:任意工具配置:[tool] 表
  • pyproject.toml 规范中的 [tool] 表用法类似,但级别是包版本级别,而非锁定文件级别(后者也可通过 [tool] 获取)。
  • 表中记录的数据**必须**是可丢弃的(即,**不得**影响安装)。

[工具]

标记表达式语法的补充

本 PEP 提议对环境标记规范进行补充,以便可以在 packages.marker 中表达 [[packages]] 中条目的 extras 和依赖组关系。本 PEP 中概述的补充**仅**适用于本 PEP 定义的锁定文件上下文,不适用于使用标记语法的其他上下文(例如 METADATApyproject.toml)。

首先,将引入两个新的标记:extrasdependency_groups。它们分别表示已请求安装的 extras 和依赖组。

diff --git a/source/specifications/dependency-specifiers.rst b/source/specifications/dependency-specifiers.rst
index 06897da2..c9ab247f 100644
--- a/source/specifications/dependency-specifiers.rst
+++ b/source/specifications/dependency-specifiers.rst
@@ -87,7 +87,7 @@ environments::
                      'platform_system' | 'platform_version' |
                      'platform_machine' | 'platform_python_implementation' |
                      'implementation_name' | 'implementation_version' |
-                     'extra' # ONLY when defined by a containing layer
+                     'extra' | 'extras' | 'dependency_groups' # ONLY when defined by a containing layer
                      )
    marker_var    = wsp* (env_var | python_str)
    marker_expr   = marker_var marker_op marker_var

这并不排除在其他上下文中使用相同的语法解析器,只取决于新的标记在特定上下文中是否被认为是有效的。

其次,标记规范将更改为允许使用集合作为值(在当前对字符串和版本的支持之上)。**只有**本 PEP 中引入的新标记才允许将其值设置为集合(默认为空集合)。这**明确不**更新规范以允许集合字面量。

第三,标记表达式语法规范将更新以允许涉及集合的操作。

diff --git a/source/specifications/dependency-specifiers.rst b/source/specifications/dependency-specifiers.rst
index 06897da2..ac29d796 100644
--- a/source/specifications/dependency-specifiers.rst
+++ b/source/specifications/dependency-specifiers.rst
@@ -196,15 +196,16 @@ safely evaluate it without running arbitrary code that could become a security
vulnerability. Markers were first standardised in :pep:`345`. This document
fixes some issues that were observed in the design described in :pep:`426`.

-Comparisons in marker expressions are typed by the comparison operator.  The
-<marker_op> operators that are not in <version_cmp> perform the same as they
-do for strings in Python. The <version_cmp> operators use the version comparison
-rules of the :ref:`Version specifier specification <version-specifiers>`
-when those are defined (that is when both sides have a valid
-version specifier). If there is no defined behaviour of this specification
-and the operator exists in Python, then the operator falls back to
-the Python behaviour. Otherwise an error should be raised. e.g. the following
-will result in  errors::
+Comparisons in marker expressions are typed by the comparison operator and the
+type of the marker value. The <marker_op> operators that are not in
+<version_cmp> perform the same as they do for strings or sets in Python based on
+whether the marker value is a string or set itself. The <version_cmp> operators
+use the version comparison rules of the
+:ref:`Version specifier specification <version-specifiers>` when those are
+defined (that is when both sides have a valid version specifier). If there is no
+defined behaviour of this specification and the operator exists in Python, then
+the operator falls back to the Python behaviour for the types involved.
+Otherwise an error should be raised. e.g. the following will result in errors::

    "dog" ~= "fred"
    python_version ~= "surprise"

第四,在锁定文件之外使用 extrasdependency_groups 将被视为错误(就像 核心元数据规范之外的 extra 一样)。

diff --git a/source/specifications/dependency-specifiers.rst b/source/specifications/dependency-specifiers.rst
index 06897da2..2914ef66 100644
--- a/source/specifications/dependency-specifiers.rst
+++ b/source/specifications/dependency-specifiers.rst
@@ -235,6 +235,11 @@ no current specification for this. Regardless, outside of a context where this
special handling is taking place, the "extra" variable should result in an
error like all other unknown variables.

+The "extras" and "dependency_groups" variables are also special. They are used
+to specify any requested extras or dependency groups when installing from a lock
+file. Outside of the context of lock files, these two variables should result in
+an error like all other unknown variables.
+
.. list-table::
    :header-rows: 1

这些更改,连同 packages.extras/packages.dependency-groups 和标记表达式的布尔逻辑支持,允许根据(不)请求的附加项和依赖组表达任意、详尽的包安装要求。例如,如果锁定文件包含 extras = ["extra-1", "extra-2"],您可以指定何时应安装包:

  • 指定了任意附加项('extra-1' in extras or 'extra-2' in extras
  • 仅指定了“extra-1”('extra-1' in extras and 'extra-2' not in extras
  • 未指定任何附加项('extra-1' not in extras and 'extra-2' not in extras

(此列表并非涵盖所有可能的布尔逻辑表达式。)

同样的灵活性也适用于依赖组。

用户如何告诉工具他们想要安装哪些附加项和/或依赖组,由工具决定。安装程序**必须**支持本 PEP 提出的标记表达式语法补充。锁定器**可以**支持编写使用提出的标记表达式语法补充的锁定文件(即,锁定器可以选择只支持编写单用锁定文件)。

示例

lock-version = '1.0'
environments = ["sys_platform == 'win32'", "sys_platform == 'linux'"]
requires-python = '==3.12'
created-by = 'mousebender'

[[packages]]
name = 'attrs'
version = '25.1.0'
requires-python = '>=3.8'
wheels = [
  {name = 'attrs-25.1.0-py3-none-any.whl', upload-time = 2025-01-25T11:30:10.164985+00:00, url = 'https://files.pythonhosted.org/packages/fc/30/d4986a882011f9df997a55e6becd864812ccfcd821d64aac8570ee39f719/attrs-25.1.0-py3-none-any.whl', size = 63152, hashes = {sha256 = 'c75a69e28a550a7e93789579c22aa26b0f5b83b75dc4e08fe092980051e1090a'}},
]
[[packages.attestation-identities]]
environment = 'release-pypi'
kind = 'GitHub'
repository = 'python-attrs/attrs'
workflow = 'pypi-package.yml'

[[packages]]
name = 'cattrs'
version = '24.1.2'
requires-python = '>=3.8'
dependencies = [
    {name = 'attrs'},
]
wheels = [
  {name = 'cattrs-24.1.2-py3-none-any.whl', upload-time = 2024-09-22T14:58:34.812643+00:00, url = 'https://files.pythonhosted.org/packages/c8/d5/867e75361fc45f6de75fe277dd085627a9db5ebb511a87f27dc1396b5351/cattrs-24.1.2-py3-none-any.whl', size = 66446, hashes = {sha256 = '67c7495b760168d931a10233f979b28dc04daf853b30752246f4f8471c6d68d0'}},
]

[[packages]]
name = 'numpy'
version = '2.2.3'
requires-python = '>=3.10'
wheels = [
  {name = 'numpy-2.2.3-cp312-cp312-win_amd64.whl', upload-time = 2025-02-13T16:51:21.821880+00:00, url = 'https://files.pythonhosted.org/packages/42/6e/55580a538116d16ae7c9aa17d4edd56e83f42126cb1dfe7a684da7925d2c/numpy-2.2.3-cp312-cp312-win_amd64.whl', size = 12626357, hashes = {sha256 = '83807d445817326b4bcdaaaf8e8e9f1753da04341eceec705c001ff342002e5d'}},
  {name = 'numpy-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl', upload-time = 2025-02-13T16:50:00.079662+00:00, url = 'https://files.pythonhosted.org/packages/39/04/78d2e7402fb479d893953fb78fa7045f7deb635ec095b6b4f0260223091a/numpy-2.2.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl', size = 16116679, hashes = {sha256 = '3b787adbf04b0db1967798dba8da1af07e387908ed1553a0d6e74c084d1ceafe'}},
]

[tool.mousebender]
command = ['.', 'lock', '--platform', 'cpython3.12-windows-x64', '--platform', 'cpython3.12-manylinux2014-x64', 'cattrs', 'numpy']
run-on = 2025-03-06T12:28:57.760769

安装

以下概述了从锁定文件安装的步骤(虽然要求是规定性的,但总体步骤和顺序是建议)

  1. 收集要安装的附加项和依赖组,并分别设置 extrasdependency_groups 以进行标记评估。
    1. extras 默认**应该**设置为空集。
    2. dependency_groups 默认**应该**设置为从 default-groups 创建的集合。
  2. 检查 lock-version 指定的元数据版本是否受支持;**必须**根据需要引发错误或警告。
  3. 如果指定了 requires-python,则检查所安装的环境是否满足要求;如果不满足,则**必须**引发错误。
  4. 如果指定了 environments,则检查至少一个环境标记表达式是否满足;如果不满足,则**必须**引发错误。
  5. 对于 [[packages]] 中列出的每个包
    1. 如果指定了 marker,请检查它是否满足;如果不满足,则跳到下一个包。
    2. 如果指定了 requires-python,则检查它是否满足;如果不满足,则**必须**引发错误。
    3. 检查是否没有其他冲突的包实例被计划安装;否则**必须**引发关于歧义的错误。
    4. 检查包的来源是否正确指定(即包条目中没有冲突的来源);如果发现任何问题,则**必须**引发错误。
    5. 将包添加到要安装的包集合中。
  6. 对于每个要安装的包
    • 如果设置了 vcs
      1. 将存储库克隆到 commit-id 中指定的提交 ID。
      2. 构建包,遵循 subdirectory
      3. 安装。
    • 否则,如果设置了 directory
      1. 构建包,遵循 subdirectory
      2. 安装。
    • 否则,如果设置了 archive
      1. 获取文件。
      2. 验证文件大小和哈希。
      3. 构建包,遵循 subdirectory
      4. 安装。
    • 否则,如果存在 wheels 的条目
      1. 根据 name 查找相应的 wheel 文件;如果未找到,则继续 sdist,否则**必须**引发关于项目缺少来源的错误。
      2. 获取文件
        • 如果设置了 path,则使用它。
        • 如果设置了 url,则尝试使用它;工具**可以**选择使用 packages.index 或一些工具特定的机制来下载选定的 wheel 文件(工具**不得**尝试根据可用内容更改要下载的 wheel 文件;要安装的文件应以离线方式确定以实现可重复性)。
      3. 验证文件大小和哈希。
      4. 安装。
    • 否则,如果没有找到 wheel 文件或仅设置了 sdist
      1. 获取文件。
        • 如果设置了 path,则使用它。
        • 如果设置了 url,则尝试使用它;工具**可以**选择使用 packages.index 或一些工具特定的机制来下载文件。
      2. 验证文件大小和哈希。
      3. 构建包。
      4. 安装。

requirements.txt 文件的语义差异

忽略格式,本 PEP 提出的锁定文件与通过 requirements file 可能实现的锁定文件存在一些差异。

一些差异在于安全性。要求哈希、记录文件大小以及文件发现位置(索引和文件本身的位置)有助于审计和验证锁定文件。相比之下,requirements 文件可以选择性地包含哈希,但它是一个可选功能,可以被绕过。文件上传时间和文件位置的可选包含也不同。

明确指定文件整体支持的 Python 版本和环境也是本 PEP 独有的。这是为了缓解不知道 requirements 文件何时针对特定平台的问题。

[tool] 表在 requirements 文件中没有直接的对应关系。它们确实支持注释,但由于是 TOML 格式,它们不像 [tool] 表那样具有固有的结构。

虽然 requirements 文件中的注释可以记录有助于审计和理解锁定文件内容的详细信息,但提供结构化支持来记录这些内容使得审计更加容易。预先记录包所需的 Python 版本有助于此,如果安装将失败,则更快地报错。将 wheel 文件名与 URL 或路径分开记录也有助于更轻松地读取 wheel 文件列表,因为它编码了在理解和审计文件时有用的信息。记录 sdist 文件名也是出于同样的原因。

本 PEP 支持多用途锁定文件,而 requirements 文件是单用途的。

本 PEP 并未完全取代 requirements 文件,因为

向后兼容性

由于没有预先存在的锁定文件格式,因此在 Python 打包标准方面没有明确的向后兼容性问题。

至于打包工具本身,将由每个工具自行决定是否支持本 PEP 以及以何种方式支持(即,作为导出目标或作为它们记录锁定文件的主要方式)。

安全隐患

希望通过标准化一种以安全为首的锁定文件格式,将有助于使整体打包安装更安全。然而,本 PEP 并不能解决所有潜在的安全问题。

一个潜在的担忧是篡改锁定文件。如果锁定文件未妥善保存在源代码管理中并进行适当审计,恶意行为者可能会以邪恶的方式更改文件(例如,指向恶意软件版本的包)。篡改也可能发生在传输过程中,例如传输到将代表用户执行安装的云提供商。这两种情况都可以通过在文件内的 [tool] 条目中或通过锁定文件本身外部的侧信道对锁定文件进行签名来缓解。

本 PEP 不做任何事情来阻止用户安装不正确的包。虽然包含许多细节以帮助审计包的包含,但没有任何机制可以阻止例如通过错别字攻击导致的名称混淆攻击。工具可能能够提供一些用户体验来帮助解决这个问题(例如,通过提供包的下载计数)。

如何教授此内容

用户应该被告知,当他们要求安装某个包时,该包可能有自己的依赖项,这些依赖项又可能有依赖项,依此类推。如果不记录安装该包时所安装的所有内容,那么情况可能会在他们不知情的情况下发生变化(例如包版本)。底层依赖项的更改可能导致他们的代码意外损坏。锁定文件通过提供一种记录已安装内容的方法来解决这个问题,以便您将来可以安装完全相同的内容。

将要安装的内容记录下来也有助于与他人协作。通过就锁定文件的内容达成一致,每个人最终都会安装相同的软件包。这有助于确保没有人依赖于例如只有特定版本才可用的 API,而项目中的并非所有人都安装了该版本。

锁定文件还通过确保您始终安装相同的文件而不是某人可能植入的恶意文件来帮助提高安全性。它还允许人们更刻意地升级依赖项,从而确保更改是故意的,而不是恶意行为者偷偷进行的。

锁定文件可以只支持某些**环境**。他们正在安装的环境所需安装的内容可能与另一个不同环境所需安装的内容不同。不过,一些锁定文件确实尝试做到**通用**,并适用于任何可能的环境(如果 sdist 和源树安装成功)。

锁定文件可以是**单用**或**多用**的。单用锁定文件用于单一用例。多用锁定文件可根据附加项和依赖组用于多个用例。您使用的工具决定是否可以使用多用锁定文件。所有处理锁定文件的工具至少支持单用锁定文件。这两种类型的锁定文件都没有优劣之分,只是改变了单个文件中可以记录多少内容。

遵循本 PEP 的锁定文件可以由任何实现该规范的安装程序安装。这使得锁定文件的用户可以执行安装,而不受生成锁定文件的人所使用的锁定器的限制。但这并非意味着使用不同的锁定器会产生相同的结果。这可能是由于各种原因,包括使用不同的算法来确定要锁定的内容。

参考实现

一个实现单用途锁定文件的概念验证可以在 https://github.com/brettcannon/mousebender/tree/pep 找到。其他工具,如 PDMPoetry,实现了本 PEP 各部分语义上相似的方法。

被拒绝的想法

记录依赖图以供安装

本 PEP 的早期版本记录了包的依赖关系图,而不是一组要安装的包。其思想是,通过记录依赖关系图,不仅可以获得更多信息,而且通过原生支持更多功能(例如,无需显式传播标记的平台特定依赖项)提供了更大的灵活性。

然而,最终认为这增加了不必要的复杂性(例如,它影响了审计细节的便利性,而这些细节对于本 PEP 实现其目标并非必要)。

指定需要文件间元数据一致的新核心元数据版本

曾一度,为了解决元数据在文件之间差异的问题,从而需要检查包和版本的每个发布文件以获得准确的锁定结果,有人提出了引入一个新的核心元数据版本的想法,该版本将要求同一包的单个版本的所有 wheel 文件的所有元数据都相同。然而,最终认为这没有必要,因为本 PEP 将迫使人们为了性能原因使文件保持一致,或者让索引提供独立于 wheel 文件本身的全部元数据。此外,没有简单的强制机制,因此社区期望将与新的元数据版本一样有效。

让安装程序进行依赖解析

为了支持更类似于本 PEP 起草时 Poetry 工作方式的格式,建议锁定器有效记录可能需要使安装在任何可能场景下工作的包及其版本,然后安装程序解析要安装的内容。但这通过需要更多的思维努力来了解在任何给定场景下可能安装哪些包来复杂化了对锁定文件的审计。此外,Poetry 的一位开发者建议本 PEP 的包锁定方法中表示的标记可能足以满足 Poetry 的需求。不让安装程序执行解析也简化了它们的实现,将复杂性集中在锁定器中。

要求最低哈希算法支持

曾有人提议要求文件使用基线哈希算法。这一提议被拒绝了,因为没有其他 Python 打包规范要求特定哈希算法的支持。此外,建议的最低哈希算法最终可能过时或不安全,需要进一步更新。为了促进始终使用最佳算法,未提供基线,以避免工具在不考虑该哈希算法的安全影响的情况下简单地默认使用基线。

文件命名

使用 *.pylock.toml 作为文件名

曾有人提议将文件名中 pylock 的常量部分放在锁定文件目的标识符之后。最终决定不这样做,以便在查看目录内容时,锁定文件将按文件名排序,而不是纯粹根据其目的,这可能会导致它们在目录中分散开来。

使用 *.pylock 作为文件名

有人提出不使用 .toml 作为文件扩展名,而是直接使用 .pylock。这个提议被否决了,目的是让代码编辑器能够在不了解该文件扩展名的情况下为锁定文件提供语法高亮。

文件没有命名约定

曾考虑过对锁定文件的名称不作任何要求或指导,但最终被否决。通过采用标准化的命名约定,可以方便人类和代码编辑器识别锁定文件。这有助于在例如工具想要知道所有可用的锁定文件时进行发现。

文件格式

使用 JSON 而非 TOML

由于本 PEP 的目标之一是可机器写入的格式,因此建议使用 JSON。但它被认为不如 TOML 易于人工阅读,同时在机器写入方面也没有足够的改进来证明这种改变是合理的。

使用 YAML 而非 TOML

有人认为 YAML 比 TOML 更好地满足了机器可写/人类可读的要求。但由于这是主观的,并且 pyproject.toml 已经作为 Python 打包标准中使用的人类可写文件存在,因此保持使用 TOML 被认为更重要。

其他键

整个文件使用单一哈希算法

本 PEP 的早期版本曾提议每个文件指定一个哈希算法,而不是任意数量的算法。其想法是,通过指定单个算法,当强制使用特定哈希算法时,有助于审计文件。

最终,这一想法遭到了一些反对。通常,它围绕着重新哈希大型 wheel 文件(例如 PyTorch)的成本展开。还有人担心在安装程序不知情的情况下预先做出哈希决策,这可能会导致安装程序不同意。最终认为最好具有灵活性,并让人们根据自己的意愿审计锁定文件。

对锁定文件内容本身进行哈希

曾有人提出对文件的字节内容进行哈希,并将哈希值存储在文件本身中。这样做是为了更容易合并对锁定文件的更改,因为每次合并都必须重新计算哈希值以避免合并冲突。

曾有人提出对文件的语义内容进行哈希,但这也会导致相同的合并冲突问题。

无论哈希了哪些内容,如果需要这样的哈希,两种方法都可以将哈希值存储在文件外部。

记录锁定文件的创建日期

为了了解锁定文件可能过时了多久,早期提案建议记录锁定文件的创建日期。但出于与存储文件内容哈希相同的合并冲突原因,此想法被放弃。

记录搜索中使用的包索引

曾考虑记录用于创建锁定文件的包索引。然而,最终被拒绝,因为它被认为是不必要的簿记。

锁定 sdist 的构建要求

本 PEP 的早期版本试图在 packages.build-requires 键下锁定 sdist 的构建要求。不幸的是,它让足够多的人对它的预期操作方式感到困惑,并且存在足够的边缘情况问题,因此决定不值得在本 PEP 中预先尝试这样做。相反,未来的 PEP 可以提出一个解决方案。

一个专用的 direct

早期版本有一个专门的 packages.direct 键来标记何时应将某个内容视为源自直接 URL 引用。但明确只有三种情况可能出现直接 URL 引用(VCS、目录和归档)。由于这三种情况都在 [[packages]] 中明确列出,因此设置该键在技术上是多余的。

没有此键的唯一缺点是,现在 wheel 和 sdist 文件都归属于 packages.archive。通过单独的键(或将其指定为 packages.sdistpackages.wheels 的一部分),它将允许在锁定文件本身中识别归档文件是 sdist 还是 wheel。就目前而言,安装程序必须自行推断该细节。

简化

放弃记录包版本

包版本是可选的,因为它只能在 sdist 或 wheel 文件使用时可靠地记录。而且由于这两种来源都在文件名中记录了版本,所以从技术上讲它是多余的。

但在讨论中,大家认为版本号对审计很有用,因此仍单独列出。

放弃指定 sdist 和/或 wheel 位置的要求

至少有一个人评论说,他们的工作中的所有 sdist 和 wheel 的 URL 都不稳定。因此,无论文件之前在哪里找到,他们都必须在安装时搜索所有文件。取消提供文件 URL 或路径的要求将有助于解决记录已知错误信息的问题。

允许工具以 URL 之外的其他方式查找文件的决定消除了 URL 成为可选的需求。

放弃要求文件大小和哈希

至少有一个人说,他们的工作会用内部文件修改所有 wheel 和 sdist。这意味着任何记录的哈希值和文件大小都将是错误的。通过将文件大小和哈希值设为可选——很可能通过某种选择退出机制——他们就可以继续生成符合本 PEP 要求的锁定文件。

此决定将削弱安全性。它还阻止了从替代位置安装文件。

放弃记录 sdist 文件名

虽然与放弃 URL/路径要求、包版本和哈希不兼容,但记录 sdist 文件名在技术上完全没有必要(目前记录文件名是可选的)。文件名只编码了项目名称和版本,因此没有传达关于文件的新信息(当提供了包版本时)。如果记录了位置,那么无论文件名如何,都可以处理获取文件。

但当记录的文件位置不再可用时(尽管 sdist 文件名现在已标准化,这要归功于 PEP 625,但这仅从 2020 年开始,因此有许多旧的 sdist 的名称可能无法猜测),记录文件名在寻找合适文件时可能会有所帮助。

为了简化起见,决定要求提供 sdist 文件名。

使 packages.wheels 成为一个表

有人可能会将 wheel 文件详细信息写成以文件名作为键的表格。例如:

[[packages]]
name = "attrs"
version = "23.2.0"
requires-python = ">=3.7"
index = "https://pypi.ac.cn/simple/"

[packages.wheels]
"attrs-23.2.0-py3-none-any.whl" = {upload-time = 2023-12-31T06:30:30.772444Z, url = "https://files.pythonhosted.org/packages/e0/44/827b2a91a5816512fcaf3cc4ebc465ccd5d598c45cefa6703fcf4a79018f/attrs-23.2.0-py3-none-any.whl", size = 60752, hashes = {sha256 = "99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}

[[packages]]
name = "numpy"
version = "2.0.1"
requires-python = ">=3.9"
index = "https://pypi.ac.cn/simple/"

[packages.wheels]
"numpy-2.0.1-cp312-cp312-macosx_10_9_x86_64.whl" = {upload-time = 2024-07-21T13:37:15.810939Z, url = "https://files.pythonhosted.org/packages/64/1c/401489a7e92c30db413362756c313b9353fb47565015986c55582593e2ae/numpy-2.0.1-cp312-cp312-macosx_10_9_x86_64.whl", size = 20965374, hashes = {sha256 = "6bf4e6f4a2a2e26655717a1983ef6324f2664d7011f6ef7482e8c0b3d51e82ac"}
"numpy-2.0.1-cp312-cp312-macosx_11_0_arm64.whl" = {upload-time = 2024-07-21T13:37:36.460324Z, url = "https://files.pythonhosted.org/packages/08/61/460fb524bb2d1a8bd4bbcb33d9b0971f9837fdedcfda8478d4c8f5cfd7ee/numpy-2.0.1-cp312-cp312-macosx_11_0_arm64.whl", size = 13102536, hashes = {sha256 = "7d6fddc5fe258d3328cd8e3d7d3e02234c5d70e01ebe377a6ab92adb14039cb4"}
"numpy-2.0.1-cp312-cp312-macosx_14_0_arm64.whl" = {upload-time = 2024-07-21T13:37:46.601144Z, url = "https://files.pythonhosted.org/packages/c2/da/3d8debb409bc97045b559f408d2b8cefa6a077a73df14dbf4d8780d976b1/numpy-2.0.1-cp312-cp312-macosx_14_0_arm64.whl", size = 5037809, hashes = {sha256 = "5daab361be6ddeb299a918a7c0864fa8618af66019138263247af405018b04e1"}
"numpy-2.0.1-cp312-cp312-macosx_14_0_x86_64.whl" = {upload-time = 2024-07-21T13:37:58.784393Z, url = "https://files.pythonhosted.org/packages/6d/59/85160bf5f4af6264a7c5149ab07be9c8db2b0eb064794f8a7bf6d/numpy-2.0.1-cp312-cp312-macosx_14_0_x86_64.whl", size = 6631813, hashes = {sha256 = "ea2326a4dca88e4a274ba3a4405eb6c6467d3ffbd8c7d38632502eaae3820587"}
"numpy-2.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl" = {upload-time = 2024-07-21T13:38:19.714559Z, url = "https://files.pythonhosted.org/packages/5e/e3/944b77e2742fece7da8dfba6f7ef7dccdd163d1a613f7027f4d5b/numpy-2.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", size = 13623742, hashes = {sha256 = "529af13c5f4b7a932fb0e1911d3a75da204eff023ee5e0e79c1751564221a5c8"}
"numpy-2.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl" = {upload-time = 2024-07-21T13:38:48.972569Z, url = "https://files.pythonhosted.org/packages/2c/f3/61eee37decb58e7cb29940f19a1464b8608f2cab8a8616aba75fd/numpy-2.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", size = 19242336, hashes = {sha256 = "6790654cb13eab303d8402354fabd47472b24635700f631f041bd0b65e37298a"}
"numpy-2.0.1-cp312-cp312-musllinux_1_1_x86_64.whl" = {upload-time = 2024-07-21T13:39:19.213811Z, url = "https://files.pythonhosted.org/packages/77/b5/c74cc436114c1de5912cdb475145245f6e645a6a1a29b5d08c774/numpy-2.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", size = 19637264, hashes = {sha256 = "cbab9fc9c391700e3e1287666dfd82d8666d10e69a6c4a09ab97574c0b7ee0a7"}
"numpy-2.0.1-cp312-cp312-musllinux_1_2_aarch64.whl" = {upload-time = 2024-07-21T13:39:41.812321Z, url = "https://files.pythonhosted.org/packages/da/89/c8856e12e0b3f6af371ccb90d604600923b08050c58f0cd26eac9/numpy-2.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", size = 14108911, hashes = {sha256 = "99d0d92a5e3613c33a5f01db206a33f8fdf3d71f2912b0de1739894668b7a93b"}
"numpy-2.0.1-cp312-cp312-win32.whl" = {upload-time = 2024-07-21T13:39:52.932102Z, url = "https://files.pythonhosted.org/packages/15/96/310c6f6d146518479b0a6ee6eb92a537954ec3b1acfa2894d1347/numpy-2.0.1-cp312-cp312-win32.whl", size = 6171379, hashes = {sha256 = "173a00b9995f73b79eb0191129f2455f1e34c203f559dd118636858cc452a1bf"}
"numpy-2.0.1-cp312-cp312-win_amd64.whl" = {upload-time = 2024-07-21T13:40:17.532627Z, url = "https://files.pythonhosted.org/packages/b5/59/f6ad378ad85ed9c2785f271b39c3e5b6412c66e810d2c60934c9f/numpy-2.0.1-cp312-cp312-win_amd64.whl", size = 16255757, hashes = {sha256 = "bb2124fdc6e62baae159ebcfa368708867eb56806804d005860b6007388df171"}

然而,总的来说,人们并不喜欢这种方法,而是更喜欢本 PEP 所采取的方法。

自引用

删除 [tool]

[tool] 表之所以包含在内,是因为它在 pyproject.toml 文件中被发现非常有用。为本 PEP 提供类似的灵活性,希望能够带来类似的益处。

但有些人担心这样的表对工具来说太有吸引力,会导致文件工具专用且其他工具无法使用。这可能会给尝试安装、审计等工具带来问题,因为它们不知道 [tool] 表中的哪些细节是至关重要的。

作为折衷,本 PEP 规定 [tool] 中记录的详细信息**必须**是可丢弃的,并且**不得**影响包的安装。

列出文件的需求输入

目前,文件不记录作为文件输入的各项要求。这是为了简化,并且不以某种不可预见的方式明确限制文件(例如,为具有不同要求的新平台更新初始创建后的文件,而无需解决如何编写一套全面的要求)。

但如果原始要求以某种方式记录下来,可能会有助于审计和任何文件的重新创建。如果文件中使用了多个要求,这可以是一个字符串或一个字符串数组。

最终认为,试图以通用方式捕获工具用于构建锁定文件的输入过于复杂。

审计

记录依赖者

记录包的依赖者对于安装它来说并非必要。因此,它已被排除在本 PEP 之外,因为它可以通过 [tool] 包含。

但是,了解一个包对其他包的重要性可能是有益的。此信息已包含在 pip-tools 中,因此有先例包含它。可以使用灵活的方法记录依赖者,例如,详细程度足以区分文件中同一包的任何其他条目(灵感来自 uv)。

然而,最终决定记录依赖关系是更好的选择。

致谢

感谢所有参与 discuss.python.org 讨论的人。还要感谢 Randy Döring、Seth Michael Larson、Paul Moore 和 Ofek Lev 在公开之前对本 PEP 草案提供反馈。感谢 Poetry 的 Randy Döring、uv 的 Charlie Marsh 和 PDM 的 Frost Ming 代表他们各自的项目提供反馈。


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

上次修改:2025-06-20 17:43:34 GMT