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

Python 增强提案

PEP 771 – Python 软件包的默认附加功能

作者:
Thomas Robitaille <thomas.robitaille at gmail.com>,Jonathan Dekhtiar <jonathan at dekhtiar.com>
发起人:
Pradyun Gedam <pradyunsg at gmail.com>
讨论至:
Discourse 帖子
状态:
草案
类型:
标准跟踪
主题:
打包
创建日期:
2025年1月13日
发布历史:
2025年1月15日2025年2月6日2025年6月9日

目录

摘要

PEP 508 指定了一种用于声明软件包依赖项的迷你语言。该语言的一个特性是能够指定**附加功能 (extras)**,它们是分发包的可选组件,使用时会安装额外的依赖项。本 PEP 提出了一种机制,允许在未明确提供任何附加功能时,默认安装一个或多个附加功能。

动机

本 PEP 中默认附加功能的各种用例和可能的解决方案在此 DPO 帖子中进行了广泛讨论。这些用例可分为两大类,为本 PEP 提供了动机。

支持多个后端或前端的软件包

使用附加功能的另一个常见用例是定义不同的后端或前端以及每个后端或前端需要安装的依赖项。软件包可能需要安装至少一个后端或前端才能正常运行,但可能对哪个后端或前端是灵活的。此类前端或后端的具体示例包括:

根据当前的打包标准,维护者要么必须要求其中一个后端或前端,要么要求用户始终指定附加功能,例如 package[backend],因此如果用户只安装 package,则存在无法使用的安装风险。如果有一种方法可以指定一个或多个默认后端或前端,并提供一种方法来覆盖这些默认值,这将为用户提供更好的体验,本 PEP 中描述的方法将允许这样做。

请注意,本 PEP 不旨在解决禁止冲突或不兼容的附加功能的问题,例如,如果一个软件包需要恰好一个前端或后端软件包。目前在 Python 打包基础设施中没有机制来禁止安装冲突或不兼容的附加功能,本 PEP 不会改变这一点。

需要至少一个后端或前端才能工作并推荐默认附加功能以安装后端或前端的软件包示例包括:

在所有这三种情况下,安装不带任何附加功能的软件包会导致安装失败,这对于其中一些软件包来说是一个常见的支持问题。

基本原理

社区多年来广泛讨论了许多可能的解决方案,包括在此 DPO 帖子以及许多问题和拉取请求中。下面提出的解决方案:

  • 是一种可选的解决方案,这意味着软件包维护者可以选择是否使用它
  • 足够灵活,可以适应动机中描述的两种主要用例
  • 重新使用 PEP 508 的语法

这是所有讨论过的解决方案中唯一一个满足所有三个标准的解决方案。

规范

Default-Extra 元数据字段

一个新的多用途元数据字段 Default-Extra 将添加到核心软件包元数据中。对于此字段,每个条目必须是一个字符串,指定一个附加功能,该附加功能将在未明确指定任何附加功能的情况下安装软件包时自动包含。

只有已在 Provides-Extra 条目中指定的条目才能在 Default-Extra 条目中使用。

示例

Default-Extra: recommended
Default-Extra: backend1
Default-Extra: backend2
Default-Extra: backend3

由于这在核心软件包元数据中引入了一个新字段,因此这将需要将Metadata-Version提升到下一个次要版本(撰写本文时为 2.5)。

[project] 元数据表中的新键

一个新的键将被添加到 PEP 621 中最初定义并在PyPA 规范中定义的 [project] 元数据表中。此键将命名为 default-optional-dependency-keys,描述如下:

  • TOML 类型:字符串数组
  • 相应的核心元数据字段:Default-Extra

default-optional-dependency-keys 中的每个字符串都必须是在可选依赖项中定义的附加功能的名称,并且此数组中的每个附加功能都将转换为核心软件包元数据中匹配的 Default-Extra 条目。将生成上一节中显示的示例 Default-Extra 条目的有效用法示例是:

[project]
default-optional-dependency-keys = [
    "recommended",
]

