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

Python 增强提案

PEP 725 – 在 pyproject.toml 中指定外部依赖项

作者:
Pradyun Gedam <pradyunsg at gmail.com>,Jaime Rodríguez-Guerra <jaime.rogue at gmail.com>,Ralf Gommers <ralf.gommers at gmail.com>
讨论至:
Discourse 帖子
状态:
草案
类型:
标准跟踪
主题:
打包
创建日期:
2023年8月17日
发布历史:
2023年8月18日2025年9月22日

目录

摘要

本 PEP 规定了如何在 pyproject.toml 文件中编写项目的外部(非 PyPI)构建和运行时依赖项,供打包相关工具使用。

本 PEP 建议在 pyproject.toml 中添加一个 [external] 表,包含七个键。“build-requires”、“host-requires”和“dependencies”用于指定三种类型的*必需*依赖项

  1. build-requires,用于在构建机器上运行的构建工具
  2. host-requires,主机机器所需的构建依赖项,但在构建时也需要。
  3. dependencies,在主机机器运行时需要,但在构建时不需要。

这三个键也有其*可选*的 external 对应项(optional-build-requiresoptional-host-requiresoptional-dependencies),它们扮演的角色与 project.optional-dependencies 对于 project.dependencies 所扮演的角色相同。最后,dependency-groups 提供了与 PEP 735 相同的功能,但适用于外部依赖项。

通过区分构建和主机依赖项,考虑了交叉编译。还支持可选的构建时和运行时依赖项,其方式类似于 [project] 表中支持的方式。

动机

Python 软件包可能依赖于构建工具、库、命令行工具或其他 PyPI 上不存在的软件。目前没有办法在标准化元数据中表达这些依赖项 [1], [2]。本 PEP 的主要动机是

  • 使工具能够自动将外部依赖项映射到其他打包存储库中的软件包,
  • 使 Python 软件包安装程序和构建前端发出的错误消息中包含所需的依赖项,
  • 为软件包作者提供一个规范的地方来记录此依赖项信息。

像 Linux 发行版、conda、Homebrew、Spack 和 Nix 这样的打包生态系统需要 Python 软件包的完整依赖项集,并且拥有 pyp2spec (Fedora)、Grayskull (conda) 和 dh_python (Debian) 等工具,它们试图从上游 Python 软件包的元数据中自动生成自己的软件包管理器的依赖项元数据。外部依赖项目前是手动处理的,因为 pyproject.toml 或任何其他标准位置中没有此元数据。其他工具则从 Python 软件包中的扩展模块和共享库中提取依赖项,例如 elfdeps (Fedora)。本 PEP 的一个关键好处是,通过仅使用显式注释的元数据来实现这种类型的转换自动化,使为发行版打包 Python 软件包变得更容易、更可靠。此外,作者设想其他类型的工具也将利用此信息,例如依赖项分析工具,如 RepologyDependabotlibraries.io

软件物料清单(SBOM)生成工具也可以使用此信息,例如,用于标记 pyproject.toml 中列出但未包含在 wheel 元数据中的外部依赖项可能已捆绑在 wheel 中。PEP 770 规范了 SBOM 如何包含在 wheel 中,其中包含一个关于该 PEP 与本 PEP 之间区别的说明性部分。

带有外部依赖项的软件包通常很难从源代码构建,并且构建失败的错误消息对于最终用户来说往往难以理解。最终用户系统上缺少外部依赖项是构建失败最可能的原因。如果安装程序可以在错误消息中显示所需的外部依赖项,这可能会为用户节省大量时间。

目前,外部依赖项的信息仅在各个软件包的安装文档中捕获。软件包作者很难维护,并且往往会过时。用户和发行版打包人员也很难找到它。拥有一个规范的地方来记录此依赖项信息将改善这种情况。

本 PEP 并非试图指定如何使用外部依赖项,也不是一种实现从 PyPI 上发布的 Python 项目的规范软件包名称到其他打包生态系统中的软件包名称映射的机制。规范名称和名称映射机制将在 PEP 804 中讨论。

基本原理

外部依赖项的类型

