跳到内容

解析

解析是将需求列表转换为满足需求的包版本列表的过程。解析需要递归搜索包的兼容版本,确保满足请求的需求以及请求包的需求兼容。

依赖

大多数项目和包都有依赖项。依赖项是当前包工作所需的其他包。一个包将其依赖项定义为需求,大致是包名称和可接受版本的组合。当前项目定义的依赖项称为直接依赖项。当前项目的每个依赖项添加的依赖项称为间接传递依赖项

注意

有关依赖项的详细信息,请参阅 Python 打包文档中的依赖项说明符页面

基本示例

为了帮助演示解析过程,请考虑以下依赖项

  • 该项目依赖于 foobar
  • foo 有一个版本,1.0.0
    • foo 1.0.0 依赖于 lib>=1.0.0
  • bar 有一个版本,1.0.0
    • bar 1.0.0 依赖于 lib>=2.0.0
  • lib 有两个版本,1.0.0 和 2.0.0。两个版本都没有依赖项。

在此示例中,解析器必须找到一组满足项目要求的包版本。由于 foobar 都只有一个版本,因此将使用这些版本。解析还必须包括传递依赖项,因此必须选择 lib 的版本。foo 1.0.0 允许 lib 的所有可用版本,但 bar 1.0.0 需要 lib>=2.0.0,因此必须使用 lib 2.0.0

在某些解析中,可能存在多个有效解决方案。考虑以下依赖项

  • 该项目依赖于 foobar
  • foo 有两个版本,1.0.0 和 2.0.0
    • foo 1.0.0 没有依赖项。
    • foo 2.0.0 依赖于 lib==2.0.0
  • bar 有两个版本,1.0.0 和 2.0.0
    • bar 1.0.0 没有依赖项。
    • bar 2.0.0 依赖于 lib==1.0.0
  • lib 有两个版本,1.0.0 和 2.0.0。两个版本都没有依赖项。

在此示例中,必须选择 foobar 的某些版本;但是,确定哪个版本需要考虑 foobar 的每个版本的依赖项。foo 2.0.0bar 2.0.0 不能一起安装,因为它们在其所需的 lib 版本上存在冲突,因此解析器必须选择 foo 1.0.0(以及 bar 2.0.0)或 bar 1.0.0(以及 foo 1.0.0)。两者都是有效的解决方案,不同的解析算法可能会产生任一结果。

平台标记

标记允许将表达式附加到需求,以指示何时应使用该依赖项。例如,bar ; python_version < "3.9" 表示 bar 只能安装在 Python 3.8 及更早版本上。

标记用于根据当前环境或平台调整包的依赖项。例如,标记可用于通过操作系统、CPU 架构、Python 版本、Python 实现等来修改依赖项。

注意

有关标记的更多详细信息,请参阅 Python 打包文档中的环境标记部分。

标记对于解析非常重要,因为它们的值会更改所需依赖项。通常,Python 包解析器使用当前平台的标记来确定要使用的依赖项,因为该包通常在当前平台上安装。但是,对于锁定依赖项,这存在问题——锁定文件仅适用于使用创建锁定文件的相同平台的开发人员。为了解决这个问题,存在平台无关或“通用”解析器。

uv 同时支持平台特定通用解析。

平台特定解析

默认情况下,uv 的 pip 接口,即uv pip compile,生成特定于平台的解析,如 pip-tools。无法在 uv 的项目界面中使用特定于平台的解析。

uv 还支持使用 --python-platform--python-version 选项解析特定、替代平台和 Python 版本。例如,如果在 macOS 上使用 Python 3.12,则可以使用 uv pip compile --python-platform linux --python-version 3.10 requirements.in 来生成适用于 Linux 上 Python 3.10 的解析。与通用解析不同,在平台特定解析期间,提供的 --python-version 是要使用的确切 Python 版本,而不是下限。

注意