[project]
default-optional-dependency-keys = [
    "backend1",
    "backend2",
    "backend3"
]

覆盖默认附加功能

如果在依赖项规范中明确给出了附加功能,则忽略默认附加功能。否则,将安装默认附加功能。

例如,如果一个软件包定义了 extra1 默认附加功能以及一个非默认的 extra2 附加功能,那么如果用户使用以下命令安装软件包:

$ pip install package

将包含默认的 extra1 依赖项。如果用户改为使用以下命令安装软件包:

$ pip install package[extra2]

则将安装 extra2 附加功能,但将忽略默认的 extra1 附加功能。

如果在安装命令或依赖项树中多次指定了同一个软件包,则如果任何软件包实例未指定附加功能,则必须安装默认附加功能。例如,如果安装软件包 spam,其中 package 在依赖项树中多次出现:

spam
├── tomato
│   ├── package[extra2]
└── egg
    └── package

则应安装默认附加功能,因为 package 至少出现一次且未指定附加功能。

空附加功能集,例如 package[],应解释为表示应**不**安装任何默认附加功能(除非 package 出现在依赖项树的其他位置,在这种情况下,将**安装**默认附加功能,如上所述)。这将提供一种获取软件包最小安装的通用方法。

我们还注意到,某些工具(例如 pip)目前会忽略无法识别的附加功能,并向用户发出警告以指示附加功能未被识别,例如:

$ pip install package[non-existent-extra]
WARNING: package 3.0.0 does not provide the extra 'non-existent-extra'
...

对于表现如此(而不是引发错误)的工具,如果在依赖项规范中识别出附加功能无效,则应忽略它;如果所有指定的附加功能都无效,则应将其视为等同于 package[](而不是 package),并且**不**安装任何默认附加功能。

最后,我们注意到(正如依赖工具来取消选择任何默认附加功能中讨论的那样),软件包安装器可以实现自己的选项来控制上述行为,例如实现一个选项,禁用某些或所有软件包的默认附加功能,无论这些软件包在依赖项树中的位置如何。如果实现了此类工具特定的选项,工具开发人员应使其可选,并且用户应将上述 PEP 771 行为作为默认行为。

示例

本节我们将研究动机一节中描述的用例,以及如何通过使用上面概述的规范来解决这些用例。

需要至少一个后端或前端的软件包

动机中所述,某些软件包可能支持多个后端和/或前端,在某些情况下,可能需要确保始终至少安装一个后端或前端软件包,否则软件包将无法使用。具体示例可能包括需要 GUI 库才能使用的 GUI 应用程序,但能够支持不同的 GUI 库,或者依赖于不同计算后端但需要至少安装一个的软件包。

在这种情况下,软件包维护者可以选择为每个后端或前端定义一个附加功能,并提供一个默认值,例如:

[project]
default-optional-dependency-keys = [
    "backend1"
]

[project.optional-dependencies]
backend1 = [
    "package1",
    "package2"
]
backend2 = [
    "package3"
]

如果软件包可以同时支持例如多个后端,并且其中一些后端应该始终安装,那么这些后端的依赖项必须作为必需的依赖项提供,而不是使用默认附加功能机制。

动机中提到的一个具体示例为例,napari 软件包可以使用 PyQt5PyQt6PySide2PySide6 中的一个,用户目前需要明确指定 napari[all] 才能安装其中一个,或者例如 napari[pyqt5] 来明确指定其中一个前端软件包。安装不带任何附加功能的 napari 会导致软件包无法运行。有了这个 PEP,napari 可以定义以下配置:

[project]
default-optional-dependency-keys = [
    "pyqt5"
]

[project.optional-dependencies]
pyqt5 = [
    "PyQt5",
    "..."
]
pyside2 = [
    "PySide2",
    "..."
]
pyqt6 = [
    "PyQt6",
    "..."
]
pyside6 = [
    "PySide6",
    "..."
]

这意味着:

$ pip install napari

将开箱即用,但仍然会有一种机制供用户明确指定前端,例如:

$ pip install napari[pyside6]

支持不应删除默认附加功能的附加功能

