解析
解析是指将需求列表转换为满足这些需求的软件包版本列表的过程。解析需要递归搜索软件包的兼容版本,确保满足所请求的需求,并且所请求软件包的需求相互兼容。
依赖项
大多数项目和软件包都有依赖项。依赖项是当前软件包正常运行所必需的其他软件包。软件包将其依赖项定义为 需求,大致是软件包名称和可接受版本的组合。当前项目定义的依赖项称为 直接依赖项。当前项目的每个依赖项所添加的依赖项称为 间接 或 传递依赖项。
注意
有关依赖项的详细信息,请参阅 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 1.0.0
)。这两个都是有效的解决方案,不同的解析算法可能会产生不同的结果。
平台标记
标记允许将表达式附加到需求上,以表明何时应使用该依赖项。例如 bar ; python_version < "3.9"
表示 bar
仅应安装在 Python 3.8 及更早版本上。
标记用于根据当前环境或平台调整软件包的依赖项。例如,可以使用标记按操作系统、CPU 架构、Python 版本、Python 实现等修改依赖项。
注意
有关标记的更多详细信息,请参阅 Python 打包文档中的环境标记部分。
标记对于解析很重要,因为它们的值会改变所需的依赖项。通常,Python 软件包解析器使用 当前 平台的标记来确定使用哪些依赖项,因为软件包通常是在当前平台上 安装。但是,对于 锁定 依赖项来说,这存在问题 —— 锁定文件仅适用于使用与创建锁定文件相同平台的开发人员。为了解决此问题,存在与平台无关的或 “通用” 的解析器。
特定平台解析
默认情况下,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
标志。生成的需求文件
将包含标记,以指示每个依赖项适用于哪个平台。
在通用解析过程中,如果不同平台需要不同版本,一个包可能会以不同版本或 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:
或者,避免解析替代的 Python 实现: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
对于声明对非最新平台的明确支持很有用,因为这通常需要回溯到这些软件包的最新发布版本之前。例如,要确保任何仅包含已构建发行版的软件包都支持英特尔架构的 macOS:
[tool.uv]
required-environments = [
"sys_platform == 'darwin' and platform_machine == 'x86_64'"
]
依赖项偏好
如果解析输出文件存在,即 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
文件:
运行 uv pip compile requirements.in
将生成以下 requirements.txt
文件:
# 此文件由 uv 通过以下命令自动生成:
# 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
则会生成:
此文件由 uv 通过以下命令自动生成:
uv pip compile requirements.in --resolution lowest
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` 运行测试,以确保与声明的下限兼容。
## 预发布版本处理
默认情况下,uv 在两种情况下会在依赖解析过程中接受预发布版本:
1. 如果包是直接依赖项,并且其版本说明符包含预发布说明符(例如,`flask>=2.0.0rc1`)。
1. 如果包的所有已发布版本都是预发布版本。
如果由于传递性预发布版本导致依赖解析失败,uv 将提示使用 `--prerelease allow` 以允许所有依赖项使用预发布版本。
或者,可以将传递性依赖项作为[约束](#dependency-constraints)或直接依赖项(即在 `requirements.in` 或 `pyproject.toml` 中)添加,并带有预发布版本说明符(例如,`flask>=2.0.0rc1`),以选择对该特定依赖项的预发布版本支持。
预发布版本[极难](https://pubgrub-rs-guide.netlify.app/limitations/prerelease_versions)建模,并且是其他打包工具中常见的错误来源。uv 的预发布版本处理是有意受限的,需要用户选择预发布版本以确保正确性。
有关更多详细信息,请参阅[预发布版本兼容性](../pip/compatibility.md#pre-release-compatibility)。
## 多版本解析
在通用解析过程中,由于不同平台或 Python 版本可能需要不同版本的包,同一个锁文件中可能会多次列出具有不同版本或 URL 的同一个包。
`--fork-strategy` 设置可用于控制 uv 如何在以下两者之间进行权衡:(1)尽量减少所选版本的数量;(2)为每个平台选择尽可能最新的版本。前者可提高跨平台的一致性,而后者则尽可能使用较新的包版本。
默认情况下(`--fork-strategy requires-python`),uv 将针对每个受支持的 Python 版本,优化选择每个包的最新版本,同时尽量减少跨平台所选版本的数量。
例如,在解析 `numpy` 且 Python 要求为 `>=3.8` 时,uv 将选择以下版本:
```txt
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
),该文件可缩小给定软件包可接受的版本范围。约束文件与需求文件类似,但仅作为约束列出并不会导致软件包被纳入解析范围。相反,只有当请求的软件包已作为直接或传递依赖项引入时,约束才会生效。约束对于缩小传递依赖项的可用版本范围很有用。它们还可用于使解析与其他一些已解析版本集保持同步,无论两者之间哪些软件包存在重叠。
依赖覆盖
依赖覆盖允许通过覆盖包声明的依赖项,绕过解析失败或不理想的情况。当你明确知道某个依赖与包的特定版本兼容,但元数据却显示不兼容时,覆盖是一种有用的最终手段。
例如,如果某个传递依赖声明需要 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 仅读取 name
、version
、requires-dist
、requires-python
和 provides-extra
。version
字段也被视为可选。如果省略,元数据将用于指定软件包的所有版本。
下限
默认情况下,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
字段。如果某个发行版不存在该字段,则该发行版将被视为不可用。PyPI 为所有软件包提供了 upload-time
字段。
为确保可重现性,对于无法满足的解析结果,相关消息不会提及因 --exclude-newer
标志而排除的发行版,较新的发行版将被视为不存在。
注意
--exclude-newer
选项仅适用于从注册表读取的软件包(与例如 Git 依赖项不同)。此外,在使用 uv pip
接口时,除非提供 --reinstall
标志,否则 uv 不会降级先前安装的软件包,在这种情况下,uv 将执行新的解析。
源发行版
《PEP 625》 规定,软件包必须以 gzip 压缩的 tar 包(.tar.gz
)存档形式分发源发行版。在该规范之前,出于向后兼容性考虑,也允许使用其他存档格式。uv 支持读取和提取以下格式的存档:
- gzip 压缩的 tar 包(
.tar.gz
、.tgz
) - bzip2 压缩的 tar 包(
.tar.bz2
、.tbz
) - xz 压缩的 tar 包(
.tar.xz
、.txz
) - zstd 压缩的 tar 包(
.tar.zst
) - lzip 压缩的 tar 包(
.tar.lz
) - lzma 压缩的 tar 包(
.tar.lzma
) - zip 压缩包(
.zip
)
了解更多
有关解析器内部原理的更多详细信息,请参阅 解析器参考文档。
锁文件版本控制
uv.lock
文件使用有版本的模式。模式版本包含在锁文件的 version
字段中。
任何特定版本的 uv 都可以读取和写入具有相同模式版本的锁文件,但会拒绝模式版本更高的锁文件。例如,如果您的 uv 版本支持模式 v1,uv lock
在遇到现有模式为 v2 的锁文件时将报错。
如果模式更新是向后兼容的,支持模式 v2 的 uv 版本 可能 能够读取模式为 v1 的锁文件。但是,这并不能保证,uv 在遇到模式版本过时的锁文件时可能会报错退出。
模式版本被视为公共 API 的一部分,因此只有在次要版本发布时才会提升,作为一种不兼容变更(请参阅 版本控制)。因此,给定 uv 次要版本内的所有 uv 补丁版本都保证具有完全的锁文件兼容性。换句话说,锁文件可能仅在次要版本之间被拒绝。