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

Python 增强提案

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 日

目录

摘要

本 PEP 指定了一种在 pyproject.toml 文件中存储包需求的机制,使得它们不会包含在项目的任何构建分发中。

这适用于创建类似于 requirements.txt 文件的命名依赖项分组,启动器、IDE 和其他工具可以按名称找到并识别它们。

此处定义的功能称为“依赖项分组”。

动机

Python 社区没有标准化答案的两个主要用例

  • 如何为包定义开发依赖项?
  • 如何为不构建分发(非包项目)的项目定义依赖项?

为了支持这两个需求,有两个类似于本提案的常见解决方案

  • requirements.txt 文件
  • extras

requirements.txt 文件和 extras 都存在本标准试图克服的局限性。

请注意,上面提到的两个用例描述了本 PEP 试图支持的两种不同类型的项目

  • Python 包,例如库
  • 非包项目,例如数据科学项目

requirements.txt 文件的局限性

许多项目可能定义一个或多个 requirements.txt 文件,并将它们排列在项目根目录(例如 requirements.txttest-requirements.txt)或目录中(例如 requirements/base.txtrequirements/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 extra 的 foo

由于 extras 是包元数据,因此在项目不构建分发(即不是包)时无法使用它们。

对于作为包的项目,extras 是定义开发依赖项的常见解决方案,但即使在这种情况下,它们也存在缺点

  • 由于 extra 定义了可选的附加依赖项,因此无法在不安装当前包及其依赖项的情况下安装 extra
  • 由于它们是用户可安装的,因此 extras 是包的公共接口的一部分。由于 extras 是发布的,因此包开发者经常担心确保他们的开发 extras 不与面向用户的 extras 混淆。

原理

本 PEP 定义了将需求数据存储在 [dependency-groups] 表格中的列表中。这个名称是为了与该功能的规范名称(“依赖项分组”)相匹配而选择的。

此格式应该尽可能简单易学,在许多情况下其格式与现有的 requirements.txt 文件非常相似。[dependency-groups] 中的每个列表都定义为包说明符列表。例如

[dependency-groups]
test = ["pytest>7", "coverage"]

存在许多 requirements.txt 文件的用例,这些用例需要在 PEP 508 依赖项说明符中无法表达的数据。此类字段在依赖项分组中无效。包含 pip 支持的许多数据和字段,例如索引服务器、哈希和路径依赖项,需要新的标准。本标准为新的标准和发展留下了空间,但没有尝试支持所有有效的 requirements.txt 内容。

唯一的例外是 -r 标志,requirements.txt 文件使用它将一个文件包含在另一个文件中。依赖项分组支持一个“包含”机制,其含义类似,允许一个依赖项分组扩展另一个依赖项分组。

依赖项分组具有两个与 requirements.txt 文件类似的附加功能

  • 它们不会作为任何构建分发中的不同元数据发布
  • 安装依赖项分组并不意味着安装包的依赖项或包本身

用例

以下用例被认为是本 PEP 的重要目标。它们在用例附录中进行了更详细的定义。

  • 通过非 Python 打包构建过程部署的 Web 应用程序
  • 带有未发布的开发依赖项分组的库
  • 具有依赖项分组但没有核心包的数据科学项目
  • 输入数据到锁文件生成(依赖项分组通常不应作为锁定依赖项数据的存储位置使用)
  • 环境管理器的输入数据,例如 tox、Nox 或 Hatch
  • IDE 可配置的测试和 linter 需求发现
  • 公开包依赖项以进行安装,而不包含包本身

project.dependencies 中包含支持

允许 project.dependenciesproject.optional-dependencies 表格包含依赖项分组,需要更新这些表格的规范。

做出这些更改的驱动力是,某些用例通过添加此类支持得到了很好的解决,并且如果在初始依赖项分组 PEP 中没有包含支持,而是在随后的 PEP 中添加此类支持,那么对于工具维护者来说,支持环境将变得更加困难。

project.dependenciesproject.optional-dependencies 中包含依赖组,需要采用依赖组包含的形式,在下面的规范部分定义。

关于 Poetry 和 PDM 依赖项分组

现有的 Poetry 和 PDM 工具已经提供了“依赖组”的功能。但是,由于缺少定义依赖集合的标准,每个工具在 [tool] 表的相关部分以工具特定的方式定义了它们。

(PDM 也使用 extras 来表示一些依赖组,并且与 extras 的概念重叠很大。)

本 PEP 不支持 Poetry 和 PDM 的所有功能,这些功能与 piprequirements.txt 文件一样,支持对常见依赖规范的多种非标准扩展。

这些工具可以使用标准化的依赖组来扩展其自己的依赖组机制。但是,定义一个新的数据格式来替换现有的 Poetry 和 PDM 解决方案并非目标。这样做需要标准化一些额外的功能,例如路径依赖,这些功能由这些工具支持。

依赖项分组不是隐藏的 extras

依赖组与未发布的 extras 非常相似。但是,它们有三个主要特征将它们与 extras 进一步区分开

  • 它们支持非包项目
  • 安装依赖组并不意味着安装包的依赖项(或包本身)
  • 包的依赖项(和 extras)可能依赖于依赖组

未来兼容性和无效数据

依赖组旨在在未来的 PEP 中可扩展。但是,依赖组也应该可以在单个 Python 项目中被多个工具使用。当多个工具使用相同的数据时,一个工具可能实现一个扩展依赖组的未来 PEP,而另一个工具则不实现。

为了支持这种情况下的用户,本 PEP 定义并推荐验证行为,其中工具只检查它们正在使用的依赖组。这允许使用不同版本依赖组数据的多个工具共享 pyproject.toml 中的单个表。

规范

本 PEP 在 pyproject.toml 文件中定义了一个新的部分(表),名为 dependency-groupsdependency-groups 表包含任意数量的用户定义键,每个键的值都是一个依赖项列表(定义如下)。这些键必须是 有效的非规范化名称,并且必须在比较之前 规范化

工具应该默认情况下优先向用户显示原始的非规范化名称。如果在规范化后遇到重复的名称,工具应该发出错误。

dependency-groups 下的依赖项列表可以包含字符串、表(Python 中的“字典”)或字符串和表的混合。

依赖项列表中的字符串必须是有效的 依赖规范,如 PEP 508 中所定义。

依赖项列表中的表必须是有效的依赖项对象规范,在下面定义。

pyproject.toml 中的 project 表被修改,这样 project.dependenciesproject.optional-dependencies 的值可以包含依赖项对象规范。

依赖项对象说明符

依赖项对象规范是定义零个或多个依赖项的表。

本 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"]。工具应该像处理其他任何情况一样处理这样的列表,在这种情况下,他们被要求使用不同的版本约束多次处理同一个依赖项。

依赖组包含可以包含包含依赖组包含的列表,在这种情况下,这些包含也应该被扩展。依赖组包含不能包含循环,如果工具检测到循环,应该报告错误。

project 表格更改

最初在 PEP 621 中定义的 [project] 表以两种方式扩展。

dependencies

除了 PEP 508 字符串,数组还可以包含依赖项对象规范。

optional-dependencies

除了 PEP 508 字符串,该表中的数组值还可以包含依赖项对象规范。

依赖项分组表格示例

以下是一个使用此方法定义四个依赖组的 pyproject.toml 部分示例: testdocstypingtyping-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 重叠的安装 UX

工具可以选择为安装依赖组提供与安装 extras 相同的接口。

请注意,本规范不禁止有一个名称与依赖组匹配的 extra。在这种情况下,工具必须定义自己的优先级顺序或消除歧义的语义。

建议用户避免创建名称与 extras 匹配的依赖组。工具不应该将这种匹配视为错误。

验证和兼容性

支持依赖组的工具可能希望在使用数据之前验证数据。但是,实现这种验证行为的工具应该注意允许未来对本规范进行扩展,以便它们在出现新语法时不会不必要地发出错误或警告。

工具在评估或处理依赖组中无法识别的 data 时应该报错。

工具不应该急切地验证 **所有** 依赖组的列表内容。

这意味着在存在以下数据的情况下,大多数工具将允许使用 foo 组,并且只有在使用 bar 组时才会报错

[dependency-groups]
foo = ["pyparsing"]
bar = [{set-phasers-to = "stun"}]

Linter 和验证器可能更严格

对于主要安装或解析依赖组的工具,不建议进行急切验证。Lint 工具和验证工具可能会有充分的理由忽略此建议。

参考实现

以下参考实现将依赖组的内容打印到标准输出,以换行符分隔。因此,输出是有效的 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 中指定的标准使用,因此没有直接的向后兼容性问题。

但是,将该功能引入作为 project 数据的潜在组件对许多生态系统工具有影响。

审计和更新工具

许多工具能够识别 Python 依赖数据,这些数据以 project.dependenciesproject.optional-dependencies 的形式表达,并且可能额外支持 setup.cfgrequirements.txt,甚至 setup.py。(例如,Dependabot、Tidelift 等)

这些工具会检查依赖数据,并在某些情况下提供工具辅助或完全自动化的更新。我们预计,这些工具最初不会支持新的依赖关系组,并且整个生态系统可能需要数月甚至数年才能完全支持。

因此,在用户开始使用依赖关系组时,其工作流程和工具支持将出现退化。任何有关依赖数据编码位置和方式的新标准都会出现这种情况。

重新打包

重新打包,特别是 Grayskull 或 PyInstaller 等工具辅助的重新打包,需要处理对软件包元数据定义的更改。

由于 conda 和 Linux 发行版等其他生态系统的重新打包者通常与软件包发布者不同,因此与软件包维护者受到影响的情况相比,更难以解决这种兼容性问题。软件包维护者可能不知道使用依赖关系组的影响,并且可能无意中做出影响下游重新打包工作流程的更改。

解决此问题主要有两种方法:

  • 通过教育和“如何教授”——重新打包软件包的用户应该了解,使用新标准可能会导致下游软件包使用者出现问题。
  • 通过构建后端驱动更多行为——只要通过 PEP 517 接口收集依赖关系元数据,下游重新打包者就可以忽略正在使用的构建系统以及它是否支持依赖关系组。

安全隐患

本 PEP 引入了新的语法和数据格式,用于在项目中指定依赖关系信息。但是,它没有引入新的依赖关系处理或解析机制。

因此,除了可能用于安装依赖关系的任何工具固有的安全问题外,它不包含其他安全问题——也就是说,恶意依赖关系可能在这里指定,就像它们可能在 requirements.txt 文件中指定一样。

如何教授它

此功能应使用其规范名称“依赖关系组”来引用。

基本的使用形式应该作为典型 requirements.txt 数据的一种变体来教授。标准依赖关系规范符(PEP 508)可以添加到命名列表中。pip 或相关工作流程工具将从命名的依赖关系组进行安装,而不是要求 pip 从 requirements.txt 文件进行安装。

对于新的 Python 用户,他们可以被直接教导如何在 pyproject.toml 中创建包含其依赖关系组的部分,类似于他们目前被教导如何使用 requirements.txt 文件的方式。这也使新的 Python 用户能够学习 pyproject.toml 文件,而无需学习软件包构建。仅包含 [dependency-groups] 且不包含其他表的 pyproject.toml 文件是有效的。

对于新的和有经验的用户,都需要解释依赖关系组包含。对于使用 requirements.txt 经验丰富用户,这可以描述为 -r 的一种类似物。对于新用户,他们应该被教导包含允许一个依赖关系组扩展另一个依赖关系组。类似的配置接口和 Python list.extend 方法可以用来通过类比解释这个概念。

使用过 setup.py 打包的 Python 用户可能熟悉在 pyproject.toml 出现之前常用的做法,其中软件包元数据是动态定义的。从 requirements.txt 文件加载的要求和在 setup() 调用之前定义静态列表,很容易与依赖关系组进行类比。

重新打包的包说明

需要特别注意的是,那些软件包被 Linux 发行版、Homebrew、conda 等重新打包的软件包维护者。

在核心项目元数据 project.dependenciesproject.optional-dependencies 中使用依赖关系组包含,可能会破坏这些使用者对您软件包的使用。由于他们可能会消费并直接与您的存储库的源代码进行交互,因此他们的工具链可能不支持依赖关系组,而软件包维护者的工具却已更新。

确保重新打包使用者可以在出现问题时与您联系,并在更改日志中记录过渡到使用依赖关系组。

使用依赖项分组的接口

本规范没有提供与依赖关系组交互的通用接口,除了通过 project 表包含在已构建的软件包中。这对于工具作者和用户都有影响。

工具作者应该确定依赖关系组是否与其用户故事相关,以及如何构建自己的接口以适应。对于环境管理器、解析器、安装程序和相关的非构建工具,他们将能够记录他们支持“PEP 735 依赖关系组”,但他们将负责记录其使用模式。对于构建后端,支持依赖关系组需要支持从 project 表包含,但没有设置其他严格的要求。

对于用户,主要的影响是,他们在希望在软件包构建之外使用依赖关系组时,必须查阅相关工具文档。工具应该通过文档、运行时警告或错误来告知用户关于不推荐或不支持的使用方法。例如,如果一个工具希望要求所有依赖关系组相互兼容,不包含任何矛盾的软件包规范符,则它应该记录该限制,并告知用户如何为其目的适当利用依赖关系组。

被拒绝的想法

为什么不将每个依赖项分组定义为一个表格?

如果我们的目标是允许将来扩展,那么将每个依赖关系组定义为一个子表,从而使我们能够将未来的键附加到每个组,将为未来提供最大的灵活性。

但是,这也使结构嵌套更深,因此更难教和学。本 PEP 的目标之一是成为许多 requirements.txt 用例的简单替代方案。

为什么不定义一个特殊的字符串语法来扩展依赖项说明符?

本规范的早期草案定义了依赖关系组包含和路径依赖的语法形式。

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

  • 它使必须教授的字符串语法比 PEP 508 更复杂。
  • 生成的字符串总是需要与 PEP 508 规范符区分开来,这会使实现复杂化。

为什么不允许使用更多非 PEP 508 依赖项说明符?

在讨论中,出现了一些用例,这些用例需要比 PEP 508 允许的更具表达力的规范符。

“路径依赖”,指的是本地路径,以及对 [project.dependencies] 的引用,尤其受到关注。

然而,目前还没有这些功能的现有标准(除了 pip 实现细节的事实标准)。

因此,试图在本 PEP 中包含这些功能会导致范围大幅增长,以试图标准化这些各种功能和 pip 行为。

特别注意了试图标准化可编辑安装的表达,如 pip install -ePEP 660 所表达的那样。但是,尽管可编辑安装的创建已针对构建后端标准化,但可编辑安装的行为尚未针对安装程序标准化。在本 PEP 中包含可编辑安装要求任何支持工具允许安装可编辑安装。

因此,尽管 Poetry 和 PDM 为某些功能提供了语法,但目前这些功能被认为不够标准化,不适合包含在依赖关系组中。

为什么表格不命名为 [run][project.dependency-groups],…?

这个概念有许多可能的名称。它必须与已经存在的 [project.dependencies][project.optional-dependencies] 表共存,并且可能还需要一个新的 [external] 依赖关系表(在撰写本文时,PEP 725 定义了 [external] 表,正在进行中)。

[run] 是早期讨论中领先的提议,但其提出的用途集中在一组运行时依赖关系上。本 PEP 明确概述了多组依赖关系,这使得 [run] 不太适合——这不是特定运行时上下文的依赖关系数据,而是多个上下文的依赖关系数据。

[project.dependency-groups] 将与 [project.dependencies][project.optional-dependencies] 提供良好的并行,但对于非软件包项目来说存在重大缺点。 [project] 要求定义几个键,例如 nameversion。使用这个名称要么需要重新定义 [project] 表以允许这些键缺失,要么会对非软件包项目强制要求定义和使用这些键。由此类推,它实际上会要求任何非软件包项目允许自己被视为一个软件包。

为什么 pip 计划实现的 --only-deps 不够?

pip 目前路线图上有一个功能,要添加一个 –only-deps 标志。该标志旨在允许用户安装软件包依赖关系和附加项,而无需安装当前软件包。

它没有解决非软件包项目的需要,也没有允许在没有软件包依赖关系的情况下安装附加项。

为什么 <环境管理器> 不是解决方案?

像 tox、Nox 和 Hatch 这样的现有环境管理器已经具备将内联依赖项作为其配置数据的一部分进行列出的能力。这满足了许多开发依赖项需求,并将依赖项组清晰地与可执行的相关任务相关联。这些机制很好,但它们不足够

首先,它们没有解决非包项目的需要。

其次,没有其他工具可以用来访问这些数据的标准。这对像 IDE 和 Dependabot 这样的高级工具有影响,这些工具无法支持与这些依赖项组的深度集成。(例如,在撰写本文时,Dependabot 不会标记在 tox.ini 文件中固定好的依赖项。)

为什么不支持 [build-system.requires] 中的依赖项分组包含?

虽然将来允许这样做可能很有趣,但将该提案合并到构建系统表中会降低用户依赖此功能来启动支持的能力。PEP 517 前端需要支持依赖项组才能实现这一点,而这些前端的版本不容易由包控制。

通过仅定义构建后端支持,包就可以开始利用新的语法和功能,而不必担心控制包的构建和安装环境。

附录 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 可以用来代替版本号。使用 URL 时,它们隐式地引用包源代码的压缩包。

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)元数据的文件