Python 的环境标记比简单的 --python-platform 参数可以表达的关于当前机器的信息要多得多。例如,macOS 上的 platform_version 标记包括内核构建的时间,理论上可以将其编码在包需求中。uv 的解析器尽最大努力生成与在目标 --python-platform 上运行的任何机器兼容的解析,这对于大多数用例来说应该足够了,但可能会失去复杂包和平台组合的保真度。

通用解析

uv 的锁定文件 (uv.lock) 是使用通用解析创建的,并且可以在平台之间移植。这确保了依赖项被锁定给所有在项目上工作的人,无论操作系统、架构和 Python 版本如何。uv 锁定文件由项目命令创建和修改,例如 uv lockuv syncuv add

通用解析也可在 uv 的 pip 接口中使用,即uv pip compile,带有 --universal 标志。生成的 requirements 文件将包含标记,以指示每个依赖项适用的平台。

在通用解析期间,如果不同的平台需要不同的版本,则一个包可能会多次列出,具有不同的版本或 URL — 标记确定将使用哪个版本。通用解析通常比平台特定解析更受约束,因为我们需要考虑所有标记的需求。

在通用解析期间,所有必需的包都必须与 pyproject.toml 中声明的 整个 requires-python 范围兼容。例如,如果项目的 requires-python>=3.8,如果给定依赖项的所有版本都需要 Python 3.9 或更高版本,则解析将失败,因为该依赖项缺少 Python 3.8(项目支持范围的下限)的可用版本。换句话说,项目的 requires-python 必须是其所有依赖项的 requires-python 的子集。

在为给定依赖项选择兼容版本时,uv 将(默认情况下)尝试为每个支持的 Python 版本选择最新的兼容版本。例如,如果项目的 requires-python>=3.8,并且依赖项的最新版本需要 Python 3.9 或更高版本,而支持 Python 3.8 的所有先前版本,则解析器将为运行 Python 3.9 或更高版本的用户选择最新版本,并为运行 Python 3.8 的用户选择先前版本。

在评估依赖项的 requires-python 范围时,uv 仅考虑下限,完全忽略上限。例如,>=3.8, <4 被视为 >=3.8。尊重 requires-python 的上限通常会导致形式上正确但实际上不正确的解析,因为例如,解析器将回溯到省略上限的第一个已发布版本(参见:Requires-Python 上限)。

受限解析环境

默认情况下,通用解析器尝试解决所有平台和 Python 版本。

如果您的项目仅支持有限的平台或 Python 版本集,则可以通过 environments 设置来约束已解决平台的集合,该设置接受PEP 508 环境标记列表。换句话说,您可以使用 environments 设置来减少支持的平台集。

例如,要将锁定文件约束为 macOS 和 Linux,并避免解决 Windows

pyproject.toml
[tool.uv]
environments = [
    "sys_platform == 'darwin'",
    "sys_platform == 'linux'",
]

或者,要避免解决替代 Python 实现

pyproject.toml
[tool.uv]
environments = [
    "implementation_name == 'cpython'"
]

environments 设置中的条目必须是不相交的(即,它们不能重叠)。例如,sys_platform == 'darwin'sys_platform == 'linux' 是不相交的,但 sys_platform == 'darwin'python_version >= '3.9' 不是,因为两者可能同时为真。

所需环境

在 Python 生态系统中,包可以作为源码分发、构建分发(轮子)或两者都发布;但是要安装一个包,需要一个构建分发。如果一个包缺少构建分发,或者缺少当前平台或 Python 版本的分发(构建分发通常是平台特定的),uv 将尝试从源码构建包,然后安装生成的构建分发。

一些包(如 PyTorch)发布构建分发,但省略了源码分发。此类包可在具有可用构建分发的平台上安装。例如,如果一个包发布了 Linux 的构建分发,但没有发布 macOS 或 Windows 的构建分发,则该包可在 Linux 上安装。

缺少源码分发的包会导致通用解析出现问题,因为通常至少有一个平台或 Python 版本无法安装该包。