我们在此考虑的另一个情况是,软件包维护者希望支持用户选择非默认附加功能,而不删除默认附加功能。本质上,他们可能希望:

  • package[] 提供不带任何附加功能的安装
  • package 安装推荐的依赖项(在 recommended 附加功能中)
  • package[alternative] 不安装默认附加功能,但安装一组备选的可选依赖项(在 alternative 附加功能中)
  • package[additional] 同时安装推荐和附加依赖项(在 additional 附加功能中)

这可以通过例如以下方式实现:

[project]
default-optional-dependency-keys = [
    "recommended"
]

[project.optional-dependencies]
recommended = [
    "package1",
    "package2"
]
alternative = [
    "package3"
]
additional = [
    "package[recommended]",
    "package4"
]

软件包在附加功能中引用自身的功能得到了现有 Python 打包工具的支持。

再次考虑一个具体示例,通过此 PEP,astropy 可以定义一个 recommended 附加功能(如推荐依赖项和最小安装中所述)。但是,它还定义了其他附加功能,例如 jupyter,它添加了增强 Jupyter 环境中用户体验的软件包。选择此附加功能的用户可能仍然希望安装推荐的依赖项。在这种情况下,以下配置将解决此问题:

[project]
default-optional-dependency-keys = [
    "recommended"
]

[project.optional-dependencies]
recommended = [
    "scipy",
    "..."
]
jupyter = [
    "astropy[recommended]",
    "ipywidgets",
    "..."
]

用户安装:

$ pip install astropy[jupyter]

然后将得到与以下内容相同的结果:

$ pip install astropy[recommended, jupyter]

具有多种默认值的软件包

在某些情况下,软件包可能需要多种默认设置。例如,在需要至少一个后端或前端的软件包中,我们考虑了只有后端或前端的软件包的情况,但在某些情况下,软件包可能必须同时支持后端和前端,并且希望指定一个或多个默认前端和一个或多个默认后端。

理想情况下,人们可能希望以下行为:

$ pip install package  # installs default backend and frontend
$ pip install package[]  # installs no backends or frontends
$ pip install package[backend1]  # installs backend1 and default frontend
$ pip install package[frontend2]  # installs frontend2 and default backend
$ pip install package[backend1, frontend2]  # installs backend1 and frontend2

然而,本 PEP 选择不提供一种机制,使其在指定 backend1 时,默认后端被禁用,但默认前端被启用,因为这会增加复杂性。

维护者目前应记录,如果明确指定了后端或前端,则需要同时指定后端和前端。对于想要这样做但却发现问题的用户来说,可发现性应该不是问题,因为用户在任何情况下都需要阅读文档才能找出可用的后端或前端,因此他们可以同时了解如何正确使用后端和前端的附加功能。

一种提高用户友好性的选择是,维护者可以创建名为 defaultbackenddefaultfrontend 的附加功能,它们确实安装了默认后端和前端。然后他们可以建议用户这样做:

$ pip install package  # installs default backend and frontend
$ pip install package[]  # installs no backends or frontends
$ pip install package[backend1, defaultfrontend]  # installs backend1 and default frontend
$ pip install package[defaultbackend, frontend2]  # installs frontend2 and default backend
$ pip install package[backend1, frontend2]  # installs backend1 and frontend2

这将允许(如果需要)用户获得任何推荐的后端,即使该默认值随时间变化。

如果将来有希望实施更好的解决方案,我们相信本 PEP 不应排除这一点。例如,将来可以想象添加一项功能,允许附加功能指定它禁用了**哪些**默认附加功能,如果未指定此功能,则明确指定的附加功能将禁用所有默认附加功能(与本 PEP 一致)。

向后兼容性

不使用默认附加功能的软件包

一旦打包生态系统中的工具添加了对本 PEP 的支持,不使用默认附加功能的软件包将继续按原样工作,并且不应出现兼容性问题。

使用默认附加功能的软件包

