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-platformuv 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 就行。