文章列表
7 分钟阅读

uv 进阶:脚本、工作区与工具链实战


系列:开发环境管理

第 4 / 10 篇

  1. nvm 安装和使用教程
  2. uv 安装和使用教程
  3. pip 源的设置和使用
  4. uv 进阶:脚本、工作区与工具链实战
  5. pyenv 安装与使用指南
  6. pnpm 安装与使用指南
  7. Poetry 与 uv 对比:Python 项目管理怎么选
  8. direnv 使用指南:按目录自动加载环境变量
  9. Ruff 安装与使用指南
  10. GitHub CLI 使用指南:从 issue 到 PR 的命令行工作流

uv 安装和使用教程 讲了 uv 的基本用法:创建项目、加依赖、同步环境、管理 Python 版本。那篇覆盖了日常工作 80% 的场景。

这篇聊几个进阶话题——这些是我在实际项目中踩过坑之后发现真正提高效率的东西,希望你也用得上。

我本机跑的是 uv 0.11.x,下面聊到的功能在 0.10 之后基本都稳定了。

内联脚本:一个文件就是完整项目

Python 一直缺一个好用的”单文件脚本”方案。以前写个一次性脚本,要么污染全局环境,要么手动建 venv,要么用 pipx run(还得知道包名)。uv 把 PEP 723 的内联脚本元数据做得很好用——脚本自己声明依赖,uv 自动处理环境。

基础用法

.py 文件顶部用 TOML 声明依赖:

# /// script
# requires-python = ">=3.12"
# dependencies = [
# "httpx",
# "rich",
# ]
# ///
import httpx
from rich import print
resp = httpx.get("https://httpbin.org/json")
data = resp.json()
print(data)

然后直接跑:

Terminal window
uv run script.py

第一次运行 uv 会自动创建隔离环境、安装依赖、缓存结果。第二次跑几乎瞬间启动,因为环境已经就绪。

开发工具也可以声明

脚本不止能声明运行依赖,还能声明开发工具:

[tool.uv]
# /// script
# requires-python = ">=3.12"
# dependencies = ["pandas"]
# dev-dependencies = ["pytest", "ruff"]
# ///

然后:

Terminal window
uv run --dev ruff check script.py

这个方案特别适合数据分析脚本、一次性迁移脚本、自动化运维脚本——你把它扔给同事,他装上 uv 就能直接跑,不需要 requirements.txt、不需要 README 里写安装步骤。

什么时候用内联脚本,什么时候用项目

场景
一次性数据处理、临时迁移、自动化脚本内联脚本
需要测试、CI、多文件模块、部署到服务器完整项目

我的习惯:超过 200 行或者被别的文件 import,就升级成项目。之前的脚本搬进 src/ 也很方便。

工作区(Workspace):管理多个相关包

如果你的项目拆成了多个包——比如核心库 + CLI + Web 服务——以前得每个目录单独配 venv、单独 lock,版本容易飘。uv workspace 就是解决这个问题的,类似 npm 的 workspace 或者 Cargo 的 workspace。

创建 workspace

顶层 pyproject.toml

[tool.uv.workspace]
members = ["packages/*"]
[tool.uv]
dev-dependencies = ["pytest", "ruff"]

目录结构:

my-project/
├── pyproject.toml
├── packages/
│ ├── core/
│ │ ├── pyproject.toml
│ │ └── src/core/__init__.py
│ ├── cli/
│ │ ├── pyproject.toml
│ │ └── src/cli/__init__.py
│ └── web/
│ ├── pyproject.toml
│ └── src/web/__init__.py
└── uv.lock

子包的 pyproject.toml 各自声明自己的依赖,uv 会统一解析到根目录的一把 uv.lock

workspace 互引

子包之间可以直接引用。假设 cli 依赖 core

packages/cli/pyproject.toml
[project]
name = "my-cli"
dependencies = ["my-core"]
[tool.uv.sources]
my-core = { workspace = true }