默认情况下,uv 要求每个此类包都包含至少一个与目标 Python 版本兼容的轮子。可以使用 required-environments 设置来确保生成的解析包含特定平台的轮子,或者如果没有任何此类轮子可用,则会失败。该设置接受PEP 508 环境标记列表。

虽然 environments 设置限制了 uv 在解析依赖项时将考虑的环境集,但 required-environments 扩展了 uv 在解析依赖项时必须支持的平台集。

例如,environments = ["sys_platform == 'darwin'"] 会将 uv 限制为解决 macOS(并忽略 Linux 和 Windows)。另一方面,required-environments = ["sys_platform == 'darwin'"]要求任何没有源码分发的包都包含 macOS 的轮子才能安装(如果没有任何此类轮子可用,则会失败)。

在实践中,required-environments 可用于声明对非最新平台的显式支持,因为这通常需要回溯到这些包的最新发布版本。例如,要保证任何仅限构建分发的包都包含对 Intel macOS 的支持

pyproject.toml
[tool.uv]
required-environments = [
    "sys_platform == 'darwin' and platform_machine == 'x86_64'"
]

依赖偏好

如果解析输出文件存在,即 uv 锁定文件 (uv.lock) 或 requirements 输出文件 (requirements.txt),uv 将优先使用其中列出的依赖项版本。同样,如果将包安装到虚拟环境中,如果存在,uv 将优先使用已安装的版本。这意味着锁定的或已安装的版本不会更改,除非请求了不兼容的版本或使用 --upgrade 显式请求了升级。

解析策略

默认情况下,uv 尝试使用每个包的最新版本。例如,uv pip install flask>=2.0.0 将安装 Flask 的最新版本,例如 3.0.0。如果 flask>=2.0.0 是项目的依赖项,则仅使用 flask 3.0.0。这很重要,例如,因为运行测试不会检查项目是否实际与其声明的 flask 2.0.0 下限兼容。

使用 --resolution lowest,uv 将为所有依赖项(包括直接和间接(传递))安装尽可能低的可能版本。或者,--resolution lowest-direct 将对所有直接依赖项使用最低兼容版本,而对所有其他依赖项使用最新兼容版本。uv 将始终对构建依赖项使用最新版本。

例如,给定以下 requirements.in 文件

requirements.in
flask>=2.0.0

运行 uv pip compile requirements.in 将生成以下 requirements.txt 文件

requirements.txt
# This file was autogenerated by uv via the following command:
#    uv pip compile requirements.in
blinker==1.7.0
    # via flask
click==8.1.7
    # via flask
flask==3.0.0
itsdangerous==2.1.2
    # via flask
jinja2==3.1.2
    # via flask
markupsafe==2.1.3
    # via
    #   jinja2
    #   werkzeug
werkzeug==3.0.1
    # via flask

但是,uv pip compile --resolution lowest requirements.in 将改为生成

requirements.in
# This file was autogenerated by uv via the following command:
#    uv pip compile requirements.in --resolution lowest
click==7.1.2
    # via flask
flask==2.0.0
itsdangerous==2.0.0
    # via flask
jinja2==3.0.0
    # via flask
markupsafe==2.0.0
    # via jinja2
werkzeug==2.0.0
    # via flask

发布库时,建议在持续集成中使用 --resolution lowest--resolution lowest-direct 单独运行测试,以确保与声明的下限兼容。

预发布版本处理

默认情况下,uv 将在依赖项解析期间接受预发布版本,在两种情况下

  1. 如果该包是直接依赖项,并且其版本说明符包括预发布说明符(例如,flask>=2.0.0rc1)。
  2. 如果包的所有发布版本都是预发布版本。

如果由于传递预发布版本而导致依赖项解析失败,uv 将提示使用 --prerelease allow 以允许所有依赖项的预发布。

或者,可以将传递依赖项添加为约束或直接依赖项(即在 requirements.inpyproject.toml 中),并使用预发布版本说明符(例如,flask>=2.0.0rc1)来选择加入对该特定依赖项的预发布支持。