提供 bundle 命令的 bundler 工具是使用 Gemfile 数据的主要接口。

gem 工具负责通过 gem build 命令从 .gemspec 数据构建 gem。

Gemfiles 和 bundle

Gemfile 是一个 Ruby 文件,其中包含 gem 指令,这些指令包含在任意数量的 group 声明中。 gem 指令也可以在 group 声明之外使用,在这种情况下,它们会形成一个隐式命名的依赖项组。

例如,以下 Gemfilerails 列为项目依赖项。所有其他依赖项都在组下列出

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,所有组都会被安装。用户可以通过手动或通过 CLI 创建或修改 .bundle/config 中的 bundler 配置来取消选择组。例如, bundle config set --local without 'lint:docs'

使用以上数据,无法排除对 'rails' gem 的顶级使用,也无法按名称引用该隐式分组。

gemspec 和打包的依赖项数据

gemspec 文件 是一个 Ruby 文件,其中包含一个 Gem::Specification 实例声明。

Gem::Specification 中只有两个字段与包依赖项数据有关。它们是 add_development_dependencyadd_runtime_dependencyGem::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 使用。

有关完整细节,请参阅 bundlerGemfile 文档 中的 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 都对安装依赖项组提供工具特定支持。由于这两个项目都支持他们自己的锁定文件格式,因此它们都具有透明地使用依赖项组名称来引用该组的锁定依赖项数据的功能。

