3.7 解析

原文链接: https://docs.astral.sh/uv/concepts/resolution/

3.7 解析

解析是指把一组要求转换为满足这些要求的一组包版本的过程。解析需要递归搜索包的兼容版本,确保所请求的要求得到满足,并且被请求包的要求之间相互兼容。

依赖

大多数项目和包都有依赖。依赖是让当前包能够工作所必需的其他包。包把它的依赖定义为要求,大致是包名与可接受版本的组合。当前项目定义的依赖称为直接依赖。当前项目的每个依赖所引入的依赖称为间接依赖或传递依赖。

注意

关于依赖的细节,请参阅 Python 打包文档中的依赖说明符页面。

基本示例

为便于说明解析过程,考虑以下依赖:

  • 项目依赖 foo 和 bar。
  • 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。两个版本都没有依赖。

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

某些解析可能存在多个有效解。考虑以下依赖:

  • 项目依赖 foo 和 bar。
  • 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。两个版本都没有依赖。

在这个示例中,必须为 foo 和 bar 各选择一个版本;不过要确定选哪个版本,需要考虑 foo 和 bar 每个版本的依赖。foo 2.0.0 与 bar 2.0.0 无法一起安装,因为它们在 lib 的所需版本上冲突,因此解析器必须选择 foo 1.0.0(连同 bar 2.0.0)或 bar 1.0.0(连同 foo 2.0.0)。两者都是有效解,不同的解析算法可能给出任一结果。

平台标记

标记允许为 requirement 附加一个表达式,指示该依赖应在何时使用。例如 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 lock、uv sync 和 uv 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 求解:

1
2
3
4
5
6
# pyproject.toml
[tool.uv]
environments = [
    "sys_platform == 'darwin'",
    "sys_platform == 'linux'",
]

或者,避免为其他 Python 实现求解:

1
2
3
4
5
# pyproject.toml
[tool.uv]
environments = [
    "implementation_name == 'cpython'"
]

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

必需环境

在 Python 生态中,包可以发布为源码分发、构建分发(wheel)或两者兼有;但要安装一个包,必须有构建分发。如果某个包缺少构建分发,或缺少适用于当前平台或 Python 版本的构建分发(构建分发通常与平台相关),uv 会尝试从源码构建该包,然后安装生成的构建分发。

有些包(例如 PyTorch)发布构建分发,但省略源码分发。这类包只能在有可用构建分发的平台上安装。例如,如果某个包为 Linux 发布构建分发,但不为 macOS 或 Windows 发布,那么该包只能在 Linux 上安装。

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

默认情况下,uv 要求每个这类包至少包含一个与目标 Python 版本兼容的 wheel。required-environments 设置可用于确保解析结果包含特定平台的 wheel,或者在没有这类 wheel 时失败。该设置接受一组 PEP 508 环境标记。

environments 设置限制 uv 在解析依赖时会考虑的环境集合,而 required-environments 扩展 uv 在解析依赖时必须支持的平台集合。

例如 environments = ["sys_platform == 'darwin'"] 会把 uv 限制为只为 macOS 求解(忽略 Linux 和 Windows)。而 required-environments = ["sys_platform == 'darwin'"] 会要求任何没有源码分发的包都包含 macOS 的 wheel 才能安装(如果没有这样的 wheel 则会失败)。

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

1
2
3
4
5
# pyproject.toml
[tool.uv]
required-environments = [
    "sys_platform == 'darwin' and platform_machine == 'x86_64'"
]

要要求某个 macOS 版本,请在 platform_release 中使用它的 Darwin 内核版本。例如 macOS 15 使用 Darwin 24:

1
2
3
4
5
# pyproject.toml
[tool.uv]
required-environments = [
    "sys_platform == 'darwin' and platform_machine == 'arm64' and platform_release == '24.0.0'"
]

使用 == 要求在基线处得到覆盖。像 >= '24.0.0' 这样的范围可以由只支持更新版本的 wheel 满足。面向更新版本的 wheel 仍会保留在锁文件中。

最低 libc 版本

注意

minimum-libc-version 处于预览阶段。使用 --preview-features minimum-libc-version 或 preview-features = ["minimum-libc-version"] 可以消除警告。

环境标记不包含 libc 实现或版本。minimum-libc-version 设置指定 required-environments 中必须支持的最旧 libc 版本。

例如,要求在 x86-64 和 ARM64 Linux 上支持 glibc 2.31:

