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

Python 增强提案

PEP 804 – 外部依赖注册表和名称映射机制

作者:
Pradyun Gedam <pradyunsg at gmail.com>, Ralf Gommers <ralf.gommers at gmail.com>, Michał Górny <mgorny at quansight.com>, Jaime Rodríguez-Guerra <jaime.rogue at gmail.com>, Michael Sarahan <msarahan at gmail.com>
讨论至:
Discourse 帖子
状态:
草案
类型:
标准跟踪
主题:
打包
要求:
725
创建日期:
2025年9月3日
发布历史:
2025年9月22日

目录

摘要

本 PEP 规定了一种名称映射机制,允许打包工具将外部依赖标识符(如 PEP 725 中引入的)映射到它们在其他包存储库中的对应项。

动机

PyPI 上的包通常需要 PyPI 中不存在的构建时和运行时依赖项。PEP 725 引入了元数据来表达此类依赖项。为 Python 包使用具体的外部依赖元数据需要将给定的依赖标识符映射到其他生态系统使用的说明符,这将允许

  • 使工具能够自动将外部依赖项映射到其他打包存储库/生态系统中的包,
  • 在 Python 包安装程序和构建前端发出的错误消息中,包含所需的外部依赖项**(带有用户系统上相关系统包管理器使用的包名称)**,并允许用户直接查询这些名称以获取安装说明。

像 Linux 发行版、conda、Homebrew、Spack 和 Nix 这样的打包生态系统需要 Python 包的完整依赖集,并且拥有 pyp2rpm (Fedora)、Grayskull (conda) 和 dh_python (Debian) 等工具,它们试图自动从上游 Python 包中可用的元数据生成依赖信息。在 PEP 725 之前,外部依赖项是手动处理的,因为 pyproject.toml 或任何其他标准元数据文件中没有此元数据。实现其自动转换是此 PEP 的一个关键优势,使 Python 打包更容易、更可靠。此外,作者设想其他类型的工具利用这些信息;例如,RepologyDependabotlibraries.io 等依赖分析工具。

基本原理

现有技术

R 语言有一个 R 包的系统要求,其中包含一个中心注册表,该注册表知道如何将外部依赖元数据转换为 apt-get 等包管理器的安装命令。此注册表集中了针对一系列 Linux 发行版以及 Windows 的映射。macOS 不存在。其 README 的“规则覆盖率” 曾显示该系统提高了从 CRAN 源构建包的成功几率。在所有 CRAN 包中,Ubuntu 18 从 78.1% 提高到 95.8%,CentOS 7 从 77.8% 提高到 93.7%,openSUSE 15.0 从 78.2% 提高到 89.7%。成功几率取决于注册表的维护情况,但收益是显著的:在 Docker 容器中,Ubuntu 和 CentOS 上构建失败的包数量减少了约 4 倍。

基于 RPM 的发行版,如 Fedora,可以在 pyp2rpm 中使用 基于规则的实现 (NameConvertor)。主要规则是 PyPI 包的 RPM 名称是 f"python-{pypi_package_name}"。这似乎效果很好;有一些变体,如 Python 版本特定的名称,其中前缀包含 Python 主版本号和次版本号(例如 python311- 而不是 python-)。

Gentoo 遵循类似的 Python 包命名方法,使用 dev-python/ 类别和一些 明确的规则

Conda-forge 有一个更明确的名称映射,因为 conda-forge 中的基本名称与 PyPI 上的相同(例如,numpy 映射到 numpy),但由于名称冲突和重命名,存在许多例外(例如,PyTorch 的 PyPI 名称是 torch,而在 conda-forge 中它是 pytorch)。有几个名称映射工作由不同的团队维护。Conda-forge 的基础设施在 regro/cf-graph-countyfair 中生成了一个。Grayskull 维护 其自己的精选映射。Prefix.dev 创建了 parselmouth 映射 以支持其工具中的 conda 和 PyPI 集成。有关其方法、优点和缺点的更完整概述可在 conda/grayskull#564 中找到。

OpenStack 生态系统也需要处理一些映射工作。所有这些都只关注 Linux 发行版。pkg-map 伴随 diskimage-builder,并提供一种文件格式,用户在其中定义任意变量名称及其在目标发行版(Red Hat、Debian、OpenSUSE 等)中的相应名称。参见 PyYAML 示例bindep 定义了一个文件 bindep.txt(参见 示例),用户可以在其中写下无法从 PyPI 安装的依赖项。该格式是基于行的,每行包含一个在 Debian 生态系统中找到的依赖项。对于其他发行版,它在方括号之间提供“过滤器”语法,用户可以在其中指示其他目标平台、可选依赖项和附加项。