一旦软件包开始定义默认附加功能,这些默认值将仅在实现本 PEP 的最新版本打包工具中生效,但这些软件包仍可使用旧版打包工具进行安装——主要区别在于,在使用旧版打包工具时,默认附加功能不会自动安装。

如何教授中所述,软件包作者需要根据其用户群仔细评估何时以及如何采用默认附加功能,因为某些操作(例如将必需依赖项移动到默认附加功能)可能会导致使用旧版软件包安装程序(不支持默认附加功能)的用户出现故障。从这个意义上讲,软件包作者应注意,此功能如果以某些方式使用,可能会给用户带来向后兼容性问题,因此他们有责任确保将对用户的影响降至最低。

安全隐患

本 PEP 没有已知的安全隐患。

如何教授

本节概述了与本 PEP 实施相关的应提供给社区中不同群体的信息。下面描述的一些方面甚至在 PEP 完全在打包工具中实施之前就已相关,因为可以在此实施之前进行一些准备工作,以促进未来的任何潜在过渡。下面涵盖的群体有:

软件包最终用户

应向软件包用户提供清晰的安装说明,其中显示软件包可用的附加功能及其行为方式,例如解释默认情况下将安装哪些推荐依赖项或给定的前端或后端,以及如何根据可用情况选择退出或覆盖默认值。

软件包作者

虽然定义附加功能及其相关使用规则清晰明了,但软件包作者在将其功能应用于软件包之前需要仔细考虑几个方面,以避免无意中破坏向后兼容性。

支持旧版本的软件包安装器

诸如 pipuv 等软件包安装器不一定同时实现对默认附加功能的支持,一旦它们实现了,软件包作者很可能希望继续支持没有最新版本软件包安装器的用户。在这种情况下,以下建议适用:

  • 将软件包从必需依赖项转换为默认附加功能将是一个破坏性更改,因为旧版本的软件包安装器不会识别默认附加功能的概念,然后会安装依赖项较少的软件包,这可能会影响依赖这些依赖项的用户。因此,只有在将来开发人员只想支持使用实现此 PEP 的安装器的用户时,才应在已建立的软件包中将依赖项从必需依赖项更改为默认附加功能。
  • 将现有附加功能设为默认应该更安全,例如将 astropy 中的 recommended 设为默认附加功能,但为了支持使用旧版软件包安装器的用户,文档应尽可能明确提及该附加功能(直到明确大多数/所有用户都使用实现此 PEP 的软件包安装器)。保持明确提及附加功能没有坏处,但这将确保使用现代工具但未阅读文档的用户(这可能是用户社区中不可忽略的一部分)将默认获得推荐的依赖项。
  • 由于在此 PEP 之前,package[] 等同于 package,因此作者将能够将 package[] 记录为获取最小安装的向后兼容的通用方式。对于定义默认附加功能的软件包,即使使用旧版打包工具(如 pip),安装 package[] 也将始终提供最小安装,并且早于特定软件包引入默认附加功能的此软件包版本也可以通过 package[] 进行安装(尽管在这些情况下,这将等同于 package)。对于不定义默认附加功能的软件包,package[] 将继续等同于 package

避免添加许多默认依赖项

作者的一个诱惑可能是默认包含许多依赖项,因为他们可以提供一种选择退出的方法。然而,我们建议作者仔细考虑默认包含的内容,以避免不必要地膨胀安装并使依赖树复杂化。使用默认附加功能并不意味着所有附加功能都必须是默认的,用户仍然可以选择明确选择非默认附加功能。

默认附加功能通常应与必需依赖项具有相同的“权重”。当软件包被广泛使用时,引入默认附加功能将导致该附加功能的依赖项被传递包含——除非所有下游软件包都更新以使用最小安装规范明确选择退出。

例如,pytest 软件包目前有近 1,500 个插件依赖于它。如果 pytest 添加一个默认附加功能并且这些插件没有相应更新,则安装插件将包含默认附加功能的依赖项。这并不排除使用默认附加功能,但添加默认附加功能需要仔细评估其下游影响。

继承自默认附加功能