1
2
3
4
5
6
7
8
# pyproject.toml
[tool.uv]
preview-features = ["minimum-libc-version"]
required-environments = [
    "sys_platform == 'linux' and platform_machine == 'x86_64'",
    "sys_platform == 'linux' and platform_machine == 'aarch64'",
]
minimum-libc-version = { glibc = "2.31" }

在这种配置下,manylinux_2_17 wheel 满足 glibc 要求,而 manylinux_2_34 wheel 不满足。两者都会保留在锁文件和导出的哈希中,因此 glibc 版本更新的机器可以安装更新的 wheel。musl wheel 也会保留,但不满足 glibc 要求。省略某个 libc 实现意味着它不被要求,而不是被排除。

要同时要求 glibc 和 musl 支持,请为两者都设置版本:

1
minimum-libc-version = { glibc = "2.31", musl = "1.2" }

每个配置的 libc 版本都需要在 required-environments 中得到覆盖。如果某个必需环境没有兼容的 wheel 或可用的源码分发,uv 会尝试该包的其他版本。通用 Linux wheel(例如 linux_x86_64)不约束 libc,可以满足任一实现。

常见标记值

environments 和 required-environments 设置接受 PEP 508 环境标记。这些标记的值来自 Python 运行时(例如 sys.platform、platform.machine()、platform.system() 和 os.name)。

为便于快速参考,各平台最常见的标记值如下:

标记LinuxmacOSWindows
sys_platform'linux''darwin''win32'
platform_system'Linux''Darwin''Windows'
platform_machine(x86-64)'x86_64''x86_64''AMD64'
platform_machine(ARM64)'aarch64''arm64''ARM64'
os_name'posix''posix''nt'

注意

在 Windows 上,即使在 64 位系统上,sys_platform 也始终是 'win32'。

你可以通过运行下面的命令查看当前平台的这些值:

1
$ uvx python -c "import sysconfig; print(sysconfig.get_config_vars())"

依赖偏好