但是,这两个工具的依赖项组都不能被其他工具(如 toxnoxpip)本地引用。例如,尝试在 tox 下安装依赖项组,需要显式调用 PDM 或 Poetry 来解析他们的依赖项数据并执行相关的安装步骤。

附录 C:用例

通过非 Python 打包构建过程部署的 Web 应用程序

Web 应用程序(例如 Django 或 Flask 应用程序)通常不需要构建发行版,而是将源代码捆绑并发送到部署工具链。

例如,源代码存储库可以定义 Python 打包元数据以及容器化或其他构建管道元数据(Dockerfile 等)。Python 应用程序通过将整个存储库复制到构建上下文中、安装依赖项以及将结果捆绑为机器映像或容器来构建。

此类应用程序具有用于构建的依赖项组,也具有用于 linting、测试等的依赖项组。在实践中,如今,这些应用程序通常将自己定义为包,以便能够使用打包工具和机制(如 extras)来管理他们的依赖项组。但是,从概念上来说,它们不是包,不适合以 sdist 或 wheel 格式分发。

依赖项组允许这些应用程序定义其各种依赖项,而不依赖打包元数据,也不试图用打包术语表达他们的需求。

带有未发布的开发依赖项分组的库

库是构建发行版(sdist 和 wheel)并将它们发布到 PyPI 的 Python 包。