可以区分多种类型的外部依赖项

  • 可以通过名称识别并在另一种特定语言的软件包存储库中具有规范位置的具体软件包。例如,crates.io 上的 Rust 软件包,CRAN 上的 R 软件包,npm registry 上的 JavaScript 软件包。
  • 可以通过名称识别但没有明确规范位置的具体软件包。这通常适用于用 C、C++、Fortran、CUDA 和其他低级语言编写的库和工具。例如,Boost、OpenSSL、Protobuf、Intel MKL、GCC。
  • “虚拟”软件包,它们是概念、工具类型或接口的名称。这些通常有多种实现,它们*是*具体软件包。例如,C++ 编译器、BLAS、LAPACK、OpenMP、MPI。

具体软件包很容易理解,并且是每个软件包管理系统都存在的概念。虚拟软件包也是一些打包系统(但并非总是如此)中存在的概念,它们的实现细节各不相同。

交叉编译

交叉编译(截至 2025 年 9 月)尚未得到 stdlib 模块和 pyproject.toml 元数据的良好支持。然而,在将外部依赖项转换为其他打包系统的依赖项(使用 pyp2spec 等工具)时,交叉编译非常重要。在本 PEP 中立即引入对交叉编译的支持比将来扩展 [external] 要容易得多,因此作者选择现在将其包含在内。

术语

本 PEP 使用以下术语

  • 构建机器:执行软件包构建过程的机器。
  • 主机机器:将安装和运行生成工件的机器。
  • 构建依赖项:仅在构建过程中所需的软件包。它必须在构建时可用,并为构建机器的操作系统和架构构建。典型示例包括编译器、代码生成器和构建工具。
  • 主机依赖项:在构建期间需要,通常在运行时也需要的软件包。它必须在构建期间可用,并为主机机器的操作系统和架构构建。这些通常是项目链接的库。
  • 运行时依赖项:仅在安装后使用软件包时才需要的软件包。它在构建时不需要,但必须在主机机器运行时可用。

请注意,此术语在构建和打包工具之间不一致,因此在比较 pyproject.toml 中的构建/主机依赖项与其他软件包管理器的依赖项时必须小心。

请注意,本 PEP 中未使用“目标机器”或“目标依赖项”。这通常仅与交叉编译编译器或其他此类高级场景相关 [3], [4] – 这超出了本 PEP 的范围。

最后,请注意,虽然“依赖项”是构建时所需软件包最广泛使用的术语,但 pyproject.toml 中用于 PyPI 构建时依赖项的现有键是 build-requires。因此,本 PEP 在 [external] 下使用键 build-requireshost-requires 以保持一致性。

构建和主机依赖项

明确分离与构建和主机平台定义相关的元数据,而不是假设构建和主机平台始终相同,这很重要 [5]

构建依赖项通常在构建过程中运行 - 它们可能是编译器、代码生成器或其他此类工具。如果使用构建依赖项意味着运行时依赖项,则无需显式声明该运行时依赖项。例如,当使用 gfortran 将 Fortran 代码编译成 Python 扩展模块时,该软件包可能会依赖于 libgfortran 运行时库。不显式列出此类运行时依赖项的原因有两方面:(1)它可能取决于编译器/链接器标志或构建环境的详细信息,以确定依赖项是否存在;(2)这些运行时依赖项可以由 auditwheel 等工具自动检测和处理。

主机依赖项通常不在构建过程中运行,而仅用于链接。但这并非规则——可能需要通过模拟器或通过 crossenv 等自定义工具运行主机依赖项。当主机依赖项意味着运行时依赖项时,也不必声明该运行时依赖项,就像构建依赖项一样。

当声明了主机依赖项且正在执行与交叉编译无关的操作的工具,它可能会决定将 host-requires 列表合并到 build-requires 中——这是否有用取决于上下文。

指定外部依赖项

具体软件包规范

“软件包 URL”或 PURL 是一种广泛使用的 URL 字符串,用于标识旨在跨打包生态系统可移植的软件包。其设计是

scheme:type/namespace/name@version?qualifiers#subpath

scheme 组件是一个固定字符串 pkg,其他组件中只有 typename 是必需的。

由于外部依赖项很可能是手动输入的,我们提出了一个 PURL 派生词,为了符合人体工程学和用户友好性,它引入了许多更改(下面将进一步讨论)

  • 通过新的 virtual 类型支持虚拟软件包。
  • 允许 version 字段中使用版本范围(而不仅仅是字面量)。

