Monorepo 的痛点
很多公司把多个相关的 Python 项目放在同一个仓库里:
- 一个
shared/包放公共代码(数据模型、工具函数) - 多个
services/下的微服务项目 - 一个
cli/运维工具包
在 pip/Poetry 时代,要么每个子项目各自一个 venv + 各自 lock(管理麻烦、依赖不一致),要么所有都塞进根 venv(谁依赖谁不清楚、测试边界模糊)。Rust 的 Cargo 用 workspace 优雅解决了这个问题,uv 把同样的模式引入 Python。
Workspace 基本结构
myrepo/
├── pyproject.toml # 根 workspace 配置
├── uv.lock # 唯一的 lock,所有成员共享
├── .venv/ # 唯一的 venv
├── packages/
│ ├── shared/
│ │ ├── pyproject.toml
│ │ └── src/shared/__init__.py
│ ├── api/
│ │ ├── pyproject.toml # 依赖 shared
│ │ └── src/api/__init__.py
│ └── worker/
│ ├── pyproject.toml # 也依赖 shared
│ └── src/worker/__init__.py
└── cli/
├── pyproject.toml # 独立 CLI
└── src/cli/__init__.py
根 pyproject.toml 配置
[project]
name = "myrepo"
version = "0.0.0"
requires-python = ">=3.12"
# 根项目可以不声明 dependencies,仅作为 workspace 协调器
[tool.uv.workspace]
members = ["packages/*", "cli"]
# 支持通配符和显式路径
exclude = ["packages/legacy"] # 可选:排除某些目录
[tool.uv.sources]
# 把 shared 解析为 workspace 成员(而不是去 PyPI 找)
shared = { workspace = true }
成员包的 pyproject.toml
# packages/api/pyproject.toml
[project]
name = "api"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"fastapi>=0.110",
"shared", # 依赖另一个 workspace 成员
]
[tool.uv.sources]
shared = { workspace = true }
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
Workspace 的工作方式
- 一份 uv.lock:所有成员共用,保证跨包依赖版本一致(不会出现 api 用 pydantic 2.7 而 worker 用 2.8 这种情况)。
- 一份 .venv:所有成员安装到同一个环境,成员之间通过 editable install 互相可见。
- 全局命令:
uv sync默认同步所有成员;uv run可从任意子目录运行,自动找到根 workspace。 - 共享缓存:依赖的 wheel 只下载解压一次。
常用 workspace 命令
# 在根目录
uv sync # 同步所有成员的依赖
uv sync --package api # 只同步 api 的依赖(其他成员不装)
uv run --package api uvicorn api.main:app # 在 api 环境中运行
# 在任意子目录(uv 会自动向上找根)
cd packages/api
uv sync # 效果同根目录的 uv sync
uv add httpx # 添加到当前包的 dependencies
# 构建所有成员
uv build --all-packages
# 构建特定成员
uv build --package shared
依赖跨成员共享的 4 种模式
模式 1:workspace 源依赖(最常见)
api 依赖 shared:
dependencies = ["shared"] + [tool.uv.sources] shared = { workspace = true }。构建发布时 uv 会把 shared 作为正常版本发布。模式 2:editable workspace
默认情况下 workspace 成员之间就是 editable 安装——你改 shared 的代码,api 的测试立刻看到变化,不需要重装。
模式 3:共享 dev 依赖
在根 pyproject 的
[dependency-groups].dev 里放所有成员共用的开发依赖(pytest、ruff、mypy),所有成员可直接使用。模式 4:成员特定依赖
每个成员在自己的 pyproject 里声明。但 uv 会把所有成员的依赖合并解析到同一个 lock(需要兼容)。
发布多包
workspace 成员可以独立发布到 PyPI:
# 1. 只构建 shared 包
uv build --package shared
# 生成 dist/shared-0.1.0-py3-none-any.whl
# 2. 发布
uv publish dist/shared-*.whl
# 3. 发完之后再发 api(api 依赖的是已发布的 shared)
uv build --package api
uv publish dist/api-*.whl
版本管理建议
Monorepo 里多个包可以选择 统一版本号(像 Next.js、Nx 那样所有包一起发)或 独立版本号(像 npm 的 lerna --independent)。独立版本更灵活但管理复杂。小团队建议统一版本;若业务拆分清晰可独立。
实战:一个典型的 Python Monorepo
company-platform/
├── pyproject.toml # workspace root
├── uv.lock
├── packages/
│ ├── models/ # Pydantic 数据模型
│ │ └── src/models/
│ ├── db/ # SQLAlchemy 封装
│ │ └── src/db/
│ ├── auth/ # 认证中间件
│ │ └── src/auth/
│ └── telemetry/ # 日志/指标/追踪
│ └── src/telemetry/
├── services/
│ ├── api/ # FastAPI 主服务
│ ├── worker/ # Celery 后台任务
│ └── scheduler/ # APScheduler 定时
└── tools/
├── migrate/ # 数据库迁移 CLI
└── admin/ # 运维 CLI
# 根 pyproject.toml
[tool.uv.workspace]
members = ["packages/*", "services/*", "tools/*"]
[tool.uv.sources]
models = { workspace = true }
db = { workspace = true }
auth = { workspace = true }
telemetry = { workspace = true }
[dependency-groups]
dev = ["pytest", "pytest-asyncio", "ruff", "mypy", "pre-commit"]
IDE 配置
VS Code / PyCharm 要指向根目录的 .venv/bin/python。由于成员都是 editable 安装,代码跳转、类型检查、refactor 在 Monorepo 里完全流畅。
// .vscode/settings.json
{
"python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python",
"python.analysis.extraPaths": [
"packages/models/src",
"packages/db/src",
"packages/auth/src",
"packages/telemetry/src"
]
}
本章小结
workspace = 多包 + 单 lock + 单 venv,让 Python Monorepo 终于像 npm/Cargo 一样体验丝滑。核心三个配置:[tool.uv.workspace].members 声明成员、[tool.uv.sources] 把内部依赖解析为 workspace、每个成员自己的 pyproject 声明依赖关系。下一章处理一个折磨所有 AI 开发者的话题——PyTorch CUDA 多索引源。