Python技术迷

UV Workspace:一条命令管好所有 Python 子包

仓库里刚拆出第三个 Python 子包,依赖就开始乱了。

api 依赖 order_core,定时任务也依赖 order_core。有人在根目录建虚拟环境,有人跑进子目录执行 pip install -e .,CI 里还留着三份 requirements.txt。

最麻烦的不是安装慢,而是你根本不知道大家装的是不是同一套版本。

这种仓库,我现在一般直接上 UV Workspace。

假设项目长这样:

pay-platform/
├── packages/
│   └── order_core/
├── services/
│   └── order_api/
├── jobs/
│   └── reconcile_job/
└── pyproject.toml

三个目录都是独立的 Python 包,各自有自己的 pyproject.toml,但整个仓库只保留一份 uv.lock 和一个虚拟环境。

这点很重要。UV Workspace 不是把几个目录粗暴塞进 PYTHONPATH,而是把多个包作为一个整体解析依赖,同时允许每个包保留自己的名称、依赖和发布方式。

根目录先初始化:

uv init --package pay-platform

uv init --package packages/order_core
uv init --package services/order_api
uv init --package jobs/reconcile_job

在已有项目下面执行 uv init,UV 会识别父目录中的项目,并把新项目加入 Workspace。这个行为省事,但我还是会检查一遍根目录的配置,别等 CI 报找不到成员才回来翻。

根目录的 pyproject.toml 保留这段:

[project]
name = "pay-platform"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = []

[tool.uv.workspace]
members = [
    "packages/*",
    "services/*",
    "jobs/*",
]

以后再增加子包,只要目录能匹配这些规则,并且里面存在 pyproject.toml,就会被 Workspace 管起来。

业务包之间的依赖也别再手写相对路径。

比如 order_api 需要调用订单核心包,在仓库根目录执行:

uv add --package order-api ./packages/order_core
uv add --package order-api fastapi uvicorn

UV 会把 order_core 识别成 Workspace 内部成员,而不是跑到 PyPI 上下载一个同名包。生成出来的配置大致是这样:

[project]
name = "order-api"
version = "0.1.0"
dependencies = [
    "fastapi>=0.115",
    "order-core",
    "uvicorn>=0.34",
]

[tool.uv.sources]
order-core = { workspace = true }

workspace = true 这行不能随便删。

它明确告诉解析器:order-core 来自当前仓库,不要去公共仓库里碰运气。Workspace 成员之间默认按可编辑方式安装,改完核心包代码,API 侧直接就能读到,不需要反复执行 pip install -e。

真正省事的是同步环境。

uv sync --all-packages

一条命令,UV 会读取所有成员的依赖,生成统一的 uv.lock,再把整个 Workspace 同步到根目录的 .venv。

以前三个子包分别锁版本,很容易出现 API 用 pydantic 2.10,定时任务还卡在另一个版本。现在解析器会一次检查整张依赖图,真有冲突就当场报出来,不会等代码部署后再碰。

运行某个子包,也不用切目录:

uv run --package order-api \
  uvicorn order_api.main:app --reload

跑对账任务同样处理:

uv run --package reconcile-job \
  python -m reconcile_job.runner

uv run 和 uv sync 默认从 Workspace 根项目工作,也都支持通过 --package 指定具体成员。这个用法很顺手,根目录的脚本、Makefile 和 CI 不需要再写一堆 cd services/order_api。

我一般还会在 CI 里加一道检查:

uv lock --check
uv sync --locked --all-packages

有人改了 pyproject.toml 却没提交新的 uv.lock,流水线直接失败。比起让服务器偷偷重新解析一遍依赖,我更愿意让问题停在提交阶段。

需要构建时,也可以只打指定子包:

uv build --package order-core

或者一次构建整个 Workspace:

uv build --all-packages

UV Workspace 解决的并不是“目录太多”,而是同一个仓库里的 Python 包开始互相依赖之后,环境、锁文件和执行入口越来越难对齐。

一个子包时怎么装都行。到了三个、五个,再靠开发人员记命令,迟早有人在错误的虚拟环境里调半天。

这种活没必要靠记忆。

交给 uv sync --all-packages 就行。