在此派生词中,我们将 pkg 方案替换为 dep。因此,我们将它们称为 DepURLs。

例如,PyPI 上 requests 软件包的 DepURL 将是

dep:pypi/requests
# equivalent to pkg:pypi/requests

采用 PURL 兼容字符串在 pyproject.toml 中指定外部依赖项,一次性解决了许多问题,并且 Python 和其他多种语言中已经有该规范的实现。PURL 也已受依赖项相关工具支持,如 SPDX(参见 SPDX 2.3 规范中的外部存储库标识符)、开放源代码漏洞格式Sonatype OSS Index;不必等待数年才能获得此类工具的支持,这是很有价值的。DepURL 可以非常轻松地转换为 PURL,除了 dep:virtual,它在 PURL 中没有等效项。

对于没有可引用的规范包管理器的具体包,可以使用 dep:generic/dep-name,或者直接引用包维护的 VCS 系统(例如,dep:github/user-or-org-name/dep-name)。这两种方式哪种更合适取决于具体情况。本 PEP 建议在包名明确且众所周知(例如,dep:generic/gitdep:generic/openblas)时使用 dep:generic,否则使用 VCS 作为类型。为任何给定包选择哪个名称作为规范名称,以及制定和记录此类选择的过程,是 PEP 804 的主题。

虚拟软件包规范

PURL 尚未提供对虚拟或虚拟依赖规范的支持。添加虚拟类型 的提案正在为修订版 1.1 讨论中。

与此同时,我们建议为我们的 dep: 派生词添加一个新的*类型*,即 virtual 类型,它可以接受两个*命名空间*(可通过 PEP 804 中给出的过程进行扩展)

  • interface:用于 BLAS 或 MPI 等组件。
  • compiler:用于 C 或 Rust 等编译语言。

此*名称*应是接口或语言最常见的名称,小写。一些示例包括

dep:virtual/compiler/c
dep:virtual/compiler/cxx
dep:virtual/compiler/rust
dep:virtual/interface/blas
dep:virtual/interface/lapack

由于此类依赖项数量有限,似乎它将被理解并很好地映射到具有虚拟软件包的 Linux 发行版以及 conda 和 Spack 等。

版本控制

PURL 通过 URL 的 @ 组件支持固定版本。例如,numpy===2.0 可以表达为 pkg:pypi/numpy@2.0