如果解析输出文件存在(即 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 -o requirements.txt 会生成如下 requirements.txt 文件:

# requirements.txt
# 该文件由 uv 通过以下命令自动生成:
#    uv pip compile requirements.in -o requirements.txt
blinker==1.7.0
    # 来自 flask
click==8.1.7
    # 来自 flask
flask==3.0.0
itsdangerous==2.1.2
    # 来自 flask
jinja2==3.1.2
    # 来自 flask
markupsafe==2.1.3
    # 来自
    #   jinja2
    #   werkzeug
werkzeug==3.0.1
    # 来自 flask

而 uv pip compile --resolution lowest requirements.in -o requirements.txt 会生成:

# requirements.txt
# 该文件由 uv 通过以下命令自动生成:
#    uv pip compile --resolution lowest requirements.in -o requirements.txt
click==7.1.2
    # 来自 flask
flask==2.0.0
itsdangerous==2.0.0
    # 来自 flask
jinja2==3.0.0
    # 来自 flask
markupsafe==2.0.0
    # 来自 jinja2
werkzeug==2.0.0
    # 来自 flask

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

预发布版本处理

默认情况下(if-necessary),uv 优先使用稳定版本而不是预发布版本,只有当所有满足当前约束的稳定候选都在解析过程中被拒绝时,才回退到预发布版本。

使用 --prerelease allow 可以为每个包考虑预发布版本而不优先稳定候选,使用 --prerelease disallow 则完全排除它们。

使用 --prerelease-package foo=allow 可以为特定包覆盖全局预发布策略。包级策略也可以在 [tool.uv] 中配置:

1
2
3
[tool.uv]
prerelease = "disallow"
prerelease-package = { foo = "allow", bar = "if-necessary" }

explicit 模式只对包含预发布标识的第一方 requirement 考虑预发布版本(优先稳定版本,仅在必要时回退到预发布),而对其他所有包禁用预发布版本。

更多细节请参阅预发布版本兼容性。

多版本解析

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

--fork-strategy 设置可用于控制 uv 如何在 (1) 最小化所选版本数量与 (2) 为每个平台选择尽可能新的版本之间权衡。前者带来更好的跨平台一致性,后者则尽可能使用更新的包版本。

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

例如,在为 numpy 做解析且 Python 要求为 >=3.8 时,uv 会选择以下版本:

1
2
3
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 选项传入与约束文件格式相同的文件。

默认情况下,覆盖适用于指定依赖的所有 requirement,包括直接 requirement。也可以使用内联表把覆盖限定到特定包版本的依赖:

1
2
3
4
5
[tool.uv]
override-dependencies = [
    "foo>1",
    { package = { name = "bar", version = "0.0.5" }, dependencies = ["foo>2"] },
]

在这个示例中,foo>1 是全局覆盖,而 foo>2 替换 bar==0.0.5 声明的 foo 要求。如果 bar 没有声明对 foo 的依赖,该限定覆盖会把它加上。bar 声明的其他依赖不变。package 中的 version 字段可以省略,从而把限定覆盖应用到 bar 的所有版本。特定版本的条目优先于适用于所有版本的条目,对于同一依赖,限定覆盖优先于全局覆盖。

限定覆盖目前只支持基于仓库的版本说明符。不支持直接 URL 和路径来源(包括 Git 来源)以及显式索引。

在 explicit 预发布模式下,任何限定覆盖中的显式预发布说明符都会为整个解析允许该包进行“稳定优先、预发布回退”的选择。同样,任何限定覆盖中精确固定到已撤回版本都会为整个解析让该包进入已撤回版本候选的选择,即使该范围未被选中。

如果为同一个包提供了多个覆盖,它们必须用标记加以区分。如果某个包有一个带标记的依赖,那么使用覆盖时它会无条件被替换 —— 标记求值为真还是假都无关紧要。

依赖排除

依赖排除会把包从依赖图中移除。默认情况下,排除适用于指定依赖的所有 requirement,包括直接 requirement:

1
2
[tool.uv]
exclude-dependencies = ["foo"]

也可以把排除限定到特定包版本的依赖:

1
2
3
4
[tool.uv]
exclude-dependencies = [
    { package = { name = "bar", version = "0.0.5" }, dependencies = ["foo"] },
]

在这个示例中,bar==0.0.5 声明的 foo 要求会被移除,而来自其他包的 foo 要求不变。package 中的 version 字段可以省略,从而把限定排除应用到 bar 的所有版本。特定版本的条目优先于适用于所有版本的条目。

限定排除可以与限定覆盖组合使用,把一个依赖替换为另一个:

1
2
3
4
5
6
7
[tool.uv]
override-dependencies = [
    { package = { name = "bar", version = "0.0.5" }, dependencies = ["pytorch-lightning"] },
]
exclude-dependencies = [
    { package = { name = "bar", version = "0.0.5" }, dependencies = ["lightning"] },
]

如果同一依赖在匹配范围内既被覆盖又被排除,排除优先。

依赖元数据

解析期间,uv 需要解析它所遇到的每个包的元数据,以确定其依赖。这些元数据通常作为静态文件存在于包索引中;不过对于只提供源码分发的包,元数据可能无法预先获得。

这种情况下,uv 必须构建该包才能确定其元数据(例如通过调用 setup.py)。这可能给解析带来性能损失。此外,它还要求该包能在所有平台上构建,而这可能并不成立。

例如,你可能有一个只应在 Linux 上构建和安装、但在 macOS 或 Windows 上无法成功构建的包。虽然 uv 可以为这种场景构造完全有效的锁文件,但这样做需要构建该包,而它会在非 Linux 平台上失败。

tool.uv.dependency-metadata 表可用于预先为此类依赖提供静态元数据,从而让 uv 跳过构建步骤并使用提供的元数据。

例如,要预先提供 chumpy 的元数据,请在 pyproject.toml 中包含它的 dependency-metadata:

1
2
3
4
[[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 没有声明静态元数据。通过预先声明 flash-attn 的元数据,uv 无需从源码构建该包(这本身需要安装 torch)即可解析它:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
[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 只读取 name、version、requires-dist、requires-python 和 provides-extra。version 字段同样被视为可选。省略时,该元数据会用于指定包的所有版本。

冲突的依赖

uv 要求项目声明的所有依赖彼此兼容,并在创建锁文件时一起解析所有依赖。这包括项目依赖、可选依赖(“extras”)和依赖组(开发依赖)。

如果某个 extra 中声明的依赖与另一个 extra 中的不兼容,uv 会解析项目要求失败并报错。例如,考虑两组相互冲突的可选依赖:

1
2
3
4
# pyproject.toml
[project.optional-dependencies]
extra1 = ["numpy==2.1.2"]
extra2 = ["numpy==2.0.0"]

如果用上面的依赖运行 uv lock,解析会失败:

1
2
3
4
5
$ 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.

为解决这个问题,uv 支持显式声明冲突。如果你指定 extra1 与 extra2 相互冲突,uv 会把它们分开解析。请在 tool.uv 段落中指定冲突:

1
2
3
4
5
6
7
8
# pyproject.toml
[tool.uv]
conflicts = [
    [
      { extra = "extra1" },
      { extra = "extra2" },
    ],
]

现在运行 uv lock 会成功。不过此时你无法同时安装 extra1 和 extra2:

1
2
3
$ 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]`}

这个错误会出现,是因为同时安装 extra1 和 extra2 会把同一个包的两个不同版本装进同一个环境。

上述处理冲突可选依赖的策略同样适用于依赖组:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# 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 要求所有工作区成员彼此兼容。同样,冲突也可以跨工作区成员声明。

例如,考虑下面的工作区:

1
2
3
4
5
6
# member1/pyproject.toml
[project]
name = "member1"

[project.optional-dependencies]
extra1 = ["numpy==2.1.2"]
1
2
3
4
5
6
# member2/pyproject.toml
[project]
name = "member2"

[project.optional-dependencies]
extra2 = ["numpy==2.0.0"]

要声明这些不同工作区成员中 extras 之间的冲突,请使用 package 键:

1
2
3
4
5
6
7
8
# pyproject.toml
[tool.uv]
conflicts = [
    [
      { package = "member1", extra = "extra1" },
      { package = "member2", extra = "extra2" },
    ],
]

某个工作区成员的项目依赖(即 project.dependencies)也可能与另一个成员的 extra 冲突,例如:

1
2
3
4
# member1/pyproject.toml
[project]
name = "member1"
dependencies = ["numpy==2.1.2"]
1
2
3
4
5
6
# member2/pyproject.toml
[project]
name = "member2"

[project.optional-dependencies]
extra2 = ["numpy==2.0.0"]

这种冲突也可以用 package 键声明:

1
2
3
4
5
6
7
8
# pyproject.toml
[tool.uv]
conflicts = [
    [
      { package = "member1" },
      { package = "member2", extra = "extra2" },
    ],
]

同样,某些工作区成员的项目依赖之间也可能冲突:

1
2
3
4
# member1/pyproject.toml
[project]
name = "member1"
dependencies = ["numpy==2.1.2"]
1
2
3
4
# member2/pyproject.toml
[project]
name = "member2"
dependencies = ["numpy==2.0.0"]

这种冲突也可以用 package 键声明:

1
2
3
4
5
6
7
8
# pyproject.toml
[tool.uv]
conflicts = [
    [
      { package = "member1" },
      { package = "member2" },
    ],
]

这些工作区成员将无法一起安装,例如工作区根不能定义:

1
2
3
4
# pyproject.toml
[project]
name = "root"
dependencies = ["member1", "member2"]

下界

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

在“顺利路径”下界并不关键,但在存在依赖冲突时它们很重要。例如,考虑一个需要两个包、而这两个包的依赖相互冲突的项目。解析器需要检查两个包约束范围内所有版本的组合 —— 如果全部冲突,就会报告错误,因为这些依赖无法满足。如果没有下界,解析器可以(而且往往会)一直回溯到某个包最旧的版本。这不仅因为慢而成为问题,旧版本包常常构建失败,或者解析器可能选中一个旧到不再依赖冲突包、但也不能与你的代码一起工作的版本。

在编写库时下界尤其关键。重要的是为每个依赖声明你的库能与之工作的最低版本,并验证这些下界是正确的 —— 用 --resolution lowest 或 --resolution lowest-direct 进行测试。否则,用户可能拿到你的库某个依赖的旧而不兼容的版本,导致库以意外错误失败。

可复现解析

uv 支持 --exclude-newer 选项,把解析限制在特定日期之前上传的分发,从而无论新包如何发布都能复现安装。该日期会与每个具体分发产物(即每个文件上传到包索引的时间)的上传时间比较,而不是包版本的发布日期。日期可以指定为 RFC 3339 时间戳(例如 2006-12-02T02:07:43Z),或该系统配置时区下的同格式本地日期(例如 2006-12-02)。

重要

包索引必须支持 PEP 700 规定的 upload-time 字段。如果某个分发没有该字段,该分发会被视为不可用,除非通过 --exclude-newer-package <package>=false 让该包退出该限制、或该索引配置了自己的 exclude-newer 值、或该索引通过 [[tool.uv.index]] exclude-newer = false 退出该限制。PyPI 为所有包提供 upload-time。

为确保可复现性,无法满足的解析的消息不会提到某些分发因 --exclude-newer 标志被排除 —— 更新的分发会被当作不存在。

注意

--exclude-newer 选项只应用于从仓库读取的包(不同于 Git 依赖等)。此外,使用 uv pip 接口时,除非提供 --reinstall 标志,uv 不会降级此前已安装的包;提供该标志时 uv 会执行一次新的解析。

pyproject.toml 中也支持该选项,例如:

1
2
3
# pyproject.toml
[tool.uv]
exclude-newer = "2006-12-02T02:07:43Z"

要禁用来自较低优先级配置来源的全局截止时间,请传入 --exclude-newer false、设置 UV_EXCLUDE_NEWER=false,或在更高优先级的配置文件中设置 exclude-newer = false。

在持久配置中指定时不允许使用本地日期时间。

也可以为特定包指定值,例如 --exclude-newer-package setuptools=2006-12-02,或:

1
2
3
# pyproject.toml
[tool.uv]
exclude-newer-package = { setuptools = "2006-12-02T02:07:43Z" }

包选项也接受 <package>=false 让某个包退出该限制,例如 --exclude-newer-package setuptools=false,或:

1
2
3
# pyproject.toml
[tool.uv]
exclude-newer-package = { setuptools = false }

这适合临时使用某个包的更新版本,或允许从不公布上传时间的索引解析某个包。

包级值优先于全局值和索引级值。

同样,单个索引可以覆盖全局截止时间:

1
2
3
4
5
6
7
8
# pyproject.toml
[tool.uv]
exclude-newer = "2006-12-02T02:07:43Z"

[[tool.uv.index]]
name = "internal"
url = "https://internal.example.com/simple"
exclude-newer = "7 days"

或者为该索引完全禁用它:

1
2
3
4
5
# pyproject.toml
[[tool.uv.index]]
name = "internal"
url = "https://internal.example.com/simple"
exclude-newer = false

这对不公布 upload-time 的私有索引很有用,也可用于对特定索引应用不同的可复现窗口,同时在其他地方保持全局行为。

依赖冷却期

uv 也支持依赖“冷却期”,即在解析时忽略比某个时长更新的包。这是改善安全态势的好方法:延迟包更新,直到社区有机会审查包的新版本。

该特性通过 exclude-newer 选项提供,语义相同。

通过指定时长而不是绝对值来定义依赖冷却期。可以使用“友好”时长(例如 24 hours、1 week、30 days)或 ISO 8601 时长(例如 PT24H、P7D、P30D)。

注意

时长不遵循本地时区语义,始终按一天 24 小时解析为固定的秒数(例如忽略夏令时切换)。不允许使用月和年这类日历单位,因为它们的长度本质上并不一致。

解析使用时长时,会相对当前时间计算一个时间戳。使用 uv.lock 文件时,该时间戳会包含在锁文件中。当前时间变化时 uv 不会更新锁文件;相反,uv 会在执行新解析时(例如使用 --upgrade 或 --refresh 时)更新时间戳。

pyproject.toml 中也支持该选项,例如:

1
2
3
# pyproject.toml
[tool.uv]
exclude-newer = "1 week"

也可以为特定包指定值,例如 --exclude-newer-package "setuptools=30 days",或:

1
2
3
4
# pyproject.toml
[tool.uv]
exclude-newer = "1 week"
exclude-newer-package = { setuptools = "30 days" }

源码分发

PEP 625 规定包必须把源码分发发布为 gzip tarball(.tar.gz)归档。在该规范之前,也允许其他归档格式,出于向后兼容需要仍要支持。

重要

从 0.12 起,uv 会拒绝不符合 [PEP 625] 扩展名要求的源码分发,.zip 归档除外 —— 出于向后兼容它仍被接受。

锁文件版本管理

uv.lock 文件使用带版本的 schema。schema 版本包含在锁文件的 version 字段中。

任何给定版本的 uv 都可以读写相同 schema 版本的锁文件,但会拒绝 schema 版本更高的锁文件。例如,如果你的 uv 版本支持 schema v1,uv lock 遇到 schema 为 v2 的现有锁文件时会报错。

支持 schema v2 的 uv 版本可能能读取 schema v1 的锁文件(如果该 schema 更新是向后兼容的)。不过这并不保证,uv 遇到 schema 版本过期的锁文件时可能报错退出。

schema 版本被视为公共 API 的一部分,因此只会在次要版本中作为破坏性变更递增(参见版本管理)。因此,给定 uv 次要版本内的所有补丁版本都保证具有完整的锁文件兼容性。换句话说,锁文件只可能跨次要版本被拒绝。

锁文件的 revision 字段用于跟踪对锁文件的向后兼容变更,例如为分发包添加新字段。revision 的变化不会导致旧版本 uv 报错。

了解更多

关于解析器内部实现的更多细节,请参阅解析器参考文档。

最后修改 September 25, 2026: 更新 (221c74c33)