跳转至

pippip-tools 的兼容性

uv 旨在直接替代常见的 pippip-tools 工作流程。

通俗来讲,其目的是让现有的 pippip-tools 用户切换到 uv 时,无需对其打包工作流程进行重大更改;并且在大多数情况下,将 pip install 替换为 uv pip install 应该就能“正常工作”。

然而,uv 并非要成为 pip 的完全复刻版,你偏离常见 pip 工作流程越远,就越有可能遇到行为上的差异。在某些情况下,这些差异可能是已知且有意为之的;在其他情况下,它们可能是实现细节导致的;还有些情况,它们可能是 bug。

本文概述了 uv 与 pip 之间已知的差异,并说明了原因、解决方法以及未来兼容性方面的意向声明。

配置文件和环境变量

uv 不会读取 pip 特有的配置文件或环境变量,例如 pip.confPIP_INDEX_URL

读取其他工具的配置文件和环境变量存在诸多弊端: 1. 这需要与目标工具在错误处理上保持完全兼容,因为用户最终会依赖于格式、解析器等方面的错误。 2. 如果目标工具以某种方式更改了格式,uv 也必须以相同方式进行更改。 3. 如果该配置以某种方式进行了版本控制,uv 需要知道用户期望使用目标工具的哪个版本。 4. 这会阻止 uv 引入目标工具中不存在的任何设置或配置,因为否则 pip.conf(或类似文件)将无法再与 pip 一起使用。 5. 这可能会导致用户困惑,因为 uv 会读取实际上并不影响其行为的设置,而且许多用户可能不期望 uv 读取其他工具的配置文件。

相反,uv 支持其自身的环境变量,例如 UV_INDEX_URL。uv 还支持在 uv.toml 文件或 pyproject.toml[tool.uv.pip] 部分进行持久化配置。更多信息,请参阅 配置文件

预发布版本兼容性

默认情况下,uv 在以下两种情况下会在依赖解析时接受预发布版本: 1. 如果该软件包是直接依赖项,并且其版本标记包含预发布说明符(例如 flask>=2.0.0rc1)。 2. 如果某个软件包的所有已发布版本都是预发布版本。

如果由于传递性预发布版本导致依赖解析失败,uv 会提示用户使用 --prerelease allow 重新运行,以允许所有依赖项使用预发布版本。

或者,你可以将传递性依赖项及其预发布说明符(例如 flask>=2.0.0rc1)添加到 requirements.in 文件中,以选择对该特定依赖项支持预发布版本。