对映射的需求也存在于其他生态系统,例如 SageMath,但最终用户也希望使用他们选择的系统包管理器安装 PyPI 包(StackOverflow 示例问题)。

名称映射的治理和维护成本

外部依赖映射到大量打包生态系统的维护成本可能很高。我们选择以以下方式定义注册表:

  • 中央机构维护公认的 DepURL 列表和已知生态系统映射。
  • 映射本身由目标打包生态系统维护。

因此,该系统对于给定生态系统是可选的,并且相关的维护成本是分散的。

生成特定于包管理器的安装命令

具有外部依赖项的 Python 包作者通常在其文档中提供这些外部依赖项的安装说明。这些说明难以编写和保持最新,并且通常只涵盖一个或最多少数几个平台。例如,以下是 SciPy 的外部构建依赖项(C/C++/Fortran 编译器、OpenBLAS、pkg-config)的说明:

  • Debian/Ubuntu: sudo apt install -y gcc g++ gfortran libopenblas-dev liblapack-dev pkg-config python3-pip python3-dev
  • Fedora: sudo dnf install gcc-gfortran python3-devel openblas-devel lapack-devel pkgconfig
  • CentOS/RHEL: sudo yum install gcc-gfortran python3-devel openblas-devel lapack-devel pkgconfig
  • Arch Linux: sudo pacman -S gcc-fortran openblas pkgconf
  • macOS 上的 Homebrew: brew install gfortran openblas pkg-config

包名称差异很大,并且存在一些差异,例如一些发行版将头文件和其他构建时依赖项分离到单独的 -dev/-devel 包中,而另一些则没有。通过此 PEP 中的注册表,可以通过具有“**显示此生态系统首选的包管理器安装所有外部依赖项的命令**”语义的工具命令使其更全面且更易于维护。这可以作为独立工具完成,也可以作为任何 Python 开发工作流工具(例如 Pip、Poetry、Hatch、PDM、uv)中的新子命令完成。

为此,每个生态系统映射都可以提供一个已知兼容的包管理器列表,以及有关如何安装和查询包的模板化说明。提供的安装命令模板与查询命令模板配对,以便这些工具可以检查所需的包是否已存在,而无需尝试安装操作(这可能很昂贵并且具有版本升级等意外副作用)。

注册表设计

映射基础设施旨在提供以下组件和属性

  • PEP 725 标识符 (DepURL) 的中央注册表,至少包括被认为是规范的知名通用和虚拟标识符。
  • 已知生态系统列表,生态系统维护者可以在其中注册其名称映射。
  • 一个标准化模式,定义映射应如何构建。每个映射还可以提供有关其支持的包管理器如何工作的编程细节。

上述文档以 JSON 文件的形式提供,并由配套的 JSON 模式进行验证。提供了一个 Python 库和 CLI 来查询和利用这些资源。用户可以配置他们偏好使用哪个系统包管理器来获取默认包映射和命令生成(例如,Ubuntu 用户可能偏好使用 condabrewspack 而不是 apt 作为他们选择的包管理器来提供外部依赖项)。

规范

提出了三种模式

  1. 如 PEP 725 中引入的已知 DepURL 的中心注册表。
  2. 已知生态系统列表及其映射的规范 URL。
  3. DepURL 到其相应生态系统说明符的特定于生态系统的映射,以及其包管理器的详细信息。

中心注册表

中心注册表定义了哪些标识符被认为是规范的,以及已知的别名。每个条目**必须**在 id 字段中提供有效的 DepURL,并带有一个可选的自由格式 description 文本。此外,一个条目**可以**通过其 provides 字段引用另一个条目,该字段接受一个字符串或一个已在注册表中定义为 id 的字符串列表。这对于别名(例如 dep:generic/arrowdep:github/apache/arrow)和 dep:virtual/ 条目的具体实现(例如 dep:generic/gcc 将提供 dep:virtual/compiler/c)都很有用。没有 provides 内容,或者如果已填充,则仅包含 dep:virtual/ 标识符的条目被认为是规范的。provides 字段**不得**出现在 dep:virtual/ 定义中。

拥有一个中心注册表可以启用 [external] 表的验证。所有相关工具**必须**检查提供的标识符是否格式正确。此外,某些工具**可以**检查正在使用的标识符是否被认为是规范的。更具体地说

  • 构建后端、构建前端和安装程序**不应**默认对标识符的规范性进行任何验证。
  • 上传者(如 twine)**应**验证标识符是否规范,并向用户发出警告或报告错误,并提供退出机制。如果可用,他们**应**建议规范的替代品。
  • 索引服务器(如 PyPI)**可以**执行与上传者相同的验证,并在必要时拒绝工件。

此注册表还**应**集中对其内容做出权威性决策,例如别名集合中的哪个条目被优先作为规范,或者哪个版本控制方案适用于虚拟 DepURL(参见附录 B)。相应的答案不在此 PEP 中给出;相反,我们将该责任委托给中心注册表维护者。