预发布版本很难建模,并且是其他打包工具中错误的常见来源。uv 的预发布处理是有意限制的,并且需要用户选择加入预发布,以确保正确性。

有关更多详细信息,请参阅预发布兼容性

多版本解析

在通用解析期间,一个包可能会在同一个锁定文件中多次列出,具有不同的版本或 URL,因为不同的平台或 Python 版本可能需要不同的版本。

--fork-strategy 设置可用于控制 uv 如何在 (1) 最小化所选版本的数量和 (2) 为每个平台选择最新可能版本之间进行权衡。前者导致跨平台的一致性更高,而后者导致在可能的情况下使用较新的包版本。

默认情况下 (--fork-strategy requires-python),uv 将优化为为每个支持的 Python 版本的每个包选择最新版本,同时最小化跨平台的所选版本数量。

例如,在解决 Python 需求为 >=3.8numpy 时,uv 将选择以下版本

numpy==1.24.4 ; python_version == "3.8"
numpy==2.0.2 ; python_version == "3.9"
numpy==2.2.0 ; python_version >= "3.10"

此解析反映了 NumPy 2.2.0 及更高版本至少需要 Python 3.10,而早期版本与 Python 3.8 和 3.9 兼容。

--fork-strategy fewest 下,uv 将改为最小化每个包的所选版本数量,优先选择与更广泛的支持的 Python 版本或平台兼容的旧版本。

例如,在上面的场景中,uv 将为所有 Python 版本选择 numpy==1.24.4,而不是升级到 Python 3.9 的 numpy==2.0.2 和 Python 3.10 及更高版本的 numpy==2.2.0

依赖约束

与 pip 类似,uv 支持约束文件 (--constraint constraints.txt),这些文件缩小了给定包的可接受版本集。约束文件类似于 requirements 文件,但仅列为约束不会导致将包包含到解析中。相反,约束仅在请求的包已作为直接或传递依赖项拉入时才生效。约束对于减少传递依赖项的可用版本范围很有用。它们还可以用于使解析与其他一些已解析的版本集保持同步,而不管两个之间重叠的包是什么。

依赖覆盖

依赖项覆盖允许通过覆盖包声明的依赖项来绕过不成功或不需要的解析。对于您知道依赖项与包的特定版本兼容的情况,尽管元数据另有说明,覆盖是一种有用的最后手段。

例如,如果传递依赖项声明需求 pydantic>=1.0,<2.0,但确实适用于 pydantic>=2.0,则用户可以通过在覆盖中包含 pydantic>=1.0,<3 来覆盖声明的依赖项,从而允许解析器选择较新版本的 pydantic

具体来说,如果在覆盖中包含 pydantic>=1.0,<3,uv 将忽略所有声明的 pydantic 需求,并将其替换为覆盖。在上面的示例中,pydantic>=1.0,<2.0 需求将被完全忽略,而将被替换为 pydantic>=1.0,<3

虽然约束只能减少包的可接受版本集,但覆盖可以扩展可接受版本集,从而为错误的上限版本边界提供了一个逃生舱口。与约束一样,覆盖不会添加包的依赖项,并且仅在直接或传递依赖项中请求该包时才生效。

pyproject.toml 中,使用 tool.uv.override-dependencies 定义覆盖列表。在 pip 兼容的界面中,可以使用 --override 选项传递与约束文件格式相同的文件。

如果为同一包提供了多个覆盖,则必须使用标记来区分它们。如果一个包具有带有标记的依赖项,则在使用覆盖时会无条件地替换它 — 标记的评估结果为真还是假都无关紧要。

依赖元数据

在解析期间,uv 需要解析它遇到的每个包的元数据,以便确定其依赖项。此元数据通常作为包索引中的静态文件提供;但是,对于仅提供源码分发的包,元数据可能无法提前获得。

在这种情况下,uv 必须构建包以确定其元数据(例如,通过调用 setup.py)。这可能会在解析期间引入性能损失。此外,它提出了包可以在所有平台上构建的要求,这可能不是真的。

