PEP 735 – pyproject.toml 中的依赖组
- 作者:
- Stephen Rosen <sirosen0 at gmail.com>
- 发起人:
- Brett Cannon <brett at python.org>
- PEP 代理人:
- Paul Moore <p.f.moore at gmail.com>
- 讨论至:
- Discourse 帖子
- 状态:
- 最终版
- 类型:
- 标准跟踪
- 主题:
- 打包
- 创建日期:
- 2023年11月20日
- 发布历史:
- 2023年11月14日, 2023年11月20日
- 决议:
- 2024年10月10日
摘要
本 PEP 规定了一种在 pyproject.toml 文件中存储包需求的机制,使其不包含在项目的任何构建分发中。
这适用于创建命名的依赖项组,类似于 requirements.txt 文件,启动器、IDE 和其他工具可以通过名称找到并识别这些组。
此处定义的功能被称为“依赖组”。
动机
Python 社区有两个主要用例没有标准答案
- 如何定义包的开发依赖项?
- 对于不构建分发(非包项目)的项目,应如何定义依赖项?
为了满足这两个需求,有两种与本提案类似的常见解决方案
requirements.txt文件- 包 额外项
无论是 requirements.txt 文件还是 extras 都有本标准旨在克服的局限性。
请注意,上述两个用例描述了本 PEP 旨在支持的两种不同类型的项目
- Python 包,如库
- 非包项目,如数据科学项目
一些主要的用例在 用例附录中详细定义。
requirements.txt 文件的局限性
许多项目可能会定义一个或多个 requirements.txt 文件,并可能将其放置在项目根目录(例如 requirements.txt 和 test-requirements.txt)或目录中(例如 requirements/base.txt 和 requirements/test.txt)。但是,以这种方式使用需求文件存在主要问题
- 没有标准化的命名约定,工具无法通过名称发现或使用这些文件。
requirements.txt文件 不是标准化的,而是为pip提供选项。
因此,很难根据 requirements.txt 文件定义工具行为。它们不容易通过名称发现或识别,并且其内容可能包含包说明符和额外的 pip 选项的混合。
requirements.txt 内容缺乏标准也意味着它们无法移植到任何希望处理它们而不是 pip 的替代工具。
此外,requirements.txt 文件需要每个依赖列表一个文件。对于某些用例,这使得依赖分组的边际成本相对于其收益较高。对于拥有许多小型依赖组的项目来说,更简洁的声明是有益的。
与此相反,依赖组在 pyproject.toml 的已知位置定义,内容完全标准化。它们不仅将立即具有实用价值,而且还将成为未来标准的起点。
extras 的局限性
extras 是在 [project.optional-dependencies] 表中声明的额外包元数据。它们为包说明符列表提供名称,这些名称作为包元数据的一部分发布,用户可以根据该名称请求,例如 pip install 'foo[bar]' 以安装带有 bar 额外项的 foo。
由于 extras 是包元数据,因此它们不保证是静态定义的,可能需要构建系统来解决。此外,[project.optional-dependencies] 的定义向许多工具表明项目是一个包,并可能驱动工具行为,例如 [project] 表的验证。
对于包项目,extras 是定义开发依赖项的常见解决方案,但即使在这种情况下,它们也有缺点
- 由于
extra定义了可选的 额外 依赖项,因此在不安装当前包及其依赖项的情况下无法安装extra。 - 由于它们是用户可安装的,
extras是包公共接口的一部分。由于extras已发布,包开发者通常担心他们的开发额外项与面向用户的额外项混淆。
基本原理
本 PEP 定义了在 [dependency-groups] 表中以列表形式存储需求数据。此名称旨在与该功能的规范名称(“依赖组”)匹配。
此格式应尽可能简单易学,在许多情况下与现有 requirements.txt 文件具有非常相似的格式。[dependency-groups] 中的每个列表都定义为包说明符列表。例如
[dependency-groups]
test = ["pytest>7", "coverage"]
对于 requirements.txt 文件,有许多用例需要的数据无法用 PEP 508 依赖说明符表示。此类字段在依赖组中无效。包含 pip 支持的许多数据和字段,例如索引服务器、哈希和路径依赖项,需要新的标准。本标准为新标准和开发留有空间,但不试图支持所有有效的 requirements.txt 内容。
唯一的例外是 requirements.txt 文件用于将一个文件包含在另一个文件中的 -r 标志。依赖组支持类似的“包含”机制,允许一个依赖组扩展另一个依赖组。
依赖组有两个附加功能,与 requirements.txt 文件类似
- 它们不作为任何构建分发中的独立元数据发布
- 安装依赖组并不意味着安装包的依赖项或包本身
用例
以下用例被认为是本 PEP 的重要目标。它们在 用例附录中详细定义。
- 通过非 Python 打包构建过程部署的 Web 应用程序
- 具有未发布开发依赖组的库
- 具有依赖组但没有核心包的数据科学项目
- 锁定文件生成的 输入数据(依赖组通常不应作为锁定依赖数据的位置)
- 环境管理器(如 tox、Nox 或 Hatch)的输入数据
- 测试和代码检查器要求的 IDE 可配置发现
关于 Poetry 和 PDM 依赖组
现有的 Poetry 和 PDM 工具已经提供了一个它们都称为“依赖组”的功能。然而,由于缺乏用于指定依赖项集合的任何标准,每个工具都以工具特定的方式在 [tool] 表的相关部分定义这些功能。
(PDM 也将额外项用于某些依赖组,并与额外项的概念有很大重叠。)
本 PEP 不支持 Poetry 和 PDM 的所有功能,这些工具与 pip 的 requirements.txt 文件一样,支持对常见依赖说明符的几种非标准扩展。
此类工具应能够将标准化的依赖组用作其自己的依赖组机制的扩展。但是,定义一个新的数据格式来取代现有的 Poetry 和 PDM 解决方案并非目标。这样做将需要标准化其他几个功能,例如这些工具支持的路径依赖项。
未来兼容性与无效数据
依赖组旨在在未来的 PEP 中进行扩展。但是,依赖组也应该能够在单个 Python 项目中被多个工具使用。当多个工具使用相同的数据时,一个工具可能会实现扩展依赖组的未来 PEP,而另一个工具则不会。
为了在这种情况下支持用户,本 PEP 定义并推荐了验证行为,其中工具只检查它们正在使用的依赖组。这允许使用不同版本依赖组数据的多个工具在 pyproject.toml 中共享一个表。
规范
本 PEP 在 pyproject.toml 文件中定义了一个名为 dependency-groups 的新部分(表)。dependency-groups 表包含任意数量的用户定义键,每个键的值都是一个需求列表(如下定义)。这些键必须是有效的非规范化名称,并且在比较之前必须规范化。
工具应优先向用户显示原始的、非规范化名称。如果在规范化后遇到重复名称,工具应发出错误。
dependency-groups 下的需求列表可能包含字符串、表(Python 中的“dicts”)或字符串和表的混合。
需求列表中的字符串必须是有效的依赖说明符,如 PEP 508 中所定义。
需求列表中的表必须是有效的依赖对象说明符。
依赖对象说明符
依赖对象说明符是定义零个或多个依赖项的表。
本 PEP 仅标准化一种类型的依赖对象说明符,即“依赖组包含”。其他类型可以在未来的标准中添加。
依赖组包含
依赖组包含将另一个依赖组的依赖项包含到当前依赖组中。
包含定义为一个表,其中只有一个键 "include-group",其值是一个字符串,即另一个依赖组的名称。
例如,{include-group = "test"} 是一个包含,它扩展到 test 依赖组的内容。
包含被定义为与命名依赖组的内容完全等价,并插入到当前组中包含的位置。例如,如果 foo = ["a", "b"] 是一个组,而 bar = ["c", {include-group = "foo"}, "d"] 是另一个组,那么当依赖组包含展开时,bar 应该评估为 ["c", "a", "b", "d"]。
依赖组包含可以多次指定同一个包。工具不应去重或以其他方式更改包含产生的列表内容。例如,给定以下表格
[dependency-groups]
group-a = ["foo"]
group-b = ["foo>1.0"]
group-c = ["foo<1.0"]
all = ["foo", {include-group = "group-a"}, {include-group = "group-b"}, {include-group = "group-c"}]
all 的解析值应该是 ["foo", "foo", "foo>1.0", "foo<1.0"]。工具应像处理任何其他需要多次处理具有不同版本约束的同一需求的情况一样处理此类列表。
依赖组包含可以包含包含依赖组包含的列表,在这种情况下,这些包含也应展开。依赖组包含不得包含循环,如果工具检测到循环,则应报告错误。
依赖组表示例
以下是 pyproject.toml 的部分示例,它使用此方法定义了四个依赖组:test、docs、typing 和 typing-test
[dependency-groups]
test = ["pytest", "coverage"]
docs = ["sphinx", "sphinx-rtd-theme"]
typing = ["mypy", "types-requests"]
typing-test = [{include-group = "typing"}, {include-group = "test"}, "useful-types"]
请注意,这些依赖组声明都不会隐式安装当前包、其依赖项或任何可选依赖项。使用像 test 这样的依赖组来测试包,需要用户配置或工具链也安装当前包(.)。例如,
$TOOL install-dependency-group test
pip install -e .
可以使用(假设 $TOOL 是支持安装依赖组的工具)来构建测试环境。
这也允许在不将项目作为包安装的情况下使用 docs 依赖组
$TOOL install-dependency-group docs
包构建
构建后端不得将依赖组数据作为包元数据包含在构建分发中。这意味着 sdists 中的 PKG-INFO 和 wheels 中的 METADATA 不包含任何可引用的包含依赖组的字段。
在动态元数据评估中使用依赖组是有效的,sdists 中包含的 pyproject.toml 文件自然仍将包含 [dependency-groups] 表。但是,表内容不属于已发布包的接口。
安装依赖组
支持依赖组的工具预计将提供新的选项和接口,以允许用户从依赖组安装。
未定义用于表达包依赖组的语法,原因有二
- 引用 PyPI 的第三方包的依赖组是无效的(因为数据被定义为未发布)
- 不保证依赖组有当前包——其目的之一是支持非包项目
例如,pip 安装依赖组的可能接口是
pip install --dependency-groups=test,typing
请注意,这只是一个示例。本 PEP 不声明工具如何支持依赖组安装的任何要求。
与 Extras 重叠的安装用户体验
工具可以选择为安装依赖组提供与安装额外项相同的接口。
请注意,本规范不禁止存在与依赖组名称匹配的额外项。
建议用户避免创建与额外项名称匹配的依赖组。工具可以将此类匹配视为错误。
验证和兼容性
支持依赖组的工具可能希望在使用数据之前验证数据。但是,实施此类验证行为的工具应注意允许此规范的未来扩展,以免在存在新语法时不必要地发出错误或警告。
工具在评估或处理依赖组中无法识别的数据时应报错。
工具不应急于验证 所有 依赖组的列表内容。
这意味着在存在以下数据的情况下,大多数工具将允许使用 foo 组,并且只有在使用 bar 组时才会报错
[dependency-groups]
foo = ["pyparsing"]
bar = [{set-phasers-to = "stun"}]
代码检查器和验证器可能更严格
不鼓励主要安装或解析依赖组的工具进行急切验证。代码检查器和验证工具可能有充分理由忽略此建议。
参考实现
以下参考实现将依赖组的内容打印到标准输出,以换行符分隔。因此,输出是有效的 requirements.txt 数据。
import re
import sys
import tomllib
from collections import defaultdict
from packaging.requirements import Requirement
def _normalize_name(name: str) -> str:
return re.sub(r"[-_.]+", "-", name).lower()
def _normalize_group_names(dependency_groups: dict) -> dict:
original_names = defaultdict(list)
normalized_groups = {}
for group_name, value in dependency_groups.items():
normed_group_name = _normalize_name(group_name)
original_names[normed_group_name].append(group_name)
normalized_groups[normed_group_name] = value
errors = []
for normed_name, names in original_names.items():
if len(names) > 1:
errors.append(f"{normed_name} ({', '.join(names)})")
if errors:
raise ValueError(f"Duplicate dependency group names: {', '.join(errors)}")
return normalized_groups
def _resolve_dependency_group(
dependency_groups: dict, group: str, past_groups: tuple[str, ...] = ()
) -> list[str]:
if group in past_groups:
raise ValueError(f"Cyclic dependency group include: {group} -> {past_groups}")
if group not in dependency_groups:
raise LookupError(f"Dependency group '{group}' not found")
raw_group = dependency_groups[group]
if not isinstance(raw_group, list):
raise ValueError(f"Dependency group '{group}' is not a list")
realized_group = []
for item in raw_group:
if isinstance(item, str):
# packaging.requirements.Requirement parsing ensures that this is a valid
# PEP 508 Dependency Specifier
# raises InvalidRequirement on failure
Requirement(item)
realized_group.append(item)
elif isinstance(item, dict):
if tuple(item.keys()) != ("include-group",):
raise ValueError(f"Invalid dependency group item: {item}")
include_group = _normalize_name(next(iter(item.values())))
realized_group.extend(
_resolve_dependency_group(
dependency_groups, include_group, past_groups + (group,)
)
)
else:
raise ValueError(f"Invalid dependency group item: {item}")
return realized_group
def resolve(dependency_groups: dict, group: str) -> list[str]:
if not isinstance(dependency_groups, dict):
raise TypeError("Dependency Groups table is not a dict")
if not isinstance(group, str):
raise TypeError("Dependency group name is not a str")
return _resolve_dependency_group(dependency_groups, group)
if __name__ == "__main__":
with open("pyproject.toml", "rb") as fp:
pyproject = tomllib.load(fp)
dependency_groups_raw = pyproject["dependency-groups"]
dependency_groups = _normalize_group_names(dependency_groups_raw)
print("\n".join(resolve(pyproject["dependency-groups"], sys.argv[1])))
向后兼容性
撰写本文时,pyproject.toml 文件中的 dependency-groups 命名空间尚未使用。由于顶层命名空间仅供 packaging.python.org 指定的标准使用,因此不存在直接向后兼容性问题。
但是,该功能的引入对许多生态系统工具产生了影响,特别是那些试图支持检查 setup.py 和 requirements.txt 中数据的工具。
审计和更新工具
各种工具都理解以 requirements.txt 文件表示的 Python 依赖数据。(例如,Dependabot、Tidelift 等)
此类工具检查依赖数据,并在某些情况下提供工具辅助或完全自动化的更新。我们预计起初没有此类工具会支持新的依赖组,而广泛的生态系统支持可能需要数月甚至数年才能到来。
因此,当用户开始使用依赖组时,他们将体验到工作流程和工具支持的退化。对于任何关于依赖数据编码位置和方式的新标准来说,情况都是如此。
安全隐患
本 PEP 引入了用于在项目中指定依赖信息的新语法和数据格式。但是,它没有引入处理或解析依赖项的新指定机制。
因此,它除了安装依赖项可能已使用的任何工具固有的安全问题外,不带来其他安全问题——即,此处可以指定恶意依赖项,就像它们可以在 requirements.txt 文件中指定一样。
如何教授此内容
此功能应以其规范名称“依赖组”来指代。
应将基本用法形式作为典型 requirements.txt 数据的变体来教授。可以将标准依赖说明符(PEP 508)添加到命名列表中。与其要求 pip 从 requirements.txt 文件安装,不如由 pip 或相关工作流工具从命名的依赖组安装。
对于新的 Python 用户,可以直接教他们如何在 pyproject.toml 中创建包含其依赖组的部分,类似于当前教他们使用 requirements.txt 文件的方式。这还允许新的 Python 用户了解 pyproject.toml 文件,而无需学习包构建。pyproject.toml 文件只包含 [dependency-groups] 而不包含其他表是有效的。
对于新用户和经验丰富的用户,都需要解释依赖组包含。对于有使用 requirements.txt 经验的用户,可以将其描述为 -r 的模拟。对于新用户,应教导包含允许一个依赖组扩展另一个依赖组。可以使用类似的配置接口和 Python 的 list.extend 方法通过类比来解释这个概念。
使用过 setup.py 打包的 Python 用户可能熟悉 pyproject.toml 之前常见的实践,其中包元数据是动态定义的。requirements.txt 文件中加载的需求和 setup() 调用之前的静态列表定义很容易与依赖组进行类比。
依赖组的使用接口
本规范不提供与依赖组交互的通用接口,除了通过 project 表将其包含在构建包中。这对工具作者和用户都有影响。
工具作者应确定依赖组如何或是否与他们的用户故事相关,并构建自己的接口以适应。对于环境管理器、解析器、安装程序和相关的非构建工具,他们将能够记录他们支持“PEP 735 依赖组”,但他们将负责记录其使用模式。对于构建后端,支持依赖组将需要支持从 project 表中包含,但没有其他严格要求。
对于用户而言,主要后果是,每当他们希望在包构建之外使用依赖组时,都必须查阅相关工具文档。工具应通过文档或运行时警告或错误,向用户提供关于不推荐或不支持的用法的建议。例如,如果某个工具希望要求所有依赖组相互兼容,不包含相互矛盾的包说明符,则应记录该限制并指导用户如何适当地利用依赖组以达到其目的。
被拒绝的想法
为什么不将每个依赖组定义为一个表?
如果我们的目标是允许未来扩展,那么将每个依赖组定义为子表,从而使我们能够将未来的键附加到每个组,将提供最大的未来灵活性。
然而,它也使结构嵌套更深,因此更难教授和学习。本 PEP 的目标之一是成为许多 requirements.txt 用例的简单替代品。
为什么不允许使用特殊的字符串语法来扩展依赖说明符?
本规范的早期草案定义了依赖组包含和路径依赖项的语法形式。
然而,这种方法存在三个主要问题
- 它使必须教授的字符串语法复杂化,超出了 PEP 508
- 生成的字符串总是需要与 PEP 508 说明符区分开来,这使得实现复杂化
为什么不允许更多的非 PEP 508 依赖说明符?
在讨论中浮现出几个用例,它们需要比 PEP 508 可能提供的更具表现力的说明符。
“路径依赖项”,指代本地路径,以及对 [project.dependencies] 的引用尤其受关注。
然而,对于这些功能,目前没有现有的标准(除了 pip 实现细节的事实标准)。
因此,试图将这些功能包含在本 PEP 中会导致范围显著扩大,需要尝试标准化这些各种功能和 pip 行为。
特别注意尝试标准化可编辑安装的表达,如 pip install -e 和 PEP 660 所表达的。然而,尽管可编辑安装的创建已针对构建后端标准化,但可编辑的行为并未针对安装程序标准化。在本 PEP 中包含可编辑安装要求任何支持工具都允许安装可编辑安装。
因此,尽管 Poetry 和 PDM 为其中一些功能提供了语法,但目前认为它们尚未充分标准化,无法包含在依赖组中。
为什么该表不命名为 [run]、[project.dependency-groups] 等?
这个概念有很多可能的名称。它将与已有的 [project.dependencies] 和 [project.optional-dependencies] 表并存,并可能还有一个新的 [external] 依赖表(撰写本文时,定义 [external] 表的 PEP 725 正在进行中)。
[run] 是早期讨论中的主要提案,但其建议用法围绕一组运行时依赖项。本 PEP 明确列出了多个依赖组,这使得 [run] 不太适合——这不仅仅是特定运行时上下文的依赖数据,而是多个上下文的依赖数据。
[project.dependency-groups] 将与 [project.dependencies] 和 [project.optional-dependencies] 很好地并行,但对于非包项目存在主要缺点。[project] 要求定义几个键,例如 name 和 version。使用此名称将需要重新定义 [project] 表以允许这些键不存在,或者对非包项目施加要求来定义和使用这些键。因此,它将有效地要求任何非包项目允许自己被视为包。
为什么 pip 计划实现的 --only-deps 不够用?
pip 目前的路线图上有一个功能,用于添加 –only-deps 标志。此标志旨在允许用户安装包依赖项和额外项,而无需安装当前包。
它不解决非包项目的需求,也不允许在没有包依赖项的情况下安装额外项。
为什么 不是解决方案?
现有的环境管理器,如 tox、Nox 和 Hatch,已经能够在其配置数据中列出内联依赖项。这满足了许多开发依赖需求,并明确地将依赖组与可以运行的相关任务关联起来。这些机制是 好的,但它们 不足够。
首先,它们没有解决非包项目的需求。
其次,没有其他工具可用于访问这些数据的标准。这对 IDE 和 Dependabot 等高级工具产生了影响,这些工具无法与这些依赖组进行深度集成。(例如,撰写本文时,Dependabot 不会标记在 tox.ini 文件中固定版本的依赖项。)
延迟的想法
为什么不支持在 [project.dependencies] 或 [project.optional-dependencies] 中包含依赖组?
本规范的早期草案允许在 [project] 表中使用依赖组包含。然而,在社区反馈期间提出了几个问题,导致其被删除。
通过包含依赖组,只有少数额外的用例会得到解决,并且这显著增加了规范的范围。特别是,此包含将增加受此添加影响的各方数量。许多 [project] 表的读者,包括构建后端、SBOM 生成器和依赖分析器,都受到 [project] 更改的影响,但在存在新的(但无关的)[dependency-groups] 表时,它们可以继续照常运行。
除了上述担忧之外,允许从 [project] 表中包含依赖组会鼓励包维护者将依赖元数据移出当前标准位置。这使得静态 pyproject.toml 元数据复杂化,并与 PEP 621 将依赖元数据存储在一个位置的目标相冲突。
最后,本 PEP 排除 [project] 支持并非最终决定。从该表中使用包含,或从 [dependency-groups] 到 [project] 的包含语法,可以通过未来的 PEP 引入,并根据其自身优点进行考虑。
从 [project] 中包含依赖组的用例
尽管在本 PEP 中推迟了,但允许从 [project] 表中包含将解决几个用例。
特别是,在某些情况下,包开发者希望只安装包的依赖项,而不安装包本身。
例如
- 在构建依赖项与构建包本身时指定不同的环境变量或选项
- 创建分层容器镜像,其中依赖项与要安装的包隔离
- 向分析环境(例如类型检查)提供依赖项,而无需构建和安装包本身
例如,考虑以下示例 pyproject.toml
[project]
dependencies = [{include = "runtime"}]
[optional-dependencies]
foo = [{include = "foo"}]
[dependency-groups]
runtime = ["a", "b"]
foo = ["c", "d"]
typing = ["mypy", {include = "runtime"}, {include = "foo"}]
在这种情况下,可以定义一个 typing 组,其中包含包的所有运行时依赖项,但不包含包本身。这使得 typing 依赖组的用户可以跳过包的安装——这不仅更高效,而且可以减少对测试系统的要求。
为什么不支持在 [build-system.requires] 中包含依赖组?
鉴于我们将不允许 [project] 使用依赖组,可以将 [build-system.requires] 与 [project.dependencies] 进行比较。
在组中指定构建要求的理论用法少于包要求。此外,这种更改的影响涉及 PEP 517 前端,该前端将需要支持依赖组才能准备构建环境。
与 [project.dependencies] 和 [project.optional-dependencies] 的更改相比,更改 [build-system.requires] 的影响更大,潜在用途更少。因此,鉴于本 PEP 拒绝更改 [project] 表,更改 [build-system] 也被推迟。
为什么不支持包含当前项目的依赖组?
依赖组的几个使用场景围绕着将依赖组与 [project] 表中定义的包一起安装。例如,测试一个包涉及安装测试依赖项和包本身。此外,依赖组与主包的兼容性是锁定文件生成器的一个有价值的输入。
在这种情况下,依赖组声明它依赖于项目本身是可取的。讨论中的示例语法包括 {include-project = true} 和 {include-group = ":project:"}。
然而,如果建立了一个规范来扩展 PEP 508 与路径依赖项,这将导致依赖组有两种指定主包的方式。例如,如果 . 得到正式支持,并且 {include-project = true} 包含在本 PEP 中,那么依赖组可以指定以下任何组
[dependency-groups]
case1 = [{include-project = true}]
case2 = ["."]
case3 = [{include-project = true}, "."]
case4 = [{include-project = false}, "."]
为了避免未来出现多个不同选项指定 pyproject.toml 中定义的包的混乱局面,本 PEP 中省略了任何声明此关系的语法。
附录 A:非 Python 语言中的现有技术
本节主要提供信息,用于记录其他语言生态系统如何解决类似问题。
JavaScript 和 package.json
在 JavaScript 社区中,包包含一个规范的配置和数据文件,其范围类似于 pyproject.toml,位于 package.json。
package.json 中的两个键控制依赖数据:"dependencies" 和 "devDependencies"。"dependencies" 的作用实际上与 pyproject.toml 中的 [project.dependencies] 相同,声明包的直接依赖项。
"dependencies" 数据
依赖数据在 package.json 中声明为从包名称到版本说明符的映射。
版本说明符支持一种小型的可能版本、范围和其他值的语法,类似于 Python 的 PEP 440 版本说明符。
例如,这是一个声明了一些依赖项的部分 package.json 文件
{
"dependencies": {
"@angular/compiler": "^17.0.2",
"camelcase": "8.0.0",
"diff": ">=5.1.0 <6.0.0"
}
}
@ 符号的使用是一个范围,它声明了包的所有者,用于组织拥有的包。"@angular/compiler" 因此声明了一个名为 compiler 的包,分组在 angular 拥有权下。
引用 URL 和本地路径的依赖项
依赖说明符支持 URL 和 Git 仓库的语法,类似于 Python 打包中的规定。
URL 可以代替版本号使用。使用时,它们隐式引用包源代码的 tarball。
Git 仓库也可以类似地使用,包括支持提交说明符。
与 PEP 440 不同,NPM 允许使用本地路径作为依赖项的包源代码目录。当这些数据通过标准 npm install --save 命令添加到 package.json 时,路径会被规范化为相对于包含 package.json 的目录的相对路径,并以 file: 为前缀。例如,以下部分 package.json 包含对当前目录同级目录的引用
{
"dependencies": {
"my-package": "file:../foo"
}
}
NPM 官方文档指出,本地路径依赖项“不应”发布到公共包仓库,但未说明此类依赖数据在已发布包中的固有有效性或无效性。
"devDependencies" 数据
package.json 允许包含第二个名为 "devDependencies" 的部分,格式与 "dependencies" 相同。"devDependencies" 中声明的依赖项在从包仓库安装包时(例如作为依赖项解析的一部分)默认不会安装,但在包含 package.json 的源代码树中运行 npm install 时会安装。
正如 "dependencies" 支持 URL 和本地路径一样,"devDependencies" 也支持。
"peerDependencies" 和 "optionalDependencies"
package.json 中还有另外两个相关的部分。
"peerDependencies" 声明了一个与 "dependencies" 格式相同的依赖项列表,但其含义是这些是兼容性声明。例如,以下数据声明与包 foo 版本 2 的兼容性
{
"peerDependencies": {
"foo": "2.x"
}
}
"optionalDependencies" 声明了一个依赖项列表,如果可能应安装,但如果不可用则不应视为失败。它也使用与 "dependencies" 相同的映射格式。
"peerDependenciesMeta"
"peerDependenciesMeta" 是一个允许额外控制 "peerDependencies" 处理方式的部分。
通过在此部分将包设置为 optional,可以禁用有关缺失依赖项的警告,如下例所示
{
"peerDependencies": {
"foo": "2.x"
},
"peerDependenciesMeta": {
"foo": {
"optional": true
}
}
}
--omit 和 --include
npm install 命令支持两个选项,--omit 和 --include,它们可以控制是否安装“prod”、“dev”、“optional”或“peer”依赖项。
“prod”名称指 "dependencies" 中列出的依赖项。
默认情况下,当 npm install 对源代码树执行时,所有四个组都会安装,但这些选项可用于更精确地控制安装行为。此外,这些值可以在 .npmrc 文件中声明,允许每个用户和每个项目的配置来控制安装行为。
Ruby 和 Ruby Gems
Ruby 项目可能旨在或不旨在在 Ruby 生态系统中生成包(“gem”)。实际上,大多数语言用户预计不希望生成 gem,并且对生成自己的包不感兴趣。许多教程不涉及如何生成包,并且工具链从不需要为支持的用例打包用户代码。
Ruby 将需求规范分成两个单独的文件。
Gemfile:一个专用文件,只支持依赖组形式的需求数据<package>.gemspec:一个用于声明包(gem)元数据的专用文件
bundler 工具,提供 bundle 命令,是使用 Gemfile 数据的主要接口。
gem 工具负责通过 gem build 命令从 .gemspec 数据构建 gem。
Gemfile 和 bundle
Gemfile 是一个 Ruby 文件,包含任意数量 group 声明中包含的 gem 指令。gem 指令也可以在 group 声明之外使用,在这种情况下,它们形成一个隐式无名依赖组。
例如,以下 Gemfile 列出了 rails 作为项目依赖项。所有其他依赖项都列在组下
source 'https://rubygems.org.cn'
gem 'rails'
group :test do
gem 'rspec'
end
group :lint do
gem 'rubocop'
end
group :docs do
gem 'kramdown'
gem 'nokogiri'
end
如果用户执行 bundle install 带有这些数据,则所有组都会安装。用户可以通过在 .bundle/config 中创建或修改 bundler 配置来取消选择组,无论是手动还是通过 CLI。例如,bundle config set --local without 'lint:docs'。
使用上述数据无法排除顶级 'rails' gem 的使用,也无法通过名称引用该隐式分组。
gemspec 和打包的依赖数据
gemspec 文件是一个 Ruby 文件,包含 Gem::Specification 实例声明。
Gem::Specification 中只有两个字段与包依赖数据有关。它们是 add_development_dependency 和 add_runtime_dependency。一个 Gem::Specification 对象还提供了动态添加依赖项的方法,包括 add_dependency(添加运行时依赖项)。
这是 rails.gemspec 文件的变体,许多字段已删除或缩短以简化
version = '7.1.2'
Gem::Specification.new do |s|
s.platform = Gem::Platform::RUBY
s.name = "rails"
s.version = version
s.summary = "Full-stack web application framework."
s.license = "MIT"
s.author = "David Heinemeier Hansson"
s.files = ["README.md", "MIT-LICENSE"]
# shortened from the real 'rails' project
s.add_dependency "activesupport", version
s.add_dependency "activerecord", version
s.add_dependency "actionmailer", version
s.add_dependency "activestorage", version
s.add_dependency "railties", version
end
请注意,没有使用 add_development_dependency。其他一些主流、主要包(例如 rubocop)在其 gem 中不使用开发依赖项。
其他项目 确实 使用此功能。例如,kramdown 使用开发依赖项,在其 Rakefile 中包含以下规范
s.add_dependency "rexml"
s.add_development_dependency 'minitest', '~> 5.0'
s.add_development_dependency 'rouge', '~> 3.0', '>= 3.26.0'
s.add_development_dependency 'stringex', '~> 1.5.1'
开发依赖项的目的只是声明一个隐式组,作为 .gemspec 的一部分,然后可以由 bundler 使用。
有关详细信息,请参阅 bundler 的 Gemfiles 文档中的 gemspec 指令。然而,.gemspec 开发依赖项与 Gemfile/bundle 用法之间的集成最好通过示例来理解。
gemspec 开发依赖示例
考虑以下以 Gemfile 和 .gemspec 形式的简单项目。cool-gem.gemspec 文件
Gem::Specification.new do |s|
s.author = 'Stephen Rosen'
s.name = 'cool-gem'
s.version = '0.0.1'
s.summary = 'A very cool gem that does cool stuff'
s.license = 'MIT'
s.files = []
s.add_dependency 'rails'
s.add_development_dependency 'kramdown'
end
以及 Gemfile
source 'https://rubygems.org.cn'
gemspec
Gemfile 中的 gemspec 指令声明了对本地包 cool-gem 的依赖,该包在本地可用的 cool-gem.gemspec 文件中定义。它 还 隐式地将所有开发依赖项添加到一个名为 development 的依赖组中。
因此,在这种情况下,gemspec 指令等同于以下 Gemfile 内容
gem 'cool-gem', :path => '.'
group :development do
gem 'kramdown'
end
附录 B:Python 中的现有技术
在没有先前的依赖组标准的情况下,两个已知的工作流工具 PDM 和 Poetry 定义了它们自己的解决方案。
本节将主要关注这两个工具,作为 Python 中依赖组定义和使用的现有技术案例。
项目即包
PDM 和 Poetry 都将其支持的项目视为包。这使得它们能够使用和与标准 pyproject.toml 元数据交互以满足其某些需求,并允许它们通过使用其构建后端进行构建和安装来支持安装“当前项目”。
实际上,这意味着 Poetry 和 PDM 都不支持非包项目。
非标准依赖说明符
PDM 和 Poetry 扩展了 PEP 508 依赖说明符,增加了其他不属于任何共享标准的功能。然而,这两种工具对这些问题采用略有不同的方法。
PDM 通过一种类似于 pip install 参数集的语法支持指定本地路径和可编辑安装。例如,以下依赖组包含一个处于可编辑模式的本地包
[tool.pdm.dev-dependencies]
mygroup = ["-e file:///${PROJECT_ROOT}/foo"]
这声明了一个名为 mygroup 的依赖组,其中包含来自 foo 目录的本地可编辑安装。
Poetry 将依赖组描述为表,将包名称映射到说明符。例如,与上述 mygroup 示例相同的配置在 Poetry 下可能如下所示
[tool.poetry.group.mygroup]
foo = { path = "foo", editable = true }
PDM 仅限于字符串语法,而 Poetry 引入了描述依赖项的表格。
安装和引用依赖组
PDM 和 Poetry 都为安装依赖组提供了特定于工具的支持。由于这两个项目都支持自己的锁定文件格式,因此它们都能够透明地使用依赖组名称来引用该组的 锁定 依赖数据。
然而,这两种工具的依赖组都无法从 tox、nox 或 pip 等其他工具中原生引用。例如,尝试在 tox 下安装依赖组需要显式调用 PDM 或 Poetry 来解析其依赖数据并执行相关的安装步骤。
附录 C:用例
Web 应用程序
Web 应用程序(例如 Django 或 Flask 应用程序)通常不需要构建分发版,而是将其源代码捆绑并交付给部署工具链。
例如,源代码仓库可能定义 Python 打包元数据以及容器化或其他构建管道元数据(Dockerfile 等)。Python 应用程序通过将整个仓库复制到构建上下文、安装依赖项并将结果捆绑为机器镜像或容器来构建。
此类应用程序具有用于构建的依赖组,也用于代码检查、测试等。实际上,如今,这些应用程序通常将自己定义为包,以便能够使用打包工具和机制(如 extras)来管理其依赖组。然而,它们在概念上并非包,不适合以 sdist 或 wheel 格式分发。
依赖组允许这些应用程序定义其各种依赖项,而无需依赖打包元数据,也无需尝试以打包术语表达其需求。
库
库是构建分发版(sdist 和 wheel)并将其发布到 PyPI 的 Python 包。
对于库而言,依赖组是 extras 的替代方案,用于定义开发依赖项组,具有上述重要优势。
一个库可以定义 test 和 typing 组,允许测试和类型检查,因此依赖于库自身的依赖项(如 [project.dependencies] 中指定的)。
其他开发需求可能根本不需要安装包。例如,lint 依赖组可能有效且安装速度更快,因为它只安装 black、ruff 或 flake8 等工具。
lint 和 test 环境也可能是连接 IDE 或编辑器支持的有价值的位置。有关此类用法的更完整描述,请参阅下面的用例。
这是一个可能适用于库的依赖组表示例
[dependency-groups]
test = ["pytest<8", "coverage"]
typing = ["mypy==1.7.1", "types-requests"]
lint = ["black", "flake8"]
typing-test = [{include-group = "typing"}, "pytest<8"]
请注意,这些都没有隐式安装库本身。因此,任何环境管理工具链都有责任在需要时将适当的依赖组与库一起安装,例如 test 的情况。
数据科学项目
数据科学项目通常以脚本和实用程序逻辑集合的形式出现,用于处理和分析数据,使用通用工具链。组件可能以 Jupyter Notebook 格式(ipynb)定义,但依赖于相同的通用核心实用程序集。
在此类项目中,没有要构建或安装的包。因此,pyproject.toml 目前不提供任何依赖管理或声明解决方案。
能够为此类项目定义至少一个主要依赖分组是有价值的。例如
[dependency-groups]
main = ["numpy", "pandas", "matplotlib"]
然而,各种脚本可能还需要额外的支持工具。项目甚至可能对不同的组件具有冲突或不兼容的工具或工具版本,因为它们会随着时间的推移而演变。
考虑以下更复杂的配置
[dependency-groups]
main = ["numpy", "pandas", "matplotlib"]
scikit = [{include-group = "main"}, "scikit-learn==1.3.2"]
scikit-old = [{include-group = "main"}, "scikit-learn==0.24.2"]
这定义了 scikit 和 scikit-old 作为常用依赖套件的两个类似变体,引入了不同版本的 scikit-learn 以适应不同的脚本。
本 PEP 仅定义这些数据。它没有正式化任何机制,让数据科学项目(或任何其他类型的项目)将依赖项安装到已知环境中,或将这些环境与各种脚本关联起来。此类数据组合留给工具作者解决,并可能最终标准化。
锁定文件生成
目前 Python 生态系统中存在许多生成锁定文件的工具。PDM 和 Poetry 各自使用自己的锁定文件格式,而 pip-tools 则生成带有版本固定和哈希的 requirements.txt 文件。
依赖组不适合存储锁定文件,因为它们缺乏许多必要的功能。最值得注意的是,它们无法存储哈希值,而大多数锁定文件用户认为哈希值至关重要。
然而,依赖组是生成锁定文件的工具的有效输入。此外,PDM 和 Poetry 都允许使用依赖组名称(根据它们对依赖组的定义)来指代其锁定版本。
因此,考虑一个生成锁定文件的工具,这里称为 $TOOL。它可以按如下方式使用
$TOOL lock --dependency-group=test
$TOOL install --dependency-group=test --use-locked
所有此类工具需要做的就是确保其锁定文件数据记录名称 test,以支持此类用法。
不保证依赖组的相互兼容性。例如,上面的数据科学示例显示了 scikit-learn 的冲突版本。因此,串联安装多个锁定依赖组可能需要工具应用额外的约束或生成额外的锁定文件数据。这些问题被认为超出了本 PEP 的范围。
作为如何锁定组合的两个示例
- 工具可能要求明确生成任何组合的锁定文件数据,以使其被视为有效
- Poetry 实现了所有依赖组必须相互兼容的要求,并且只生成一个锁定版本。(这意味着它找到一个单一的解决方案,而不是一组或矩阵的解决方案。)
环境管理器输入
tox、Nox 和 Hatch 中的常见用法是将一组依赖项安装到测试环境中。
例如,在 tox.ini 中,类型检查依赖项可以内联定义
[testenv:typing]
deps =
pyright
useful-types
commands = pyright src/
这种组合在有限的上下文中提供了理想的开发者体验。在相关的环境管理器下,测试环境所需的依赖项与需要这些依赖项的命令一起声明。它们不像 extras 那样发布在包元数据中,并且对于需要它们来构建相关环境的工具来说是可发现的。
依赖组通过有效地将这些需求数据从工具特定的位置“提升”到更广泛可用的位置来应用于此类用法。在上面的示例中,只有 tox 可以访问声明的依赖项列表。在支持依赖组的实现下,相同的数据可以在依赖组中可用
[dependency-groups]
typing = ["pyright", "useful-types"]
然后可以在多个工具下使用数据。例如,tox 可以实现对 dependency_groups = typing 的支持,替换上面的 deps 用法。
为了使依赖组成为环境管理器用户的可行替代方案,环境管理器需要像支持内联依赖项声明一样支持处理依赖组。
IDE 和编辑器对需求数据的使用
IDE 和编辑器集成可能会受益于用于集成的依赖组的约定或可配置名称定义。
至少有两种已知情况,编辑器或 IDE 能够发现项目的非发布依赖项是很有价值的
- 测试:VS Code 等 IDE 支持用于运行特定测试的 GUI 界面
- Linting:编辑器和 IDE 通常支持 Linting 和自动格式化集成,这些集成会突出显示或自动更正错误
这些情况可以通过定义 test、lint 和 fix 等约定组名,或者通过定义允许选择依赖组的配置机制来处理。
例如,以下 pyproject.toml 声明了上述三个组
[dependency-groups]
test = ["pytest", "pytest-timeout"]
lint = ["flake8", "mypy"]
fix = ["black", "isort", "pyupgrade"]
本 PEP 不试图标准化这些名称或将其保留用于此类用途。IDE 可以标准化或允许用户配置用于各种目的的组名。
此声明允许项目作者关于项目适当工具的知识与该项目的所有编辑者共享。
版权
本文档置于公共领域或 CC0-1.0-Universal 许可证下,以更宽松者为准。
来源:https://github.com/python/peps/blob/main/peps/pep-0735.rst