映射

映射指定哪些特定于生态系统的标识符提供中央注册表中可用的规范条目。映射主要由字典列表组成,其中每个条目包括

  • 一个带有规范 DepURL 的 id 字段。
  • 一个可选的自由格式 description 文本。
  • 一个 specs 字段,其值**必须**是以下之一:
    • 一个带有三个键(buildhostrun)的字典。这些值**必须**是一个字符串或字符串列表,表示作为构建时、宿主和运行时依赖项所需的特定于生态系统的包标识符(有关这些定义的详细信息,请参见 PEP 725)。
    • 为方便起见,也接受字符串或字符串列表作为简写形式。在这种情况下,标识符将用于填充上述三类。
    • 一个空列表,表示该生态系统没有提供此类依赖项的包。
  • 一个 specs_from 字段,其值是一个 DepURL,从中将导入 specs 字段。 specsspecs_from **必须**存在。
  • 一个可选的 urls 字段,其值**必须**是一个 URL、一个 URL 列表,或一个将字符串映射到 URL 的字典。这对于链接到提供有关映射包的更多信息的外部资源很有用。

映射还**应**指定另一个 package_managers 部分,报告生态系统中哪些包管理器可用以及如何使用它们。此字段**必须**接受一个字典列表,其中每个字典报告以下字段

  • name(字符串),此包管理器的唯一标识符。通常是可执行文件的名称。
  • commands(字典列表),用于安装映射包和检查它们是否已安装的命令。
  • specifier_syntax:关于如何将 PEP 440 说明符的子集映射到目标包管理器的说明。提供三个级别的支持:仅名称、仅精确版本和版本范围兼容性(带有每个操作符的翻译)。

每个映射都必须有一个用于在线检索的规范 URL。这些映射也可以打包以在每个平台进行离线分发。作者建议将其放置在每个操作系统的数据工件的标准位置;例如,Linux 和其他系统上的 $XDG_DATA_DIRS,macOS 上的 ~/Library/Application Support,以及 Windows 上的 %LOCALAPPDATA%。子目录标识符必须是 external-packaging-metadata-mappings。此数据目录应仅包含名为 {ecosystem-identifier}.mapping.json 的映射文档。中心注册表和已知生态系统文档也可以作为 registry.jsonknown-ecosystems.json 分别分发在此目录中。

已知生态系统

已知生态系统列表有两个作用

  1. 报告其映射的规范 URL。
  2. 为每个生态系统分配一个短标识符。这是上面提到的映射文件名中**必须**使用的标识符,以便它们可以在本地文件系统中找到。

对于与 Linux 发行版对应的生态系统,标识符**必须**是其 os-release ID 参数报告的标识符。对于其他生态系统,**必须**在提交到已知生态系统文档列表时决定。它**必须**只使用 os-releaseID 字段中允许的字符,如正则表达式 [a-z0-9\-_.]+ 所示。

模式详情

提供了三个 JSON Schema 文档以完全标准化注册表和映射。

中心注册表模式

中央注册表由以下 JSON schema 指定

$schema
类型 字符串
描述 文档中使用的定义列表模式的 URL。
必需
schema_version
类型 整数
必需
定义
类型 数组
描述 当前识别的 DepURL 列表。
必需

此列表中的每个条目定义为

