Python 项目迁移到 uv:经验小结与可复用工作流
Python 项目迁移到 uv:经验小结与可复用工作流
一、核心知识点
1. Python 版本:requires-python 与 .python-version 是两件不同的事
1 | [project] |
1 | uv python pin 3.12 # 生成 .python-version 并建议提交进仓库 |
- requires-python:声明项目支持的版本范围,同时决定
uv lock做通用解析(universal resolution)时覆盖的版本区间——区间越窄,锁文件里为不同 Python 版本保留的 wheel 记录就越少。 - .python-version:决定
uv run、uv sync等命令创建.venv时用哪个本地解释器,不影响uv.lock的解析范围。
uv 没有 [tool.uv] python-version 这个配置项,不要混用。
不同命令对 Python 版本的选择规则不同:
| 命令 | 是否读取 requires-python |
|---|---|
uv add / uv sync / uv run |
会。没有 .python-version 时,在满足 requires-python 的本机/uv 管理的解释器中选第一个匹配的,找不到会自动下载 |
uv pip install |
不会。走 pip 兼容路径:用当前激活的虚拟环境,或当前/父目录下的 .venv(不管是否满足 requires-python),都没有则报错,需 --python / --system 显式指定 |
结论:只用 uv add/uv sync 时,只写 requires-python 够用;混用 uv pip install(比如下面第 7 点的兜底方案)时,建议先跑 uv sync/uv venv 建好 .venv,或者把 .python-version 也提交进仓库,避免命令之间用到不同解释器。
2. URL 依赖必须声明为直接依赖
uv 不允许 URL 依赖仅作为传递依赖存在。如果包 A 间接依赖了一个 URL 包 B,必须在 pyproject.toml 中:
- 在
dependencies列表里显式声明 B - 在
[tool.uv.sources]中配置 B 的 URL
1 | [project] |
注意:[tool.uv.sources] 只对已经出现在 dependencies(或 dev 依赖组)里的包名生效,先声明依赖、再配置来源,顺序不能反。
3. uv sync 与 uv add 的职责不同
| 命令 | 行为 |
|---|---|
uv sync |
根据 pyproject.toml 检查或更新 uv.lock,再让项目环境与锁文件一致;使用 --frozen 时不检查或更新锁文件 |
uv add -r |
把 requirements 文件中的依赖写入 pyproject.toml,并重新解析锁文件;导入过程中会暴露依赖声明、来源和版本冲突 |
如果项目已有一个旧的 uv.lock,一次普通的 sync 可能只是沿用或更新现有锁文件;迁移 requirements.txt 时仍应通过 uv add -r(必要时配合约束文件)重新导入并验证依赖声明。
4. 第三方包的内部 URL 依赖导致冲突
当依赖的第三方包在其自身元数据里通过 URL 声明了某个依赖,而你在 pyproject.toml 里也为同一个包声明了 URL 或版本,只要两边不完全一致(版本号、URL、hash 不同),uv 就会认为同一个包有两个不同来源,报 URL 冲突。若两边完全一致,通常只需按第 2 点显式声明即可。
这类冲突根源在第三方包内部,无法修改其元数据。解决思路:
- 确保声明的版本/URL 与第三方包内部要求的完全一致
- 若仍无法解决,先修复或重新发布上游包;短期内可用
uv pip install -r requirements.txt绕过项目解析直接安装(见第 7 点)
5. 版本不锁定导致解析不稳定
requirements.txt 中的关键包(尤其是内部私有包)务必锁定精确版本:
1 | # 不推荐 |
6. uv.lock 是跨平台锁文件,版本范围过宽可能导致文件变大
uv.lock 默认是跨平台、跨 Python 版本的通用锁文件,会记录不同环境标记下可能使用的候选分发包。requires-python 区间越宽,通常需要保留的版本和 wheel 记录越多,体积也可能越大。
修复:先根据项目实际支持范围收窄 requires-python(见第 1 点),然后重新生成锁文件:
1 | uv lock |
--python 3.12 只指定本次命令使用的 Python 解释器,不等于把锁文件限制为 3.12;真正决定项目支持范围的是 requires-python。如果项目只支持 Python 3.12,可使用 requires-python = ">=3.12,<3.13"。
7. 实用降级方案(仅用于过渡期)
1 | uv add -r requirements.txt --frozen # 跳过锁文件更新;仅用于临时导入依赖声明 |
uv add -r requirements.txt --frozen 的含义是跳过锁文件更新,不是生成一个可信的 uv.lock;如果项目没有现成锁文件,不能把它当作正常迁移方案。uv pip install -r requirements.txt 则完全绕过项目锁文件,适合临时安装或继续维护 requirements 工作流。两者都只适合过渡期,不建议作为项目终态。用 uv pip install 前确认 .venv 已按 requires-python 建好(见第 1 点表格)。
8. 多个 –index-url / –extra-index-url / –trusted-host 的迁移
原 requirements.txt:
1 | --index-url http://xxo-private-pip.vivo.lan:9090 |
转换为 pyproject.toml:
1 | [[tool.uv.index]] |
--trusted-host 不是 [[tool.uv.index]] 中的 trusted = true。对于 HTTP 或证书不受信任的内部源,应在项目配置中显式允许对应主机:
1 | [tool.uv] |
能为内部源配置有效 TLS 证书时,应优先修复证书,而不是长期依赖 allow-insecure-host。
注意:default = true 有特殊语义——它会禁用 PyPI 默认索引,并把该索引放到已配置索引的最低优先级;它不是“保持 requirements.txt 中 --index-url 的原始顺序”。如果需要严格控制某个包的来源,应使用 explicit = true 配合 [tool.uv.sources]。
**关键机制:uv 默认 index-strategy = "first-index"**(不同于 pip)——按索引定义顺序查找,包名一旦在某个索引找到,就只从这个索引解析该包的候选版本,不跨索引合并同名包版本。这是为了防止依赖混淆攻击,但迁移时容易踩坑:索引顺序会影响实际生效优先级,如果内部关键包在排序靠前的索引里版本滞后,可能导致解析失败(见第 9 点的实际案例)。
加固建议:给必须来自内部源的关键包用 explicit + tool.uv.sources 显式 pin,不完全依赖顺序:
1 | [tool.uv.sources] |
requirements.txt 里保留这些行的注意事项:uv add -r 可以临时读取 --index-url/--extra-index-url,但这只是本次命令的参数,不会自动写进 pyproject.toml;uv sync/uv lock/uv run 也不会从 requirements.txt 继承这些配置。--trusted-host 应改为 [tool.uv] allow-insecure-host,而不是写成索引字段。正确做法是把索引配置迁移进 pyproject.toml 的 [[tool.uv.index]];[tool.uv.pip] 只影响 uv pip 系列子命令,不影响项目接口的 uv add/uv sync/uv lock。迁移完成后,requirements.txt 只保留纯依赖列表,或直接删除并以 uv.lock 作为项目锁文件。
9. 实际案例:精确版本在优先索引里不存在
报错:
1 | x No solution found when resolving dependencies: |
原因:requirements.txt 把 psutil 锁死成精确版本 psutil==6.1.0。排序靠前的索引(很可能是内部 PyPI 镜像/代理)也代理了 psutil 这个包名,但同步不及时、没有 6.1.0 这个具体版本。按 first-index 策略,uv 在这个索引找到包名后就不再去后面的索引找其他版本,即使后面的索引真的有 6.1.0。
方案一(简单粗暴,有代价):全局关闭 first-index 防护
1 | [tool.uv] |
会跨所有索引找最高版本,解决问题,但对所有包、所有索引生效,如果索引里混了不完全可信的公网镜像,存在依赖混淆风险。适合”几个索引本来就是同一批人维护、互相信任”的场景。
方案二(推荐,更精确):只 pin 这一个包的来源,不动全局策略
1 | [tool.uv.sources] |
只有 psutil 从指定索引解析,其余包仍受 first-index 防护。后续再遇到同类”某包在镜像源版本滞后”的问题,可以用同样的模式逐个 pin。
二、常见报错 → 处理方式对照表
| 报错特征 | 根因 | 处理方式 |
|---|---|---|
Ignoring unsupported option ... --trusted-host |
项目接口不会把 requirements.txt 中的 --trusted-host 持久化为项目配置 |
改为 [tool.uv] allow-insecure-host = ["host"];优先修复 HTTPS 证书 |
URL dependencies must be expressed as direct requirements |
某包的传递依赖是裸 URL | 把该 URL 提升为直接依赖 + [tool.uv.sources] |
| 同一个包报”两个不同来源”冲突 | 你声明的版本/URL 和第三方包内部要求的不完全一致 | 核对版本号/hash,改到完全一致;改不了就走 --frozen 或 uv pip install 兜底 |
uv add 报错但 uv sync 能跑 |
项目可能沿用了已有锁文件,迁移导入时则触发了新的依赖解析 | 重新检查 pyproject.toml、依赖来源和约束;不要把 sync 成功当作迁移完成 |
uv.lock 体积异常大 |
requires-python 区间过宽,通用解析需要覆盖更多环境 |
收窄 requires-python 后执行 uv lock 重新生成 |
| Python 版本没有如预期被选中 | 没有 .python-version,或 uv pip 使用了另一个解释器/虚拟环境 |
uv python pin;对 uv pip 使用 --python,或改用项目接口的 uv add/uv sync |
No solution found,提示某包”found on X, but not at requested version” |
精确锁定的版本不在 first-index 命中的索引上,后面索引虽有但被跳过 | 全局 index-strategy = "unsafe-best-match"(图省事),或 tool.uv.sources 精确 pin 该包索引(更安全) |
三、可复用迁移工作流
阶段一:迁移前摸底(不动手改任何文件)
- 跑一遍现有安装命令,记录所有非默认 PyPI 来源:私有源地址、
--trusted-host、--extra-index-url、裸 wheel URL 依赖。 - 导出当前环境精确版本快照做迁移后比对:
pip freeze > baseline-freeze.txt(不进版本库)。 - 确定目标 Python 版本区间——是项目实际验证过能跑的版本,不是”最新版”。
阶段二:搭骨架
- 初始化项目文件:
uv init --no-readme(已有 pyproject.toml 则手动补[project]表)。 - 写
requires-python,收窄成单一版本区间。 uv python pin 3.12固定本地解释器,生成.python-version并提交仓库。- 按第一部分第 8 点,把
--index-url/--extra-index-url转成[[tool.uv.index]],把--trusted-host转成[tool.uv] allow-insecure-host。
阶段三:迁移依赖
- 如果只有一个已经锁定精确版本的 requirements.txt,直接执行
uv add -r requirements.txt;如果同时有未锁定的 requirements.in 和旧的 requirements.txt 快照,则执行uv add -r requirements.in -c requirements.txt,尽量避免迁移时发生无意升级。 - 按报错类型对照表逐类修复,不要零散地一条条改。
- 重复第 8 步,直到能跑通,或收敛到”仅剩内部包 URL/版本冲突”——这类问题不要死磕,转阶段五兜底。
阶段四:验证与固化关键包
对已知会反复出问题的内部包(元数据带 URL 依赖的、多个索引都有同名包的),用 explicit + tool.uv.sources 显式 pin,不完全依赖索引顺序。
阶段五:兜底方案(仅用于过渡期,不作为终态)
uv add -r requirements.txt --frozen 或 uv pip install -r requirements.txt,见第一部分第 7 点。
阶段六:收尾验证
- 生成精简锁文件并提交:
1
2uv lock
git add pyproject.toml uv.lock .python-version - 用干净环境复现:对照
1
2
3# Unix/macOS:rm -rf .venv;Windows PowerShell:Remove-Item -Recurse -Force .venv
uv sync
uv run python -c "import sys; print(sys.version)"baseline-freeze.txt抽查关键包版本,确认没有静默升级/降级。 - 在 CI 或另一台干净机器上跑一遍同样的
uv sync,确认不依赖本机缓存或已存在的.venv——这是判断迁移是否真正可复用的关键测试。