如果软件包作者选择默认安装某个附加功能,则重要的是他们要意识到,如果用户明确指定另一个附加功能,则除非他们使用支持不应删除默认附加功能的附加功能中描述的方法,否则默认附加功能可能不会被安装。

在某些情况下,例如可互换的后端,如果明确指定了附加功能,则忽略默认值是正确的做法。然而,对于其他情况,例如使用默认附加功能来包含推荐的依赖项,同时仍然提供最小安装的方法,许多其他附加功能可能**应该**明确“继承”默认附加功能,因此软件包作者应仔细考虑他们希望在哪些情况下安装默认附加功能。

不兼容的附加功能

在某些情况下,软件包可能具有相互不兼容的附加功能。在这种情况下,我们建议不要将默认附加功能用于任何包含可能与另一个附加功能不兼容的依赖项的附加功能。

考虑一个软件包,它有附加功能 package[A]package[B]。用户现在已经可以尝试安装 package[A]package[B]package[A,B],这会导致安装失败,但至少可以明确地知道两个附加功能都被安装了。然而,将 A 设为默认附加功能可能会导致不直观的问题。用户可以这样做:

$ pip install package  # this installs package[A]
$ pip install package[B]

最终导致安装失败,即使 A 和 B 从未明确同时安装过。因此,我们建议不要将默认附加功能用于可能出现此问题的依赖项。

循环依赖

当存在循环依赖时,作者需要特别小心。例如,考虑以下依赖树:

package1
└── package2
    └── package1

如果 package1 有一个名为 recommended 的默认额外功能,那么:

$ pip install package1[]

如果 package2 继续依赖 package1(未指定额外功能),则 recommended 额外功能仍将安装。这可以通过将依赖树更改为:

package1
└── package2
    └── package1[]

假设 package2 确实不依赖于 package1 的额外依赖项提供的任何功能。因此,作者需要仔细考虑迁移计划,并与 package2 的作者协调。

使用默认附加功能记录软件包

无论如何使用默认附加功能,软件包作者都应确保其软件包的文档清楚地说明如何使用附加功能。“最佳实践”文档应提及:

  • 安装 package 将等同于 package[<default extras>]
  • 安装 package[] 将只包含最小/必需的依赖项,但这不能保证如果 package 出现在依赖树的任何其他位置,可选依赖项就不会被安装
  • 其他哪些可选附加功能可用,以及它们是否禁用默认附加功能(因为这可以在支持不应删除默认附加功能的附加功能中描述的方式进行控制)
  • 任何特定于可能具有默认后端**和**前端的软件包的说明(如具有多种默认值的软件包中所述)

打包仓库维护者

需要考虑对为不同发行版(如 condaHomebrew、Linux 软件包安装器(如 aptyum)等)重新打包 Python 库的个人产生的影响。并非所有软件包发行版都有与所描述的方法一致的机制。事实上,某些发行版(如 conda)甚至没有附加功能的概念。

这里有两种情况需要考虑:

  • 在手动重新打包的情况下,例如一些 conda-forge 配方,特别是当没有等效的附加功能时,默认附加功能的引入不应产生很大影响,因为已经必须手动决定包含哪些依赖项(例如,动机中提到的 astropy 软件包的 conda-forge 配方默认包含所有 recommended 依赖项,因为用户无法以其他方式明确请求它们)。
  • 在以自动化方式进行重新打包的情况下,发行版维护者将需要仔细考虑如何处理默认附加功能,这可能意味着大量的工作和讨论。

像这样的 PEP 不可能详尽地考虑每种不同的软件包发行版。然而,最终,默认附加功能应被理解为软件包作者希望其软件包为大多数用户安装的方式,这应指导关于如何处理默认附加功能的决策,无论是手动还是自动。

参考实现

以下仓库包含一个功能齐全的演示软件包,该软件包使用默认附加功能:

https://github.com/wheel-next/pep_771

这使用了多个软件包的修改分支,以下链接指向这些分支:

此外,此分支包含 Flit 软件包的修改版本。

上述实现目前是概念验证,现有更改尚未经过相关维护者的审查。尽管如此,它们功能足以让感兴趣的维护者进行尝试。