workspace = true 告诉 uv 去 workspace 里找 my-core,而不是去 PyPI 下载。本地修改 core 的代码,cli 立刻看到,不需要任何 publish 步骤。

常用命令

Terminal window
uv workspace list # 列出当前 workspace 的所有成员
uv workspace dir # 显示 workspace 根目录
uv sync # 在根目录执行,会同步所有成员的依赖
uv run -p packages/cli my-cli --help # 在指定包的环境里运行命令

几个踩坑点

  1. 锁文件只有一把:workspace 所有包共享根目录的 uv.lock。如果两个子包声明了同一个包的不同版本约束,uv 会帮你解冲突,但这个文件必须提交到 Git。
  2. uv sync 在子目录也能跑:但实际用的还是根目录的锁文件。如果你在子目录 uv add,uv 会自动更新根目录的 pyproject.toml 和锁文件。
  3. 不需要把所有子包都 publish:多数内部包只通过 workspace 互引就行,只有公开 API 的包才需要发到 PyPI。

工具链管理:uv tool 的进阶用法

基础教程里讲的 uv tool install ruff 只是冰山一角。uv 0.11 之后 uv tool 能做更多。

从本地源码安装

团队内部的 CLI 工具,不需要先发到 PyPI 再安装:

Terminal window
uv tool install --from ./packages/cli my-cli

uv 会从本地目录构建并安装,改完代码重新跑一次就更新。

固定 Python 版本

默认 uv tool install 会用当前激活的 Python。如果工具要求特定版本:

Terminal window
uv tool install ruff --python 3.12

也可以在 uv.toml 里做全局配置:

[tool.uv]
python-preference = "only-managed"

列出和升级

Terminal window
uv tool list # 安装了哪些工具,各自什么版本
uv tool upgrade ruff # 升级单个
uv tool upgrade --all # 全部升级

临时用一下但不安装

Terminal window
uv tool run ruff check .

这个和 uvx 一样,uvx 是 uv tool run 的快捷方式。

uv check:让 uv 帮你跑类型检查

uv 0.11.18 之后可以用 uv check 跑类型检查(底层是 ty):

Terminal window
uv check

它会自动管理类型检查器的安装和环境,不用单独装 ty 之类的。不算特别成熟,但日常用用够了。

uv.lock:理解锁文件

uv 的锁文件 uv.lockpackage-lock.json / Cargo.lock 是一个思路——记录了每个依赖的确切版本和哈希值。不管谁在什么机器上 uv sync,装出来的依赖完全一致。

几个值得注意的点:

  • uv sync --frozen:只看锁文件,不解析依赖。CI 里标配。
  • uv sync --locked:检查锁文件是否和 pyproject.toml 一致,不一致就报错。也是 CI 常用。
  • uv lock 可以单独跑:你改了 pyproject.toml 的依赖约束,跑 uv lock 更新锁文件但不安装。
  • 锁文件格式换了:uv 0.11 开始默认用 pylock.toml 格式,可读性比旧的 uv.lock 好很多。两个格式都支持,老的 uv.lock 还能用。

一个典型的工作流:

Terminal window
uv add httpx # 加依赖,自动更新锁文件 + 安装
uv lock # 手动刷新锁文件(比如改了版本约束)
uv sync --frozen # CI 或部署时按锁文件安装

依赖组:组织不同类型的依赖

除了 dependenciesdev-dependencies,uv 支持自定义依赖组。用起来像这样:

pyproject.toml
[project]
name = "my-project"
dependencies = ["fastapi", "uvicorn"]
[dependency-groups]
test = ["pytest", "httpx", "coverage"]
lint = ["ruff"]
docs = ["mkdocs", "mkdocs-material"]

安装特定组:

Terminal window
uv sync --group test --group lint

或者只装非开发依赖(部署环境):

Terminal window
uv sync --no-dev

依赖组的好处是把”开发”这个概念拆得更细——测试、检查、文档各自一组,CI 里按需安装,不浪费构建时间。

配置文件 uv.toml

uv 支持全局和项目级配置文件,避免每次敲命令行参数。全局配置在 ~/.config/uv/uv.toml,项目级在 pyproject.toml[tool.uv] 里。