总之,uv 需要预先知道解析器是否应该接受给定软件包的预发布版本。与此同时,pip 可能 会根据解析器遇到相关说明符的顺序,考虑传递性依赖项中的预发布标识符(#1641)。

预发布版本 极难建模,并且是打包工具中常见的错误来源。即使被视为参考实现的 pip,在处理预发布版本方面也存在一些未解决的问题(#12469#12470#40505 等)。uv 对预发布版本的处理 有意 进行了限制,并且 有意 要求用户选择使用预发布版本,以确保正确性。

未来,uv 可能 会支持传递性依赖项中的预发布标识符。然而,这可能取决于 Python 打包规范的演变。现有的 PEP 并未涵盖 “依赖解析”,而是侧重于单个版本说明符的行为。因此,在更广泛的打包生态系统中,关于预发布版本的正确和预期行为仍存在未解决的问题。

存在于多个索引上的软件包

在 uv 和 pip 中,用户都可以指定多个软件包索引,以从中搜索给定软件包的可用版本。然而,uv 和 pip 在处理存在于多个索引上的软件包时方式有所不同。

例如,假设一家公司在私有索引(--extra-index-url)上发布了内部版本的 requests,但默认情况下也允许从 PyPI 安装软件包。在这种情况下,私有版本的 requests 将与 PyPI 上公开的requests 产生冲突。

当 uv 在多个索引中搜索软件包时,它将按顺序遍历这些索引(优先使用 --extra-index-url 而不是默认索引),一旦找到匹配项就停止搜索。这意味着,如果一个软件包存在于多个索引上,uv 将把候选版本限制在第一个包含该软件包的索引中存在的版本。

与此同时,pip 将合并来自所有索引的候选版本,并从合并后的集合中选择最佳版本,不过它并不保证搜索索引的顺序,并且期望即使跨索引,软件包在名称和版本上也是唯一的。

uv 的行为是,如果一个软件包存在于内部索引上,它应该始终从内部索引安装,而绝不是从 PyPI 安装。这样做的目的是防止 “依赖混淆” 攻击,即攻击者在 PyPI 上发布一个与内部软件包同名的恶意软件包,从而导致安装的是恶意软件包而不是内部软件包。例如,可参考2022年12月的torchtriton 攻击

从 v0.1.39 版本开始,用户可以通过 --index-strategy 命令行选项或 UV_INDEX_STRATEGY 环境变量选择采用 pip 风格的多索引行为,该选项支持以下值: - first-index(默认值):在所有索引中搜索每个软件包,将候选版本限制在第一个包含该软件包的索引中存在的版本,优先使用 --extra-index-url 索引而不是默认索引 URL。 - unsafe-first-match:在所有索引中搜索每个软件包,但优先选择第一个有兼容版本的索引,即使其他索引上有更新的版本。 - unsafe-best-match:在所有索引中搜索每个软件包,并从合并后的候选版本集合中选择最佳版本。

虽然 unsafe-best-match 最接近 pip 的行为,但它使用户面临 “依赖混淆” 攻击的风险。

uv 还支持将软件包固定到特定索引(请参阅:索引),这样给定的软件包将始终从特定索引安装。

PEP 517 构建隔离

默认情况下,uv 使用 PEP 517 构建隔离(类似于 pip install --use-pep517),这遵循了 pypa/build 的做法,并且预计未来 pip 也会默认采用 PEP 517 构建方式(pypa/pip#9175)。

如果某个软件包因缺少构建时依赖项而安装失败,可以尝试使用该软件包的较新版本;如果问题仍然存在,可以考虑向软件包维护者提交问题,要求他们更新打包设置,以声明正确的 PEP 517 构建时依赖项。

作为一种应急措施,你可以预先安装软件包的构建依赖项,然后使用 --no-build-isolation 运行 uv pip install,如下所示:

uv pip install wheel && uv pip install --no-build-isolation biopython==1.77

有关已知在 PEP 517 构建隔离下会安装失败的软件包列表,请参阅#2252

传递性 URL 依赖

虽然 uv 对 URL 依赖(例如 ruff @ https://...)提供原生支持,但在处理 传递性 URL 依赖方面,它与 pip 有两点不同。

首先,uv 假定非 URL 依赖不会在解析过程中引入 URL 依赖。换句话说,它假定从注册中心获取的依赖本身并不依赖于 URL。如果非 URL 依赖 确实 引入了 URL 依赖,uv 将在解析过程中拒绝该 URL 依赖。(请注意,PyPI 不允许已发布的包依赖于 URL 依赖;其他注册中心可能限制没那么严格。)

其次,如果使用直接 URL 依赖定义了约束(--constraint)或覆盖(--override),并且受约束的包本身有直接 URL 依赖,那么如果该 URL 在输入需求集中的其他地方未被引用,uv 可能 会在解析过程中拒绝该传递性直接 URL 依赖。

如果 uv 拒绝了传递性 URL 依赖,最佳做法是在相关的 pyproject.tomlrequirement.in 文件中将该 URL 依赖作为直接依赖提供,因为上述约束不适用于直接依赖。

默认使用虚拟环境

uv pip installuv pip sync 默认设计为与虚拟环境配合使用。

具体来说,uv 始终会将包安装到当前激活的虚拟环境中,或者在当前目录或任何父目录中搜索名为 .venv 的虚拟环境(即使该虚拟环境未激活)。

这与 pip 不同,pip 在没有激活虚拟环境时会将包安装到全局环境中,并且不会搜索未激活的虚拟环境。

在 uv 中,你可以通过 --python /path/to/python 选项指定 Python 可执行文件的路径,或者使用 --system 标志,将包安装到非虚拟环境中,--system 标志会像 pip 一样,将包安装到 PATH 中找到的第一个 Python 解释器中。

换句话说,uv 反转了默认设置,需要显式选择安装到系统 Python 中,这可能会导致故障和其他复杂问题,并且只应在有限的情况下这样做。

更多内容,请参阅 "使用任意 Python 环境"

解析策略

对于给定的一组依赖说明符,通常不存在单一 “正确” 的要安装的软件包集合。相反,有许多有效的软件包集合都可以满足这些说明符。

pip 和 uv 都不会对将要安装的软件包的确切集合做出任何保证;仅保证解析过程是一致的、可确定的,并且符合说明符。因此,在某些情况下,pip 和 uv 会产生不同的解析结果;不过,这两种解析结果都应该同样有效。

例如,考虑以下情况:

requirements.in
starlette
fastapi

在撰写本文时,最新的 starlette 版本是 0.37.2,最新的 fastapi 版本是 0.110.0。然而,fastapi==0.110.0 也依赖于 starlette,并且引入了一个上限:starlette>=0.36.3,<0.37.0

如果解析器优先包含最新版本的 starlette,那么它就需要使用较旧版本的 fastapi,该版本不包含 starlette 的上限。实际上,这需要回退到 fastapi==0.1.17

requirements.txt
# 此文件由 uv 通过以下命令自动生成:

#    uv pip compile requirements.in
annotated-types==0.6.0
    # 通过 pydantic
anyio==4.3.0
    # 通过 starlette
fastapi==0.1.17
idna==3.6
    # 通过 anyio
pydantic==2.6.3
    # 通过 fastapi
pydantic-core==2.16.3
    # 通过 pydantic
sniffio==1.3.1
    # 通过 anyio
starlette==0.37.2
    # 通过 fastapi
typing-extensions==4.10.0
    # 通过
    #   pydantic
    #   pydantic-core

或者,如果解析器优先包含最新版本的 fastapi,那么它就需要使用较旧版本的 starlette,该版本要满足上限要求。实际上,这需要回退到 starlette==0.36.3

requirements.txt
# 此文件由 uv 通过以下命令自动生成:

#    uv pip compile requirements.in
annotated-types==0.6.0
    # 通过 pydantic
anyio==4.3.0
    # 通过 starlette
fastapi==0.110.0
idna==3.6
    # 通过 anyio
pydantic==2.6.3
    # 通过 fastapi
pydantic-core==2.16.3
    # 通过 pydantic
sniffio==1.3.1
    # 通过 anyio
starlette==0.36.3
    # 通过 fastapi
typing-extensions==4.10.0
    # 通过
    #   fastapi
    #   pydantic
    #   pydantic-core

当 uv 的解析结果与 pip 出现不理想的差异时,这通常表明指定符过于宽松,用户应考虑收紧指定符。例如,对于 starlettefastapi,用户可以要求 fastapi>=0.110.0

pip check

目前,uv pip check 将显示以下诊断信息: - 某个软件包没有 METADATA 文件,或者 METADATA 文件无法解析。 - 某个软件包的 Requires-Python 与正在运行的解释器的 Python 版本不匹配。 - 某个软件包依赖的软件包未安装。 - 某个软件包依赖的软件包已安装,但版本不兼容。 - 虚拟环境中安装了某个软件包的多个版本。

在某些情况下,uv pip check 会显示 pip check 未显示的诊断信息,反之亦然。例如,与 uv pip check 不同,当当前环境中安装了某个软件包的多个版本时,pip check 不会发出警告。

--useruser 安装方案

uv 不支持 --user 标志,该标志基于 user 安装方案安装软件包。相反,我们建议使用虚拟环境来隔离软件包安装。

此外,如果 pip 检测到用户对目标目录没有写入权限(在某些系统中安装到系统 Python 时就是这种情况),它将回退到 user 安装方案。uv 不会实现任何此类回退。

更多信息,请参阅 #2077

--only-binary 强制规则

--only-binary 参数用于将安装限制为预构建的二进制分发包。 当提供 --only-binary :all: 时,pip 和 uv 都将拒绝从 PyPI 及其他仓库构建源分发包。

但是,当依赖项以直接 URL 的形式提供时(例如 uv pip install https://...),pip 不会强制实施 --only-binary,并且会为所有此类包构建源分发包。

与此同时,uv 会对直接 URL 依赖项强制实施 --only-binary,但有一个例外:给定 uv pip install https://... --only-binary flask,如果 uv 无法提前推断出包名,它将构建给定 URL 处的源分发包,因为在这种情况下,uv 在不构建元数据的情况下无法确定该包是否 “允许”。

即使提供了 --only-binary,pip 和 uv 都允许构建并安装可编辑的依赖项。例如,uv pip install -e . --only-binary :all: 是允许的。

--no-binary 强制规则

--no-binary 参数用于将安装限制为源分发包。当提供 --no-binary 时,uv 将拒绝安装预构建的二进制分发包,但会复用本地缓存中已有的任何二进制分发包。

此外,与 pip 不同的是,当提供 --no-binary 时,uv 的解析器仍将从预构建的二进制分发包中读取元数据。

manylinux_compatible 强制机制

PEP 600 描述了一种机制,通过该机制,Python 发行版可以通过在 _manylinux 标准库模块上定义 manylinux_compatible 函数,选择不兼容 manylinux

uv 遵循 manylinux_compatible,但仅针对当前的 glibc 版本进行测试,并全局应用 manylinux_compatible 的返回值。

换句话说,如果 manylinux_compatible 返回 True,uv 会将系统视为与 manylinux 兼容;如果返回 False,uv 会将系统视为与 manylinux 不兼容,而不会针对每个 glibc 版本调用 manylinux_compatible

这种方法并非对规范的完整实现,但与常见的通用 manylinux_compatible 实现(如no-manylinux)兼容:

from __future__ import annotations
manylinux1_compatible = False
manylinux2010_compatible = False
manylinux2014_compatible = False


def manylinux_compatible(*_, **__):  # PEP 600
    return False

字节码编译

pip 不同,uv 默认在安装过程中不会将 .py 文件编译为 .pyc 文件(即 uv 不会创建或填充 __pycache__ 目录)。要在安装过程中启用字节码编译,可以在 uv pip installuv pip sync 命令中传递 --compile-bytecode 标志,或者将 UV_COMPILE_BYTECODE 环境变量设置为 1

在工作流程中,跳过字节码编译可能不太理想;例如,我们建议在 Docker 构建 中启用字节码编译,以缩短启动时间(代价是增加构建时间)。

由于字节码编译会抑制 Python 解释器发出的各种警告,在极少数情况下,你可能会在运行通过 uv 安装的 Python 代码时看到 SyntaxWarningDeprecationWarning 消息,而使用 pip 安装时则不会出现这些消息。这些是有效的警告,但通常会被字节码编译过程隐藏,可以忽略、在上游修复,或者通过在 uv 中启用字节码编译来同样地抑制这些警告。

严格性和规范执行

uv 往往比 pip 更严格,并且经常会拒绝安装 pip 可以安装的软件包。例如,uv 会拒绝包含无效 URL 片段的 HTML 索引(参见:PEP 503),而 pip 会忽略此类片段。

在某些情况下,对于已知存在特定规范合规性问题的流行软件包,uv 会采取宽松的处理方式。

如果 uv 因为违反规范而拒绝安装 pip 可以安装的软件包,最佳做法是首先尝试安装该软件包的较新版本;如果失败,则向软件包维护者报告该问题。

pip 命令行选项和子命令

uv 并不支持 pip 的全部命令行选项和子命令,但支持其中很大一部分。

缺失的选项和子命令会根据用户需求和实现复杂度确定优先级,并且通常会在各个问题中进行跟踪。例如: - --trusted-host - --user

如果遇到缺失的选项或子命令,请在问题跟踪器中搜索,查看是否已有相关报告,如果没有,可以考虑新开一个问题。欢迎对现有问题点赞以表明您的关注。

注册中心认证

uv 不支持 pip--keyring-providerautoimport 选项。目前仅支持 subprocess 选项。

pip 不同,uv 默认不启用密钥环认证。

pip 不同,uv 不会等到请求返回 HTTP 401 状态码时才去查找认证信息。uv 会将认证信息附加到所有针对有可用凭证主机的请求中。

egg 支持

uv 不支持 pip 中被视为旧版或已弃用的功能。例如,uv 不支持 .egg 风格的分发包。

不过,uv 对以下两种情况有部分支持:(1).egg-info 风格的分发包(偶尔会在 Docker 镜像和 Conda 环境中看到);(2)旧版可编辑的 .egg-link 风格的分发包。

具体来说,uv 不支持安装新的 .egg-info.egg-link 风格的分发包,但在解析过程中会识别任何现有的此类分发包,使用 uv pip listuv pip freeze 列出它们,并使用 uv pip uninstall 卸载它们。

构建约束条件

当通过 --constraint(或 UV_CONSTRAINT)提供约束条件时,uv 在解析构建依赖项(即构建源发行版)时不会应用这些约束条件。相反,构建约束条件应通过专用的 --build-constraint(或 UV_BUILD_CONSTRAINT)设置来提供。

与此同时,pip 在通过 PIP_CONSTRAINT 指定约束条件时,会将其应用于构建依赖项,但在通过命令行中的 --constraint 提供约束条件时则不会。

例如,为确保使用 setuptools 60.0.0 来构建任何对 setuptools 有构建依赖的软件包,应使用 --build-constraint,而不是 --constraint

pip compile 默认设置

pip compilepip-tools 的默认行为存在一些细微但值得注意的差异。

默认情况下,uv 不会将编译后的需求写入输出文件。相反,uv 要求用户使用 -o--output-file 选项显式指定输出文件。

默认情况下,uv 在输出编译后的需求时会去除额外项。换句话说,uv 默认使用 --strip-extras,而 pip-compile 默认使用 --no-strip-extraspip-compile 计划在下一个主要版本(v8.0.0)中更改此默认设置,届时这两个工具都将默认使用 --strip-extras。要在 uv 中保留额外项,请将 --no-strip-extras 标志传递给 uv pip compile

默认情况下,uv 不会将任何索引 URL 写入输出文件,而 pip-compile 会输出任何与默认值(PyPI)不匹配的 --index-url--extra-index-url。要在输出文件中包含索引 URL,请将 --emit-index-url 标志传递给 uv pip compile。与 pip-compile 不同,当传递 --emit-index-url 时,uv 将包含所有索引 URL,包括默认索引 URL。

requires-python 强制规则

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

在根据 requires-python 说明符评估 Python 版本时,uv 会将候选版本截断为主要版本号、次要版本号和补丁版本号部分,忽略(例如)预发布和发布后标识符。

例如,声明 requires-python: >=3.13 的项目将接受 Python 3.13.0b1。虽然 3.13.0b1 并不严格大于 3.13,但省略预发布标识符后,它大于 3.13。

虽然这并不完全符合 PEP 440,但它与 pip 是一致的。

包优先级

给定一组需求,通常有多种可能的解决方案,解析器必须在它们之间做出选择。uv 的解析器和 pip 的解析器有不同的包优先级设置。虽然两个解析器都将用户提供的顺序作为其优先级之一,但 pip 有 uv 所没有的额外优先级。因此,与 pip 相比,uv 更有可能受到用户顺序变化的影响。

例如,uv pip install foo bar 会优先选择 foo 的较新版本而非 bar,这可能会导致与 uv pip install bar foo 不同的解析结果。同样,此行为也适用于 uv pip compile 输入文件中需求的排序。