被拒绝的想法

取消选择附加功能的语法

主要的竞争方法之一如下:如果明确提供了任何附加功能,则不取消选择默认值,而是需要明确取消选择默认附加功能。

在这种情况下,将引入一种新的取消选择附加功能的语法,作为 PEP 508 中定义的迷你语言的扩展。如果一个软件包定义了默认附加功能,用户可以通过在附加功能名称前使用减号 (-) 来选择退出这些默认值。提议的语法更新如下:

extras_list   = (-)?identifier (wsp* ',' wsp* (-)?identifier)*

此新语法的有效示例包括,例如:

  • package[-recommended]
  • package[-backend1, backend2]
  • package[pdf, -svg]

然而,这种方法存在两个主要问题:

  • 需要定义一些规则来解释边缘情况,例如附加功能及其否定版本同时出现在同一依赖项规范中(例如 package[pdf, -pdf])或者如果依赖项树同时包含 package[pdf]package[-pdf],而且这些规则对用户来说并不直观。
  • 更重要的是,这将向依赖项规范引入新语法,这意味着如果任何软件包使用新语法定义了依赖项,它以及任何其他依赖于它的软件包将不再能通过现有打包工具安装,因此这将是一个主要的向后兼容性中断。

由于这些原因,此替代方案未包含在最终提案中。

extras_require 中添加一个特殊条目

一种潜在的解决方案,作为引入新的 Default-Extra 元数据字段的替代方案,是利用一个具有“特殊”名称的附加功能。

一个例子是使用一个空字符串:

Provides-Extra:
Requires-Dist: numpy ; extra == ''

其思想是,作为“空”附加功能一部分安装的依赖项只有在未指定其他附加功能时才会被安装。在 https://github.com/pypa/setuptools/pull/1503 中提出了一个实现,但发现没有办法在不破坏现有用法兼容性的情况下使其工作。例如,通过 setup.py 文件使用 Setuptools 的软件包可以执行:

setup(
    ...
    extras_require={'': ['package_a']},
)

这是有效的,并且等同于在 install_requires 中定义 package_a,因此更改空字符串的含义将破坏兼容性。

此外,不能使用其他字符串(例如 'default')作为特殊字符串,因为所有将是向后兼容的有效附加功能名称的字符串可能已经在现有软件包中使用。

有人建议使用特殊的 Python 变量 None,但这再次是不可能的,因为尽管可以在 setup.py 文件中使用 None,但在声明性文件(例如 setup.cfgpyproject.toml)中则不行,而且最终附加功能名称必须转换为软件包元数据中的字符串。拥有:

Provides-Extra: None

将与字符串“None”无法区分,而“None”可能已经作为 Python 软件包中的附加功能名称使用。如果我们修改核心元数据语法以允许非字符串的“特殊”附加功能名称,那么我们又回到了修改核心元数据规范,这并不比引入 Default-Extra 更好。

依赖工具来取消选择任何默认附加功能

取消选择附加功能的另一种选择是在打包工具层面实现。例如,pip 可以包含一个选项,例如:

$ pip install package --no-default-extras

此选项可以应用于所有或特定软件包,类似于 --no-binary 选项,例如:

$ pip install package --no-default-extras :all:

这种方法的优点是支持默认附加功能的工具也可以支持取消选择它们。这种方法将类似于 apt 工具的 --no-install-recommends 选项。

然而,这种解决方案本身并不理想,因为它不允许软件包自身指定它们不需要依赖项的一些默认附加功能。它还会给用户带来风险,如果用户在大型依赖树中禁用所有默认附加功能,可能会破坏依赖于默认附加功能的树中的软件包。

尽管如此,本 PEP 并不禁止这种方法,并且由不同打包工具的维护者决定是否要支持此类选项。它是一个至少对软件包维护者有用的标志,他们希望识别依赖树中依赖默认附加功能的位置。然而,如果它受支持,则应明确指出使用此标志不能保证功能正常的环境。


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

最后修改:2025-06-09 11:29:19 GMT