PURL 支持超越固定版本的版本表达式和范围,通过 vers URI(参见规范

vers:type/version-constraint|version-constraint|...

用户应该将 pkg: URL 与 vers: URL 耦合。例如,要表达 numpy>=2.0,PURL 等效项将是 pkg:pypi/numpy 加上 vers:pypi/>=2.0。这可以通过以下方式完成

  • 一个两项列表:["pkg:pypi/numpy", "vers:pypi/>=2.0"]
  • 一个百分比编码的 URL 限定符:pkg:pypi/numpy?vers=vers:pypi%2F%3E%3D2.0

由于这些选项都不太符合人体工程学,我们选择让 DepURLs 也接受版本范围指定符,其语义是 PEP 440 语义的子集。允许的运算符是那些在包管理器中广泛可用的运算符(例如,==>>= 很常见,而 ~= 则不常见)。

一些例子

  • dep:pypi/numpy@2.0: numpy 精确锁定在 2.0 版本。
  • dep:pypi/numpy@>=2.0: numpy 版本大于或等于 2.0。
  • dep:virtual/interface/lapack@>=3.7.1: 任何实现 LAPACK 接口且版本大于或等于 3.7.1 的包。

特定虚拟软件包的版本方案(如果上游项目或标准未明确定义)将在中央注册表中定义(参见 PEP 804)。

环境标记

可以在 DepURLs 后面使用常规环境标记(最初定义于 PEP 508)。PURL 限定符,使用 ? 后跟包类型特定的依赖项规范器组件,不应用于环境标记足以满足的目的。这样做的原因是务实的:环境标记已用于 pyproject.toml 中的其他元数据,因此与 pyproject.toml 一起使用的任何工具可能已经有一个强大的实现来解析它。我们不期望需要 PURL 限定符提供的额外可能性(例如,指定 Conan 或 conda 频道,或 RubyGems 平台)。

我们将 DepURL 和环境标记的组合命名为“外部依赖项规范器”,类似于现有的依赖项规范器

依赖项的规范名称和 -dev(el) 分割软件包

发行版将一个软件包拆分成两个或更多软件包的情况相当常见,但远非普遍。特别是,运行时组件通常可以与开发组件(头文件、pkg-config 和 CMake 文件等)分开安装。后者通常具有名称,并在项目/库名称后附加 -dev-devel。此外,为了保持安装大小可控,较大的软件包有时会被拆分成多个独立的软件包。通常情况下,此类软件包拆分未由软件包维护者定义或识别,因此任何拆分的含义都是模糊的。因此,此类拆分不应反映在 [external] 表中。无法以在所有发行版中都适用的合理方式指定此内容,因此 [external] 中应仅使用规范名称。

使用 DepURL 的预期含义是“具有指定名称的完整软件包”。即,包括软件包中的所有可安装工件。软件包拆分是否相关将取决于元数据使用的上下文。例如,如果 libffi 是主机依赖项,并且工具想要为构建 wheel 准备环境,那么如果发行版已将 libffi 的头文件拆分到 libffi-devel 软件包中,那么该工具必须同时安装 libffilibffi-devel

关于如何定义规范软件包名称以及工具在实际使用 [external] 进行安装时如何处理软件包拆分,请参考 PEP 804

Python 开发头文件

Python 头文件和其他构建支持文件也可能被拆分。这与上面一节的情况相同(因为 Python 在发行版中只是一个常规软件包)。然而,python-dev|devel 依赖项很特殊,因为在 pyproject.toml 中,Python 本身是隐式而非显式依赖项。因此,这里需要做出选择——是隐式添加 python-dev,还是让每个软件包作者在 [external] 下显式添加它。为了保持 Python 依赖项和外部依赖项之间的一致性,我们选择隐式添加它。当 [external] 表包含一个或多个编译器软件包时,必须假定 Python 开发头文件是必需的。

新的核心元数据字段

提议新增两个核心元数据字段

  • Requires-External-Dep。外部需求。模仿从 RequiresRequires-Dist 的过渡。我们选择了 -Dep 后缀,以强调该值不是常规的 Python 规范器(发行版),而是包含 DepURL 的外部依赖规范器。
  • Provides-External-Extra。一个仅包含外部依赖项(如 Requires-External-Dep 中所发现的)的*额外*组。

由于核心元数据规范不包含 pyproject.toml[build-system] 表中的任何元数据字段,因此 build-requireshost-requires 的内容无需反映在现有核心元数据字段中。

此外,本 PEP 还建议弃用 Requires-External 字段。理由如下

  • 避免与新提出的字段混淆。
  • 避免与现有用法(即使有限)的潜在不兼容性。
  • 在生态系统中的渗透率低

捆绑共享库对 wheel 元数据的影响

一个 wheel 可以捆绑其外部依赖项。这在将 wheel 分发到 PyPI 或其他 Python 包索引时尤其常见——auditwheeldelvewheeldelocate 等工具会自动完成此过程。因此,sdist 中的 Requires-External-Dep 条目可能会从使用 cibuildwheel 等工具从该 sdist 构建的 wheel 中消失。也可能 Requires-External-Dep 条目保留在 wheel 中,保持不变或具有更严格的限制。auditwheel 默认不捆绑某些允许列表中的依赖项,例如 OpenGL。此外,auditwheeldelvewheel 允许用户通过 --exclude--no-dll 命令行标志手动排除依赖项。这用于避免捆绑大型共享库,例如来自 CUDA 的库。

因此,从 pyproject.toml 中的外部依赖项生成的 Requires-External-Dep 条目可能因构建/分发过程而异,在 sdist 及其对应的 wheel(s) 之间存在差异。

请注意,这并不意味着该字段必须标记为 Dynamic,因为此区分仅适用于由构建后端从 sdist 构建的 wheel。特别是,从其他 wheel 构建的 wheel 不需要满足此约束。

依赖项组

本 PEP 选择在 [external] 表下也包含 PEP 735dependency-groups。这一决定是出于对外部元数据具有类似功能的需求。顶层表不能用于外部依赖项,因为它需要 PEP 508 字符串(以及用于组包含的表),而我们选择依赖 dep: URL 来表示外部依赖项。将两者混淆将引发与现有用法相关的重大向后兼容性问题。

严格来说,dependency-groups 模式允许我们在每组子表中定义外部依赖项

[dependency-groups]
dev = [
  "pytest",
  { external = ["dep:cargo/ripgrep"] },
]

然而,这也有同样的问题:我们将不同类型的依赖项规范器混合在同一个数据结构中。我们认为将关注点分离到不同的顶层表中会更清晰,因此我们仍然倾向于使用 external.dependency-groups

可选依赖项与依赖项组

external.dependency-groups 的理由与 PEP 735 中引入 [dependency-groups] 的理由相同。因此,其预期的使用和包含/排除到核心元数据中的语义与 [dependency-groups] 相同。

external.optional-dependencies 将出现在核心元数据中。external.dependency-groups 则不会。

规范

如果元数据指定不当,工具**必须**抛出错误,以通知用户其错误。

DepURL

DepURL 实现了识别软件包的方案,旨在跨打包生态系统可移植。其设计是

dep:type/namespace/name@version?qualifiers#subpath

dep: 是一个固定字符串,始终存在。typename 是必需的,其他组件是可选的。所有组件均适用于 PURL 和虚拟 type,并具有以下要求

  • type (必需):**必须**是 PURL type,或 virtual
  • namespace (可选):**必须**是 PURL namespace,或是 DepURL 中央注册表中的命名空间(参见 PEP 804)。
  • name(必需):**必须**是一个可以解析为有效的 PURL name 的名称。如果名称未出现在 DepURL 中央注册表(参见 PEP 804)中,工具**可以**发出警告或错误。
  • version(可选):**必须**是一个常规的版本指定符(PEP 440 语义),可以是单个版本或版本范围,但仅限于以下运算符:>=><<===,
  • qualifiers(可选):**必须**解析为有效的 PURL qualifier
  • subpath(可选):**必须**解析为有效的 PURL subpath

外部依赖项规范器

外部依赖项规范器**必须**包含 DepURL,并且**可以**包含与常规依赖项规范器(最初在PEP 508中指定)所用语法相同的环境标记。

核心元数据的更改

弃用

External-Requires 核心元数据字段将被标记为*已弃用*,不鼓励使用。

新增

核心元数据新增两个字段

  • Requires-External-Dep。外部需求,表示为外部依赖项规范器字符串。
  • Provides-External-Extra。一个仅包含外部依赖项(如 Requires-External-Dep 中所发现的)的*额外*组。

版本提升

鉴于拟议的更改纯粹是增量式的,核心元数据版本将提升至 2.6。

这只会影响 PyPI 和支持外部运行时依赖项的工具,否则不需要任何更改。

pyproject.toml 中的更改

请注意,pyproject.toml 内容的格式与 PEP 621 中的格式相同。

表名

工具**必须**将本 PEP 定义的字段指定在名为 [external] 的表中。任何工具均不得向此表添加非本 PEP 或后续 PEP 定义的字段。缺少 [external] 表意味着该软件包不包含任何外部依赖项,或者其包含的外部依赖项被假定已存在于系统中。

build-requires/optional-build-requires

  • 格式:外部依赖项规范器数组(build-requires)以及一个包含外部依赖项规范器数组值的表(optional-build-requires
  • 核心元数据:不适用

构建项目所需的(可选)外部构建要求。

对于 build-requires,它是一个键,其值是字符串数组。每个字符串表示项目的一个构建要求,并且**必须**格式化为有效的外部依赖项规范器。

对于 optional-build-requires,它是一个表,其中每个键指定一组额外的构建要求,其值是一个字符串数组。数组的字符串**必须**是有效的外部依赖项规范器。

host-requires/optional-host-requires

  • 格式:外部依赖项规范器数组(host-requires)以及一个包含外部依赖项规范器数组值的表(optional-host-requires) - 核心元数据:不适用

构建项目所需的(可选)外部主机要求。

对于 host-requires,它是一个键,其值是字符串数组。每个字符串表示项目的主机要求,并且**必须**格式化为有效的外部依赖项规范器。

对于 optional-host-requires,它是一个表,其中每个键指定一组额外的主机要求,其值是一个字符串数组。数组的字符串**必须**是有效的外部依赖项规范器。

dependencies/optional-dependencies

  • 格式:外部依赖项规范器数组(dependencies)以及一个包含外部依赖项规范器数组值的表(optional-dependencies
  • 核心元数据Requires-External-DepProvides-External-Extra

项目(可选)的运行时依赖项。

对于 dependencies,它是一个键,其值是字符串数组。每个字符串表示项目的依赖项,并且**必须**格式化为有效的外部依赖项规范器。每个字符串都必须作为 Requires-External-Dep 字段添加到核心元数据中。

对于 optional-dependencies,它是一个表,其中每个键指定一个*额外*项,其值是字符串数组。数组的字符串**必须**是有效的外部依赖项规范器。对于每个 optional-dependencies

  • 组的名称**必须**作为 Provides-External-Extra 字段添加到核心元数据中。
  • 该组中的外部依赖项规范器**必须**作为 Requires-External-Dep 字段添加到核心元数据中,并带有相应的 ; extra == 'name' 环境标记。

dependency-groups

  • 格式:一个表,其中每个键是组的名称,值是外部依赖项规范器数组、表或两者的混合。
  • 核心元数据:不适用

PEP 735 风格的依赖项组,但使用外部依赖项规范器而不是 PEP 508 字符串。所有其他细节(例如组包含、名称规范化)遵循官方的依赖项组规范

示例

这些示例展示了许多软件包的 [external] 表内容,以及相应的 PKG-INFO/METADATA 内容(如果有)。

cryptography 39.0

pyproject.toml 内容

[external]
build-requires = [
  "dep:virtual/compiler/c",
  "dep:virtual/compiler/rust",
  "dep:generic/pkg-config",
]
host-requires = [
  "dep:generic/openssl",
  "dep:generic/libffi",
]

PKG-INFO / METADATA 内容:不适用。

SciPy 1.10

pyproject.toml 内容

[external]
build-requires = [
  "dep:virtual/compiler/c",
  "dep:virtual/compiler/cpp",
  "dep:virtual/compiler/fortran",
  "dep:generic/ninja",
  "dep:generic/pkg-config",
]
host-requires = [
  "dep:virtual/interface/blas",
  "dep:virtual/interface/lapack@>=3.7.1",
]

PKG-INFO / METADATA 内容:不适用。

Pillow 10.1.0

pyproject.toml 内容

[external]
build-requires = [
  "dep:virtual/compiler/c",
]
host-requires = [
  "dep:generic/libjpeg",
  "dep:generic/zlib",
]

[external.optional-host-requires]
extra = [
  "dep:generic/lcms2",
  "dep:generic/freetype",
  "dep:generic/libimagequant",
  "dep:generic/libraqm",
  "dep:generic/libtiff",
  "dep:generic/libxcb",
  "dep:generic/libwebp",
  "dep:generic/openjpeg@>=2.0",
  "dep:generic/tk",
]

PKG-INFO / METADATA 内容:不适用。

Spyder 6.0

pyproject.toml 内容

[external]
dependencies = [
  "dep:cargo/ripgrep",
  "dep:cargo/tree-sitter-cli",
  "dep:golang/github.com/junegunn/fzf",
]

PKG-INFO / METADATA 内容

Requires-External-Dep: dep:cargo/ripgrep
Requires-External-Dep: dep:cargo/tree-sitter-cli
Requires-External-Dep: dep:golang/github.com/junegunn/fzf

jupyterlab-git 0.41.0

pyproject.toml 内容

[external]
dependencies = [
  "dep:generic/git",
]

[external.optional-build-requires]
dev = [
  "dep:generic/nodejs",
]

PKG-INFO / METADATA 内容

Requires-External-Dep: dep:generic/git

PyEnchant 3.2.2

pyproject.toml 内容

[external]
dependencies = [
  # libenchant is needed on all platforms but vendored into wheels
  # distributed on PyPI for Windows. Hence choose to encode that in
  # the metadata. Note: there is no completely unambiguous way to do
  # this; another choice is to leave out the environment marker in the
  # source distribution and either live with the unnecessary ``METADATA``
  # entry in the distributed Windows wheels, or to apply a patch to this
  # metadata when building those wheels.
  "dep:github/AbiWord/enchant; platform_system!='Windows'",
]

PKG-INFO / METADATA 内容

Requires-External-Dep: dep:github/AbiWord/enchant; platform_system!="Windows"

带依赖项组

pyproject.toml 内容

[external.dependency-groups]
dev = [
  "dep:generic/catch2",
  "dep:generic/valgrind",
]

PKG-INFO / METADATA 内容:不适用。

向后兼容性

对向后兼容性没有影响,因为本 PEP 仅添加了新的可选元数据。在缺少此类元数据的情况下,软件包作者或打包工具不会有任何变化。

本 PEP 引入的唯一对现有项目有影响的更改是弃用 External-Requires 核心元数据字段。考虑到其在生态系统中的低渗透率(参见基本原理),我们估计此弃用带来的影响可以忽略不计。

该字段仍将由现有工具(如 setuptools-ext)识别,但在 Python Packaging User Guide 中将不鼓励使用它,类似于对 Requires 等过时字段(已弃用并被 Requires-Dist 取代)的处理方式。

安全隐患

本 PEP 涵盖了如何静态定义外部依赖项的元数据,因此没有直接的安全问题。任何安全问题都源于工具如何使用元数据并选择对其采取行动。

如何教授此内容

外部依赖项以及这些外部依赖项是否以及如何被捆绑,通常是 Python 包作者不详细了解的主题。我们打算从外部依赖项的定义开始,它如何被依赖(从仅运行时通过 ctypessubprocess 调用,到它作为被链接的构建依赖项),然后再探讨如何在元数据中声明外部依赖项。文档应明确哪些对包作者重要,哪些对发行版打包人员重要。

有关此主题的材料将添加到最相关的打包教程中,主要是 Python Packaging User Guide。此外,我们期望任何添加对外部依赖项元数据支持的构建后端都将在其文档中包含相关信息,auditwheel 等工具也将如此。

参考实现

本 PEP 包含一个元数据规范,而不是一个代码功能——因此不会有实现整个元数据规范的代码。但是,有些部分确实有参考实现

  1. [external] 表必须是有效的 TOML,因此可以使用 tomllib 加载。该表可以使用 pyproject-external 软件包进一步处理,如下所示。
  2. PURL 规范作为本规范的关键部分,有一个 Python 软件包提供了用于构建和解析 PURL 的参考实现:packageurl-python。该软件包封装在 pyproject-external 中,以提供 DepURL 特定的验证和处理。

一旦元数据添加到 Python 软件包中,此元数据有多种可能的消费者和用例。 PyPI 上下载量最大的 150 个软件包中,已发布特定平台 wheel 的所有软件包的测试元数据可以在 rgommers/external-deps-build 中找到。此元数据已通过将其用于在干净的 Docker 容器中从修补了该元数据的 sdist 构建 wheel 进行验证。

示例

给定一个带有此 [external] 表的 pyproject.toml

[external]
build-requires = [
  "dep:virtual/compiler/c",
  "dep:virtual/compiler/rust",
  "dep:generic/pkg-config",
]
host-requires = [
  "dep:generic/openssl",
  "dep:generic/libffi",
]

您可以使用 pyproject_external.External 来解析和操作它

>>> from pyproject_external import External
>>> external = External.from_pyproject_path("./pyproject.toml")
>>> external.validate()
>>> external.to_dict()
{'external': {'build_requires': ['dep:virtual/compiler/c', 'dep:virtual/compiler/rust', 'dep:generic/pkg-config'], 'host_requires': ['dep:generic/openssl', 'dep:generic/libffi']}}
>>> external.build_requires
[DepURL(type='virtual', namespace='compiler', name='c', version=None, qualifiers={}, subpath=None), DepURL(type='virtual', namespace='compiler', name='rust', version=None, qualifiers={}, subpath=None), DepURL(type='generic', namespace=None, name='pkg-config', version=None, qualifiers={}, subpath=None)]
>>> external.build_requires[0]
DepURL(type='virtual', namespace='compiler', name='c', version=None, qualifiers={}, subpath=None)

请注意,提议的 [external] 表是格式正确的。如果内容无效,例如

[external]
build-requires = [
  "dep:this-is-missing-the-type",
  "pkg:not-a-dep-url"
]

您将无法通过验证

>>> external = External.from_pyproject_data(
  {
    "external": {
      "build_requires": [
        "dep:this-is-missing-the-type",
        "pkg:not-a-dep-url"
      ]
    }
  }
)
ValueError: purl is missing the required type component: 'dep:this-is-missing-the-type'.

被拒绝的想法

同时也在 PyPI 上打包的外部依赖项的特定语法

有一些非 Python 软件包也在 PyPI 上打包,例如 Ninja、patchelf 和 CMake。通常希望使用的是这些软件包的系统版本,如果系统上不存在,则安装其 PyPI 软件包。作者认为,为此场景提供特定支持不是必需的(或者至少,过于复杂而无法证明这种支持是合理的);外部依赖项的依赖项提供者可以将 PyPI 视为获取软件包的一种可能来源。 PEP 804 中提出了此用例的示例映射。

使用库和头文件名称作为外部依赖项

此前的一份 PEP 草案(“外部依赖项”(2015))提议使用特定的库和头文件名称作为外部依赖项。这既过于细化,又不够充分(例如,头文件通常不带版本;多个软件包可能提供相同的头文件或库)。使用软件包名称是跨打包生态系统的一个成熟模式,应该优先采用。

使用显式的 -dev-devel 后缀分割主机依赖项

此约定在不同的打包生态系统中并不一致,也未被上游软件包作者普遍接受。由于对显式控制(例如,当软件包作为运行时依赖项而非构建时依赖项使用时安装头文件)的需求相当小众,并且我们不想在没有足够明确用例的情况下增加设计复杂性,因此我们选择仅依赖于 buildhostrun 类别划分,由工具负责在特定上下文中确定哪个类别适用于每种情况。

如果这被证明不足,未来的 PEP 可以使用 PURL 模式中存在的 URL 限定符功能(?key=value)来实现必要的调整。这可以以向后兼容的方式完成。

标识符间接引用

某些生态系统展示了根据参数化函数(如 cmake("dependency")compiler("language"))选择包的方法,这些函数根据一些额外上下文或配置返回包名。此功能可以说并不常见,即使存在,也 rarely used。此外,其动态性使其容易随时间改变含义,并且依赖特定的构建系统进行名称解析通常不是一个好主意。

作者更倾向于可以通过众所周知的元数据显式映射的静态标识符(例如,如 PEP 804 中提议的)。

实现这些间接引用的生态系统可以使用它们来支持旨在生成 PEP 804 中提议的映射的基础设施。

[build-system] 下添加 host-requires

原则上,为 PyPI 上的主机依赖项添加 host-requires 以更好地支持与支持交叉编译的其他打包系统的名称映射似乎很有用,原因与本 PEP 在 [external] 表下添加 host-requires 相同。然而,本 PEP 不需要包含此内容,因此作者倾向于将本 PEP 的范围限制在有限的范围内——未来的交叉编译 PEP 可能希望解决此问题。此问题包含更多支持和反对在本 PEP 中将 host-requires 添加到 [build-system] 下的论据。

在核心元数据中重用 Requires-External 字段

核心元数据规范包含一个相关字段,即 Requires-External。虽然乍一看它可能是记录 external.dependencies 表的良好候选者,但作者已决定不重复使用此字段来传播外部运行时依赖项元数据。

Requires-External 字段在 2.4 版本中语义定义非常松散。本质上是:name [(version)][; environment marker](方括号表示可选字段)。未定义 name 的有效字符串;规范中的示例同时使用了“C”作为语言名称和“libpng”作为包名称。收紧语义将导致向后不兼容,而保持现状似乎不尽人意。DepURLs 需要分解才能适应此语法。

允许使用生态系统特定的版本比较语义

在某些情况下,特别是处理预发布版本时,PEP 440 的版本比较语义并不完全适用。例如,1.2.3a 可能表示 1.2.3 之后的发布版本,而不是 alpha 版本。为了正确处理这些情况,有必要允许任意版本方案。本 PEP 的作者认为,允许这样做所增加的价值不值得增加的复杂性。如果需要,包作者可以使用代码注释或 DepURL 的 qualifier 字段(参见“基本原理”下的“版本控制”部分)来捕获此级别的详细信息。

未解决的问题

目前没有。

参考资料


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

最后修改时间:2025-09-29 13:22:31 GMT