对于库,依赖项组代表定义开发依赖项组的 extras 的替代方案,具有上述重要优势。

库可以定义 testtyping 组,这些组允许测试和类型检查,因此依赖于库自己的依赖项(如 [project.dependencies] 中所指定)。

其他开发需求可能根本不需要安装包。例如,lint 依赖项组可能是有效的,并且在没有库的情况下更快地安装,因为它只安装 blackruffflake8 等工具。

linttest 环境也可能是将 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"]

这将 scikitscikit-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 界面。
  • 代码风格检查:编辑器和 IDE 通常支持代码风格检查和自动格式化集成,这些集成可以突出显示或自动更正错误。

这些情况可以通过定义常规的组名称(如 testlintfix)来处理,或者通过定义允许选择依赖项组的配置机制来处理。

例如,以下 pyproject.toml 声明了上述三个组。

[dependency-groups]
test = ["pytest", "pytest-timeout"]
lint = ["flake8", "mypy"]
fix = ["black", "isort", "pyupgrade"]

本 PEP 并未尝试将这些名称标准化,也未将其保留用于此类用途。IDE 可以将名称标准化,也可以允许用户配置用于各种目的的组名称。

此声明允许项目作者对项目的适当工具的知识与该项目的所有编辑器共享。

公开包依赖项而不包含包本身

在各种用例中,包开发者希望只安装包的依赖项,而不安装包本身。

例如:

  • 在构建依赖项时与构建包本身时指定不同的环境变量或选项。
  • 创建分层容器镜像,其中依赖项与要安装的包隔离。
  • 将依赖项提供给分析环境(例如类型检查),而无需构建和安装包本身。

对于最后一个案例的示例,请考虑以下示例 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 依赖项组来跳过包的安装——这不仅更高效,而且可能会减少测试系统对需求的要求。


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

最后修改时间:2024-09-04 23:11:28 GMT