例如,您可能有一个包应该仅在 Linux 上构建和安装,但在 macOS 或 Windows 上无法成功构建。虽然 uv 可以为此场景构造一个完全有效的锁定文件,但这需要构建该包,这将在非 Linux 平台上失败。

tool.uv.dependency-metadata 表可用于提前为此类依赖项提供静态元数据,从而允许 uv 跳过构建步骤并改用提供的元数据。

例如,要提前为 chumpy 提供元数据,请在其 pyproject.toml 中包含其 dependency-metadata

[[tool.uv.dependency-metadata]]
name = "chumpy"
version = "0.70"
requires-dist = ["numpy>=1.8.1", "scipy>=0.13.0", "six>=1.11.0"]

这些声明旨在用于包提前声明静态元数据的情况,尽管它们对于需要禁用构建隔离的包也很有用。在这种情况下,提前声明包元数据可能比在解析包之前创建自定义构建环境更容易。

例如,您可以声明 flash-attn 的元数据,允许 uv 在不从源码构建包的情况下进行解析(这本身需要安装 torch

[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["flash-attn"]

[tool.uv.sources]
flash-attn = { git = "https://github.com/Dao-AILab/flash-attention", tag = "v2.6.3" }

[[tool.uv.dependency-metadata]]
name = "flash-attn"
version = "2.6.3"
requires-dist = ["torch", "einops"]

与依赖项覆盖类似,tool.uv.dependency-metadata 也可用于包元数据不正确或不完整,或者包在包索引中不可用的情况。虽然依赖项覆盖允许全局覆盖包的允许版本,但元数据覆盖允许覆盖特定包的声明元数据。

注意

tool.uv.dependency-metadata 中的 version 字段对于基于注册表的依赖项是可选的(如果省略,uv 将假定元数据适用于该包的所有版本),但对于直接 URL 依赖项(如 Git 依赖项)是必需的。

tool.uv.dependency-metadata 表中的条目遵循 Metadata 2.3 规范,尽管 uv 仅读取 nameversionrequires-distrequires-pythonprovides-extraversion 字段也被认为是可选的。如果省略,元数据将用于指定包的所有版本。

冲突依赖

uv 要求项目声明的所有可选依赖项(“extras”)彼此兼容,并且在创建锁定文件时一起解析所有可选依赖项。

如果在一个 extra 中声明的可选依赖项与另一个 extra 中的可选依赖项不兼容,uv 将无法解决项目的需求并显示错误。

为了解决这个问题,uv 支持声明冲突的 extras。例如,考虑两组彼此冲突的可选依赖项

pyproject.toml
[project.optional-dependencies]
extra1 = ["numpy==2.1.2"]
extra2 = ["numpy==2.0.0"]

如果您使用上述依赖项运行 uv lock,则解析将失败

$ uv lock
  x No solution found when resolving dependencies:
  `-> Because myproject[extra2] depends on numpy==2.0.0 and myproject[extra1] depends on numpy==2.1.2, we can conclude that myproject[extra1] and
      myproject[extra2] are incompatible.
      And because your project requires myproject[extra1] and myproject[extra2], we can conclude that your projects's requirements are unsatisfiable.

但是,如果您指定 extra1extra2 冲突,uv 将分别解析它们。在 tool.uv 部分指定冲突

pyproject.toml
[tool.uv]
conflicts = [
    [
      { extra = "extra1" },
      { extra = "extra2" },
    ],
]

现在,运行 uv lock 将成功。但请注意,现在您不能同时安装 extra1extra2

$ uv sync --extra extra1 --extra extra2
Resolved 3 packages in 14ms
error: extra `extra1`, extra `extra2` are incompatible with the declared conflicts: {`myproject[extra1]`, `myproject[extra2]`}

发生此错误是因为安装 extra1extra2 会导致将同一包的两个不同版本安装到同一环境中。

上述处理冲突 extras 的策略也适用于依赖项组

pyproject.toml
[dependency-groups]
group1 = ["numpy==2.1.2"]
group2 = ["numpy==2.0.0"]

[tool.uv]
conflicts = [
    [
      { group = "group1" },
      { group = "group2" },
    ],
]

与冲突 extras 的唯一区别是您需要使用 group 键而不是 extra

下限

默认情况下,uv add 将下限添加到依赖项,并且在使用 uv 管理项目时,如果直接依赖项没有下限,uv 将发出警告。

下限在“happy path”中并不关键,但对于存在依赖项冲突的情况很重要。例如,考虑一个需要两个包的项目,这些包具有冲突的依赖项。解析器需要检查两个包约束中所有版本的所有组合 — 如果所有组合都冲突,则会报告错误,因为依赖项不可满足。如果没有下限,解析器可以(并且通常会)回溯到包的旧版本。这不仅因为速度慢而存在问题,而且包的旧版本通常无法构建,或者解析器最终可能会选择一个足够旧的版本,以至于它不依赖于冲突的包,但也不适用于您的代码。

在编写库时,下限尤其重要。重要的是声明您的库使用的每个依赖项的最低版本,并验证边界是否正确 — 使用--resolution lowest--resolution lowest-direct 进行测试。否则,用户可能会收到您的库的依赖项的旧的、不兼容的版本,并且该库将出现意外错误。

可重现的解析

uv 支持 --exclude-newer 选项,用于将解析限制为在特定日期之前发布的发行版,从而允许在不考虑新包发布的情况下重现安装。日期可以指定为 RFC 3339 时间戳(例如,2006-12-02T02:07:43Z)或与您的系统配置的时区格式相同的本地日期(例如,2006-12-02)。

请注意,包索引必须支持 PEP 700 中指定的 upload-time 字段。如果给定发行版不存在该字段,则该发行版将被视为不可用。PyPI 为所有包提供 upload-time

为确保可重现性,不可满足的解析的消息不会提及由于 --exclude-newer 标志而排除发行版 — 较新的发行版将被视为不存在。

注意

--exclude-newer 选项仅应用于从注册表读取的包(而不是 Git 依赖项等)。此外,当使用 uv pip 界面时,uv 不会降级以前安装的包,除非提供了 --reinstall 标志,在这种情况下,uv 将执行新的解析。

源码分发

PEP 625 规定包必须将源码分发作为 gzip tarball (.tar.gz) 存档分发。在此规范之前,还允许其他存档格式,需要支持这些格式以实现向后兼容性。uv 支持读取和提取以下格式的存档

  • gzip tarball (.tar.gz, .tgz)
  • bzip2 tarball (.tar.bz2, .tbz)
  • xz tarball (.tar.xz, .txz)
  • zstd tarball (.tar.zst)
  • lzip tarball (.tar.lz)
  • lzma tarball (.tar.lzma)
  • zip (.zip)

锁定文件版本控制

uv.lock 文件使用版本化的模式。模式版本包含在锁定文件的 version 字段中。

任何给定的 uv 版本都可以读取和写入具有相同模式版本的锁定文件,但会拒绝具有更大模式版本的锁定文件。例如,如果您的 uv 版本支持模式 v1,则 uv lock 如果遇到模式为 v2 的现有锁定文件,则会出错。

如果模式更新向后兼容,则支持模式 v2 的 uv 版本可能能够读取模式为 v1 的锁定文件。但是,这不能保证,如果 uv 遇到具有过时模式版本的锁定文件,它可能会退出并显示错误。

模式版本被认为是公共 API 的一部分,因此仅在次要版本中作为重大更改进行 bumped(参见Versioning)。因此,给定次要 uv 版本中的所有 uv 补丁版本都保证具有完全的锁定文件兼容性。换句话说,锁定文件可能仅在次要版本中被拒绝。

锁定文件的 revision 字段用于跟踪对锁定文件的向后兼容更改。例如,向发行版添加新字段。对修订版的更改不会导致旧版本的 uv 出错。

了解更多

有关解析器内部结构的更多详细信息,请参阅解析器参考文档。