一些我认为值得配的选项:

# 项目级 pyproject.toml
[tool.uv]
dev-dependencies = ["pytest", "ruff"]
# 全局 ~/.config/uv/uv.toml
python-preference = "only-managed" # 只用 uv 自己管理的 Python
concurrent-downloads = 50 # 并发下载数,默认值通常够了

uv 0.11 新增了几个有用的环境变量,CI 里可能用到:

变量作用
UV_NO_INSTALL_PROJECT只装依赖不装项目本身
UV_NO_SYSTEM_CONFIG忽略系统级配置
UV_PYTHON_NO_REGISTRY不从网络找 Python 发行版
UV_NO_PROJECT完全不走项目模式

CI/CD 经验

总结几个我在 CI 里碰到的教训:

1. 始终用 --frozen

# GitHub Actions 示例
- run: uv sync --frozen
- run: uv run pytest

这保证 CI 装的依赖和本地开发的完全一致。不加这个 flag,CI 可能在解析依赖时得到和本地不同的结果。

2. 缓存 uv 的环境

- uses: actions/cache@v4
with:
path: ~/.cache/uv
key: uv-${{ runner.os }}-${{ hashFiles('uv.lock') }}

uv 的缓存目录存了下载的包和已解析的依赖信息,缓存后 uv sync 通常几秒就跑完。

3. 用 uv 安装 CI 工具

- run: uv tool run ruff check .
- run: uv tool run ty .

不需要在 CI 里 pip install ruff ty——uv tool run 自带缓存。

4. Python 版本用 .python-version

项目根目录放个 .python-version

3.12

CI 里:

- run: uv python install 3.12
- run: uv sync --frozen

uv 会严格按文件选 Python 版本,不会飘到系统默认的 Python 3.10 去。

常见问题

uv run 怎么知道用哪个 Python

uv 的 Python 发现顺序:命令行 --python > 项目 .python-version > 系统 python3。如果不确定当前用的是哪个:

Terminal window
uv run python -c "import sys; print(sys.executable)"

旧项目怎么迁移到 uv

不需要一次性迁移。可以先只把 uv 当锁文件工具用:

Terminal window
cd old-project
uv lock # 从现有的 requirements.txt 或 pyproject.toml 生成锁文件
uv sync # 用锁文件安装依赖

迁移期间 pipuv 可以共存——它们用同一个 venv 目录。

uv sync 很慢

第一次慢是正常的——uv 要下载所有包。之后的 uv sync 通常几秒完成。如果一直慢:

  1. 检查网络到 PyPI 的延迟(国内换个镜像源)
  2. 检查 uv.lock 是不是太大(几百个包的话,解析本身就要时间)
  3. 确认缓存目录没有被 CI 清掉

workspace 里包的导入路径对不上

workspace 子包默认以 editable 模式安装。如果 import my_core 找不到:

  1. 确认子包的 [project] name = "my-core" 和目录名没关系——import 路径是包内 src/ 下的 Python 模块名
  2. 确认根目录跑过 uv sync

uv tool install 的工具找不到

默认安装到 ~/.local/bin。确认这个路径在你的 PATH 里:

Terminal window
echo $PATH | tr ':' '\n' | grep local

uv 0.11 之后可以用 uv tool dir --bin 看工具的安装路径。

总结

把这篇文章的内容串起来,一个挺顺手的工作流大概是:

  • 单文件脚本用内联元数据 + uv run,扔掉 requirements.txt
  • 多包项目用 workspace,一个 uv sync 搞定所有依赖
  • 全局工具uv tool install,版本隔离不打架
  • CI 里uv sync --frozen + 缓存 ~/.cache/uv,稳且快

uv 的设计思路和我用过的其他 Python 工具不太一样——它不做抽象层,不搞插件生态,就专注于把”解析依赖、下载包、管理环境”这件事做快做准。从 pip + venv + pip-tools 的组合迁移过来之后,我再也没回去过。