字段 类型 描述 必需
id DepURLField(匹配正则表达式 ^dep:.+$string DepURL
描述 字符串 自由格式字段,用于添加有关包的一些详细信息。允许 Markdown。
提供 DepURLField | list[DepURLField] 此条目连接的标识符列表。用于注释别名或虚拟包实现。
网址 AnyUrl | list[AnyUrl] | dict[NonEmptyString, AnyUrl] 指向提供更多定义信息的网页位置的超链接。

已知生态系统模式

已知生态系统列表由以下 JSON Schema 指定

$schema
类型 字符串
描述 文档中使用的映射模式的 URL。
必需
schema_version
类型 整数
必需
生态系统
类型 字典
描述 生态系统名称及其对应详情。
必需

此字典将指代生态系统标识符的非空字符串键映射到定义为

值类型 值描述 必需
字面量['mapping'] 任意URL 到此生态系统映射的 URL

映射模式

映射由以下 JSON Schema 指定

$schema
类型 字符串
描述 文档中使用的映射模式的 URL。
必需
schema_version
类型 整数
必需
名称
类型 字符串
描述 模式名称
必需
描述
类型 string | None
描述 自由格式字段,用于添加此映射的信息。允许 Markdown。
必需
映射
类型 数组
描述 DepURL 到规范的映射列表。
必需

此列表中的每个条目定义为

字段 类型 描述 必需
id DepURLField(匹配正则表达式 ^dep:.+$string DepURL,如中心注册表中所提供
描述 字符串 自由格式字段,用于添加有关包的一些详细信息。允许 Markdown。
网址 AnyUrl | list[AnyUrl] | dict[NonEmptyString, AnyUrl] 指向提供更多定义信息的网页位置的超链接。
规格 string | list[string] | dict[Literal['build', 'host', 'run'], string | list[string]] 此包的生态系统特定标识符。完整形式是一个字典,将类别 buildhostrun 映射到其相应的包标识符。作为简写,可以提供单个字符串或字符串列表,在这种情况下,它们将用于统一填充这三个类别。 specsspecs_from 必须存在。
specs_from DepURLField(匹配正则表达式 ^dep:.+$string 从另一个映射条目获取规格。 specsspecs_from 必须存在。
extra_metadata dict[NonEmptyString, Any] 任意元数据的自由格式键值存储。
包管理器
类型 数组
描述 可用于在此生态系统中安装包的工具列表。
必需

此列表中的每个条目都定义为一个带有这些字段的字典:

字段 类型 描述 必需
名称 字符串 此包管理器的短标识符(通常是命令名称)
命令 dict[Literal['install', 'query'], dict[Literal['command', 'requires_elevation', 'multiple_specifiers'], list[str] | bool | Literal['always', 'name-only', 'never']]] 用于安装或查询给定包的命令。只允许两个键:installquery。它们的值是一个字典,包含
  • 一个必需的键 command,它接受一个字符串列表(如 subprocess.run 所期望的)。
  • 一个可选的 requires_elevation 布尔值(默认为 False),表示命令是否必须以提升的权限运行(例如 Windows 上的管理员,Linux 和 macOS 上的超级用户)。
  • 一个枚举 multiple_specifiers,它决定命令是否同时接受多个包说明符,接受以下之一
    • alwaysinstall 中的默认值。
    • name-only,命令仅在说明符不包含版本约束时接受多个说明符。
    • neverquery 中的默认值。

command 项中**必须**只有一个包含 {} 占位符,该占位符将被映射的包标识符替换。install 命令**应**支持占位符被多个标识符替换,query **必须**每个命令只接收一个标识符。

specifier_syntax dict[Literal['name_only', 'exact_version', 'version_ranges'], None | list[str] | dict[Literal['and', 'equal', 'greater_than', 'greater_than_equal', 'less_than', 'less_than_equal', 'not_equal', 'syntax'], None | str | list[str]] 允许的 PEP440 版本说明符到此包管理器中使用的语法的映射。需要三个顶级键:
  • name_only 必须采用字符串列表作为不包含任何版本信息的说明符的语法;它必须包含占位符 {name}
  • exact_version 必须为 None 或描述仅表达精确版本约束的说明符的语法字符串列表;在后一种情况下,占位符 {name}{version} 必须至少存在于其中一个字符串中(尽管不一定是同一个字符串)。
  • version_ranges 必须为 None 或包含以下必需键的字典
    • syntax 接受一个字符串列表,其中至少一个必须包含 {ranges} 占位符(由 and 的值决定,替换为可能已连接的版本约束)。它们也可以包含 {name} 占位符。
    • equalgreater_thangreater_than_equalless_thanless_than_equalnot_equal 在支持运算符时接受一个字符串,否则为 None。在前一种情况下,值必须包含 {version} 占位符,并且可以包含 {name}
    • {and} 接受一个字符串,用于将多个版本约束连接成一个令牌,如果每个令牌只能使用一个约束,则为 None。在后一种情况下,不同的约束将使用 syntax 模板“展开”成多个令牌。

    exact_versionversion_ranges 设置为 None 时,表示包管理器不支持相应类型的说明符。

示例

注册表、已知生态系统和映射

一个简化的注册表会是这样的

{
  "$schema": "https://raw.githubusercontent.com/jaimergp/external-metadata-mappings/main/schemas/central-registry.schema.json",
  "schema_version": 1,
  "definitions": [
    {
      "id": "dep:generic/zlib",
      "description": "A Massively Spiffy Yet Delicately Unobtrusive Compression Library"
    },
    {
      "id": "dep:generic/libwebp",
      "description": "WebP codec is a library to encode and decode images in WebP format. This package contains the library that can be used in other programs to add WebP support"
    },
    {
      "id": "dep:generic/clang",
      "description": "Language front-end and tooling infrastructure for languages in the C language family for the LLVM project."
    }
  ]
}

一个包含单个条目的最小已知生态系统列表将如下所示

{
  "$schema": "https://raw.githubusercontent.com/jaimergp/external-metadata-mappings/main/schemas/known-ecosystems.schema.json",
  "schema_version": 1,
  "ecosystems": {
    "conda-forge": {
      "mapping": "https://raw.githubusercontent.com/jaimergp/external-metadata-mappings/refs/heads/main/data/conda-forge.mapping.json"
    }
}

那个假想的 conda-forge 映射(conda-forge.mapping.json),为简洁起见只有几个条目,可能看起来像

{
  "schema_version": 1,
  "name": "conda-forge",
  "description": "Mapping for the conda-forge ecosystem",
  "mappings": [
    {
      "id": "dep:generic/zlib",
      "description": "zlib data compression library for the next generation systems. From zlib-ng/zlib-ng.",
      "specs": "zlib-ng",  // Simplest form
      "urls": {
        "feedstock": "https://github.com/conda-forge/zlib-ng-feedstock"
      }
    },
    {
      "id": "dep:generic/libwebp",
      "description": "WebP image library. libwebp-base ships libraries; libwebp ships the binaries.",
      "specs": {  // expanded form with single spec per category
        "build": "libwebp",
        "host": "libwebp-base",
        "run": "libwebp"
      },
      "urls": {
        "feedstock": "https://github.com/conda-forge/libwebp-feedstock"
      }
    },
    {
      "id": "dep:generic/clang",
      "description": "Development headers and libraries for Clang",
      "specs": { // expanded form with specs list
        "build": [
          "clang",
          "clangxx"
        ],
        "host": [
          "clangdev"
        ],
        "run": [
          "clang",
          "clangxx",
          "clang-format",
          "clang-tools"
        ]
      },
      "urls": {
        "feedstock": "https://github.com/conda-forge/clangdev-feedstock"
      }
    },
  ],
  "package_managers": [
    {
      "name": "conda",
      "commands": {
        "install": {
          "command": [
            "conda",
            "install",
            "{}"
          ],
          "multiple_specifiers": "always",
          "requires_elevation": false,
        },
        "query": {
          "command": [
            "conda",
            "list",
            "-f",
            "{}"
          ],
          "multiple_specifiers": "never",
          "requires_elevation": false,
        }
      },
      "specifier_syntax": {
        "exact_version": [
          "{name}=={version}"
        ],
        "name_only": [
          "{name}"
        ],
        "version_ranges": {
          "and": ",",
          "equal": "={version}",
          "greater_than": ">{version}",
          "greater_than_equal": ">={version}",
          "less_than": "<{version}",
          "less_than_equal": "<={version}",
          "not_equal": "!={version}",
          "syntax": [
            "{name}{ranges}"
          ]
        }
      }
    }
  ]
}

以下存储库提供了这些模式在实际情况中可能是什么样子的示例。它们并非旨在规定,而只是说明如何应用这些模式

pyproject-external CLI

以下示例说明了名称映射机制的用法。它们使用作为 pyproject-external 包的一部分实现的 CLI。

假设我们克隆了一个名为 my-cxx-pkg 的 Python 包的源代码,它只有一个用 C++ 实现的扩展模块,链接到 zlib,使用 pybind11,以及 meson-python 作为构建后端

[build-system]
build-backend = 'mesonpy'
requires = [
  "meson-python>=0.13.1",
  "pybind11>=2.10.4",
]

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

通过 Ubuntu 上 apt 的完整名称映射,这可能会显示以下内容

# show all external dependencies as DepURLs
$ python -m pyproject_external show .
[external]
build-requires = [
    "dep:virtual/compiler/cxx",
]
host-requires = [
    "dep:generic/zlib",
]

# show all external dependencies, but mapped to the autodetected ecosystem
$ python -m pyproject_external show --output=mapped .
[external]
build_requires = [
    "g++",
    "python3",
]
host_requires = [
    "zlib1g",
    "zlib1g-dev",
]

# show how to install external dependencies
$ python -m pyproject_external show --output=command .
sudo apt install --yes g++ zlib1g zlib1g-dev python3

我们尚未运行这些安装命令,因此外部依赖项可能缺失。如果发生构建失败,输出可能如下所示

$ pip install .
...
× Encountered error while generating package metadata.
╰─> See above for output.

note: This is an issue with the package mentioned above, not pip.

This package has the following external dependencies, if those are missing
on your system they are likely to be the cause of this build failure:

  dep:virtual/compiler/cxx
  dep:generic/zlib

如果 Pip 实现了查询名称映射注册表的支持,则该消息的结尾可以改进为

The following external dependencies are needed to install the package
mentioned above. You may need to install them with `apt`:

  g++
  zlib1g
  zlib1g-dev

如果用户希望使用 conda 包和 mamba 包管理器来安装外部依赖项,他们可以在其 ~/.config/pyproject-external/config.toml(或等效)文件中指定:

preferred_package_manager = "mamba"

这随后将改变 pyproject-external 的输出

$ python -m pyproject_external show --output command .
mamba install --yes --channel=conda-forge --channel-priority=strict cxx-compiler zlib python

pyproject-external CLI 还提供了一种简单的方法来针对中心注册表执行 [external] 表验证,以检查标识符是否被认为是规范的

$ python -m pyproject_external show --validate grpcio-1.71.0.tar.gz
WARNING  Dep URL 'dep:virtual/compiler/cpp' is not recognized in the
central registry. Did you mean any of ['dep:virtual/compiler/c',
'dep:virtual/compiler/cxx', 'dep:virtual/compiler/cuda',
'dep:virtual/compiler/go', 'dep:virtual/compiler/c-sharp']?
[external]
build-requires = [
    "dep:virtual/compiler/c",
    "dep:virtual/compiler/cpp",
]

pyproject-external API

pyproject-external Python API 也允许用户以编程方式执行这些操作

>>> from pyproject_external import External
>>> external = External.from_pyproject_data(
      {
        "external": {
          "build-requires": [
            "dep:virtual/compiler/c",
            "dep:virtual/compiler/cpp",
          ]
        }
      }
    )
>>> external.validate()
Dep URL 'dep:virtual/compiler/cpp' is not recognized in the central registry. Did you
mean any of ['dep:virtual/compiler/c', 'dep:virtual/compiler/cxx',
'dep:virtual/compiler/cuda', 'dep:virtual/compiler/go', 'dep:virtual/compiler/c-sharp']?
>>> external = External.from_pyproject_data(
      {
        "external": {
          "build-requires": [
            "dep:virtual/compiler/c",
            "dep:virtual/compiler/cxx",  # fixed
          ]
        }
      }
    )
>>> external.validate()
>>> external.to_dict()
{'external': {'build_requires': ['dep:virtual/compiler/c', 'dep:virtual/compiler/cxx']}}
>>> from pyproject_external import detect_ecosystem_and_package_manager
>>> ecosystem, package_manager = detect_ecosystem_and_package_manager()
>>> ecosystem
'conda-forge'
>>> package_manager
'pixi'
>>> external.to_dict(mapped_for=ecosystem, package_manager=package_manager)
{'external': {'build_requires': ['c-compiler', 'cxx-compiler', 'python']}}
>>> external.install_command(ecosystem, package_manager=package_manager)
# {"command": ["pixi", "add", "{}"]}
['pixi', 'add', 'c-compiler', 'cxx-compiler', 'python']
>>> external.query_commands(ecosystem, package_manager=package_manager)
# {"command": ["pixi", "list", "{}"]}
[
  ['pixi', 'list', 'c-compiler'],
  ['pixi', 'list', 'cxx-compiler'],
  ['pixi', 'list', 'python'],
]

Grayskull

一个原型概念验证实现已通过 conda/grayskull#518 贡献给 Grayskull,一个用于 Python 包的 conda recipe 生成器。

为了使用名称映射来生成我们包的 recipe,我们现在可以运行 Grayskull

$ grayskull pypi my-cxx-pkg
#### Initializing recipe for my-cxx-pkg (pypi) ####

Recovering metadata from pypi...
Starting the download of the sdist package my-cxx-pkg
my-cxx-pkg 100% Time:  0:00:10   5.3 MiB/s|###########|
Checking for pyproject.toml
...

Build requirements:
  - python                                 # [build_platform != target_platform]
  - cross-python_{{ target_platform }}     # [build_platform != target_platform]
  - meson-python >= 0.13.1                 # [build_platform != target_platform]
  - pybind11 >= 2.10.4                     # [build_platform != target_platform]
  - ninja                                  # [build_platform != target_platform]
  - libboost-devel                         # [build_platform != target_platform]
  - {{ compiler('cxx') }}
Host requirements:
  - python
  - meson-python >=0.13.1
  - pybind11 >=2.10.4
  - ninja
  - libboost-devel
Run requirements:
  - python

#### Recipe generated on /path/to/recipe/dir for my-cxx-pkg ####

向后兼容性

对向后兼容性没有影响。

安全隐患

本提案不会对现有项目造成任何安全隐患。提议的模式、注册表和映射是下游工具可以随意使用的可用资源,以他们认为合适的方式。

我们对未来的实施者有一些建议。映射模式提出了用于编码命令执行指令的字段(package_managers[].commands)。被篡改的映射可能会将这些指令更改为其他内容。因此,工具不应依赖互联网连接从在线源获取映射。相反

  • 他们应该在分发的包中包含相关文档,
  • 或依赖这些文档的预打包、离线分发,
  • 或实施获取文档真实性验证的最佳实践。

安装命令有可能修改用户的系统配置。如果可用,工具应优先为外部依赖项的安装创建临时的、隔离的环境。如果生态系统原生缺乏该功能,可以使用容器化等其他解决方案。至少,应提供操作影响的信息性消息。

如何教授此内容

至少有四个受众可能需要熟悉本 PEP 的内容

  1. 中心注册表维护者,负责管理知名 DepURL 和已映射生态系统的列表。
  2. 打包生态系统维护者,负责使其生态系统的映射保持最新。
  3. 需要外部依赖项的 Python 项目的维护者。
  4. 具有外部依赖元数据的包的最终用户。

中心 DepURL 注册表维护者

中心 DepURL 注册表维护者负责管理 DepURL 和已知生态系统的集合。这些贡献者需要能够参考明确定义的规则来确定何时可以定义新的 DepURL。对于规范 DepURL 定义,不宜过于宽松,因为每增加一个定义都会增加目标生态系统中映射的维护工作。

中心注册表维护者应就基本规则达成一致,并将其写入存储库文档中,或许辅以额外的便利设施,如问题和拉取请求模板,或 linting 工具。

包生态系统维护者的使用

缺少映射条目将导致受影响生态系统的最终用户无法获得定制的错误消息和其他用户体验功能。因此,建议每个包生态系统使其映射与中心注册表保持最新。实现这一点的关键将是自动化,例如 linting 脚本(参见 external-metadata-mappings 中的示例),或通过问题或草稿提交定期通知。

建立初始映射可能涉及大量工作,但理想情况下,持续的维护工作应需要较小的精力。

随着最佳实践的发现和达成一致,它们应在中心注册表存储库中作为映射维护者的学习材料进行记录。

Python 项目的维护者

包维护者的责任是决定最能代表其包所需的外部依赖项的 DepURL。这在 PEP 725 中有所介绍;位于 external-metadata-mappings.streamlit.app 的交互式映射浏览器演示可能会派上用场。中央注册表文档可能包含示例和常见问题解答,以指导新用户做出决定。

如果没有适合给定依赖项的 DepURL,维护者可以考虑在中心注册表中提交请求。如何执行此操作的说明应作为中心注册表文档的一部分提供。

最终用户包消费者

默认情况下,用户体验不会有任何变化。如果用户只依赖 wheel 包,这一点尤其正确,因为唯一的影响将由外部运行时依赖项驱动(预计很少见),即使在这些情况下,他们也需要通过安装兼容工具来选择加入。

选择加入的用户可能会发现其目标生态系统中缺少条目,他们应该获得指向相关文档部分的信息性错误消息。这将使他们熟悉问题的性质及其潜在解决方案。

我们希望这将导致其中一部分人报告缺失的条目,提交对受影响映射的修复,或者,如果完全没有,甚至决定自己维护一个新的映射。为此,他们应该熟悉映射维护者的职责(如上所述)。

参考实现

参考实现应包括三个组件

  1. 一个中央注册表,至少包含 DepURL 及其描述。此注册表**不得**包含包生态系统映射的具体细节。
  2. 一组映射的标准规范。JSON Schema 广泛用于许多文本编辑器中的模式,将是表达标准规范的自然选择。
  3. (2) 的实现,提供从中心注册表内容到特定于生态系统的包名称的映射。

对于 (1),JSON Schema 定义在 central-registry.schema.json。一个示例注册表可以在 registry.json 中找到。对于 (2),JSON Schema 定义在 external-mapping.schema.json。一个示例包的映射集合可以在 external-metadata-mappings 中找到。对于 (3),JSON Schema 定义在 known-ecosystems.schema.json。一个示例列表可以在 known-ecosystems.json 中找到。JSON Schema 使用 这些 Pydantic 模型 创建。

用于消费不同 JSON 文档和 [external] 表的参考 CLI 和 Python API 可以在 pyproject-external 中找到。

被拒绝的想法

由同一机构治理的集中映射

虽然注册表的中央授权机构很有用,但在 PyPI 的规模下处理多个生态系统的映射的维护负担是不可行的。因此,我们建议中央授权机构只管理中心注册表和已知生态系统列表,而映射本身的维护由目标生态系统处理。

允许特定于生态系统的包变体

某些生态系统有其自己的已知包变体;例如 Debian 的 libsymspg2-dev。虽然像 dep:debian/libsymspg2-dev 这样的标识符在语法上是有效的,但中心注册表不应将其识别为知名标识符,而应优先使用其 generic 对应项。用户仍然可以选择使用它,但工具可能会对此发出警告并建议使用通用标识符。这旨在鼓励尽可能与生态系统无关的元数据,以促进跨平台和操作系统的采用。

向中心注册表添加更多包元数据

中央注册表应只包含 DepURL 列表和最少的元数据字段,以方便其识别(自由格式文本描述和一个或多个相关位置的 URL)。

我们选择将其他详细信息排除在中心注册表之外,而是建议外部贡献者维护他们自己的映射,他们可以通过自由格式的 extra_metadata 字段用额外元数据注释标识符。

原因包括

  • 现有字段应足以识别项目主页,可以在那里获取额外的元数据(例如,URL 处的存储库可能包含有关作者身份和许可的详细信息)。
  • 这些细节也可以从实际目标生态系统中获得。在某些情况下,这甚至可能更可取;例如,对于许可,下游打包可以通过取消vendoring依赖项或调整可选部分来实际影响它。
  • 这些细节可能会在项目生命周期内发生变化,保持它们最新会增加治理机构的维护负担。
  • 因此,集中额外的元数据会引入目标生态系统之间的歧义和差异,因为可能存在或需要不同的版本。

将 PyPI 项目映射到目标生态系统中重新打包的对应项

其他生态系统使用自己的打包系统重新分发 Python 项目是很常见的。虽然对于带有编译扩展的包来说这是必需的,但对于纯 Python wheels 来说理论上是不必要的;唯一的需要似乎是元数据转换。请参阅 想要一个单一的打包工具/愿景 #68想要一个单一的打包工具/愿景 #103spack/spack#28282,以获取此方向讨论的示例。

本 PEP 中的提案不考虑 PyPI -> *生态系统*映射,但相同的模式可以为此目的重新利用。毕竟,从 PyPI 名称构建 PURL 或 DepURL 是微不足道的(例如 numpy 变为 pkg:pypi/numpy)。假设的映射维护者可以将其重新打包工作与源 PURL 标识符进行注释,然后使用该元数据生成兼容的映射,例如

{
  "$schema": "https://raw.githubusercontent.com/jaimergp/external-metadata-mappings/main/schemas/external-mapping.schema.json",
  "schema_version": 1,
  "name": "PyPI packages in Ubuntu 24.04",
  "description": "PyPI mapping for the Ubuntu 24.04 LTS (Noble) distro",
  "mappings": [
    {
      "id": "dep:pypi/numpy",
      "description": "The fundamental package for scientific computing with Python",
      "specs": ["python3-numpy"],
      "urls": {
        "home": "https://numpy.com.cn/"
      }
    }
  ]
}

这样的映射将允许下游重新分发工作专注于编译包,并直接将纯 wheel 包委托给 Python 打包解决方案。

严格的标识符验证

中心注册表提供了一系列规范标识符,这可能会诱使实现者确保所有提供的标识符确实是规范的。我们决定只**建议**某些工具类别采用这种做法,但在任何情况下都**不要求**进行此类检查。

预计随着打包社区采用 [external] 元数据表,**规范**标识符列表将不断增长,以适应不同项目中的要求。例如,引入新的 C++ 库或新的语言编译器。

如果验证过于严格并拒绝未知标识符,这将给外部元数据采用带来不必要的摩擦,并需要在时间紧迫的情况下人工审查和接受新请求的标识符,这可能会阻止需要将新标识符添加到中心注册表的包的发布。

我们建议只检查提供的标识符是否格式正确。未来的工作可能会选择在中心注册表成熟并被广泛采用后,强制要求标识符被识别为规范。

未解决的问题

目前没有。

参考资料

附录 A:操作建议

与生态系统映射相反,中心注册表和已知生态系统列表需要由中央机构维护。作者建议

  • PyPA GitHub 组织(或根据 PEP 772 的等效组织)下托管 external-metadata-mappingspyproject-external 存储库。
  • 为这两个存储库创建一个维护团队,由本 PEP 的作者组成,并根据 PEP 772 进行管理。

附录 B:虚拟版本提案

虽然虚拟依赖项可以使用与非虚拟依赖项相同的语法进行版本控制,但其含义可能模棱两可(例如,可能存在多个实现,并且虚拟接口可能无法明确版本化)。下面我们为中心注册表维护者提供了一些建议,以在标准化此类含义时考虑:

  • OpenMP:具有其标准的常规 MAJOR.MINOR 版本,因此看起来像 >=4.5
  • BLAS/LAPACK:应使用 Reference LAPACK 使用的版本控制,它定义了标准 API。使用 MAJOR.MINOR.MICRO,因此看起来像 >=3.10.0
  • 编译器:这些实现语言标准。对于 C、C++ 和 Fortran,它们的版本按年份划分。为了使版本正确排序,我们建议使用完整的年份(四位数)。因此,“至少 C99”将是 >=1999,选择 C++14 或 Fortran 77 将是 ==2014==1977。其他语言可能使用不同的版本控制方案。这些应在使用 pyproject.toml 之前在某处描述。

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

最后修改:2025-09-29 13:54:43 GMT