跳转至

解析

解析是将需求列表转换为满足这些需求的包版本的过程。解析过程需要递归地搜索包的兼容版本,确保请求的需求得到满足,并且请求的包的需求相互兼容。

依赖关系

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

注意

有关依赖关系的详细信息,请参见 依赖关系说明符页面(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 都只有一个版本,因此将使用这些版本。解析还必须包括传递依赖关系,因此必须选择一个版本的 libfoo 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 的锁定文件(uv.lock)使用通用解析创建,并且可以跨平台移植。这确保了无论操作系统、架构和 Python 版本如何,项目中所有工作的人都能确保依赖关系已被锁定。uv 锁定文件是通过 项目 命令(如 uv lockuv syncuv add)创建和修改的。

通用解析也可以通过 uv 的 pip 接口使用,即使用 --universal 标志的 uv pip compile。生成的需求文件将包含标记,以指示每个依赖项适用于哪个平台。

在通用解析过程中,如果不同平台需要不同版本的包,则同一个包可能会列出多个不同版本或 URL —— 标记将决定使用哪个版本。与平台特定解析相比,通用解析通常会更受限,因为我们需要考虑所有标记的需求。

在通用解析过程中,必须指定一个最低的 Python 版本。项目命令会从 pyproject.toml 中的 project.requires-python 获取最低要求的版本。当使用 uv 的 pip 接口时,使用 --python-version 选项提供版本值;否则,当前的 Python 版本将作为下限。例如,--universal --python-version 3.9 会执行适用于 Python 3.9 及以上版本的通用解析。

在通用解析过程中,所有选定的依赖版本必须与 pyproject.toml 中声明的 整个 requires-python 范围兼容。例如,如果项目的 requires-python>=3.8,则 uv 不允许任何版本的依赖项为仅支持 Python 3.9 或更高版本的依赖项,因为它们与 Python 3.8(项目支持范围的下限)不兼容。换句话说,项目的 requires-python 必须是所有依赖项的 requires-python 的子集。

在评估依赖项的 requires-python 范围时,uv 只考虑下限,完全忽略上限。例如,>=3.8, <4 会被视为 >=3.8

平台特定解析

默认情况下,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)或需求输出文件(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 的预发布版本处理是 故意 限制的,并且要求用户选择加入预发布版本支持,以确保正确性。

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

依赖约束

与 pip 类似,uv 支持约束文件(--constraint constraints.txt),用于缩小给定包的可接受版本范围。约束文件类似于需求文件,但单独列出作为约束不会导致包包含在解析中。相反,只有在请求的包已作为直接或传递依赖项被拉入时,约束才会生效。约束对于减少传递依赖项的可用版本范围非常有用。它们还可以用于将解析与某个已解决版本的集合保持同步,无论两个集合之间哪些包是重叠的。

依赖覆盖

依赖覆盖允许通过覆盖包声明的依赖项来绕过不成功或不希望的解析。覆盖是一个有用的最后手段,适用于你 知道 某个依赖项与特定版本的包兼容,尽管元数据显示相反的情况。

例如,如果一个传递依赖项声明了 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 也可以用于包的元数据不正确或不完整的情况,或当包在包索引中不可用时。虽然依赖覆盖允许全局覆盖包的允许版本,元数据覆盖则允许覆盖 特定包 的声明元数据。

Noe

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

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

下限

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

下限在“正常路径”中并不是至关重要,但在依赖冲突的情况下,它们非常重要。例如,考虑一个项目,它需要两个包,而这两个包之间存在冲突的依赖关系。解析器需要检查这两个包的所有版本组合,如果它们之间都存在冲突,则会报告错误,因为依赖项无法满足。如果没有下限,解析器可以(并且通常会)回溯到包的最旧版本。这不仅仅是因为这样做会变慢,旧版本的包往往无法构建,或者解析器可能会选择一个足够旧的版本,导致它不再依赖于冲突的包,但同时也无法与您的代码兼容。

在编写库时,下限特别关键。声明您的库支持的每个依赖项的最低版本,并验证这些版本的范围是否正确——可以通过测试 --resolution lowest--resolution lowest-direct 来完成。否则,用户可能会获得一个不兼容的旧版本库依赖项,并导致库出现意外的错误。

可复现的解析

uv 支持 --exclude-newer 选项,可以限制解析仅限于在特定日期之前发布的分发包,从而确保安装可复现,避免新包发布的影响。日期可以指定为 RFC 3339 时间戳(例如,2006-12-02T02:07:43Z)或本地日期,格式相同(例如,2006-12-02),并使用系统配置的时区。

需要注意的是,包索引必须支持 upload-time 字段,具体说明见 PEP 700。如果给定分发包没有此字段,该分发包将被视为不可用。PyPI 为所有包提供了 upload-time 字段。

为了确保可复现性,无法满足的解析错误信息不会提及因 --exclude-newer 标志而排除的分发包——新发布的分发包将被视为不存在。

Note

--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,当它遇到模式为 v2 的现有锁定文件时,uv lock 会报错。

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

模式版本被视为公共 API 的一部分,因此只有在次要版本更新时才会增加(请参阅 版本控制)。因此,在给定的次要 uv 版本中,所有 uv 修补版本都保证具有完全的锁定文件兼容性。换句话说,锁定文件只会在次要版本之间被拒绝。