uv 系列(六):Workspace 与 Monorepo——多包 Python 项目的依赖治理
核心目标:理解 uv workspace 的单锁文件模型,能够设计根项目与成员包、声明成员间依赖,并正确执行定向同步、运行、测试和构建。
前置知识:已掌握
pyproject.toml、uv.lock、dependency groups 和包构建流程。验证基线:uv 0.11.29;关键行为依据 2026-07-21 的 uv 官方 workspace 文档复核。
6.1 Workspace 解决什么问题
随着项目增长,weather-cli 可能同时包含:
- 只负责领域模型与数据转换的
weather-core; - 封装远端 API 的
weather-client; - 面向终端用户的
weather-cli; - 未来可选的数据库、Web 或监控插件。
把所有代码塞进一个包,边界会越来越模糊;拆成多个完全独立仓库,又会增加跨仓联调和版本协调成本。uv workspace 提供中间方案:一个仓库包含多个 Python 项目,每个成员有自己的 pyproject.toml,但共同解析并提交一个 uv.lock。
workspace 带来的核心收益:
- 成员间修改立即以 editable 方式联调;
- 所有成员在同一个解析图中,版本冲突提前暴露;
- 从仓库任意位置可按包名执行命令;
- 每个成员仍能独立构建和发布;
- CI 可以共享锁文件、缓存和工具链。
但它不等于“所有包共用一个版本号”,也不自动提供运行时依赖隔离。
6.2 什么时候不该使用 Workspace
workspace 适合相互关联、能够共享一套解析结果的包。以下场景应谨慎:
| 场景 | Workspace 是否合适 | 原因/替代方案 |
|---|---|---|
| CLI、核心库和插件共同开发 | 合适 | 边界清晰且需要频繁联调 |
| 多个包共享相同 Python 支持范围 | 合适 | 单一 requires-python 交集可满足 |
| 成员需要互相冲突的依赖版本 | 不合适 | 使用独立项目或路径依赖 |
| 每个成员必须拥有独立虚拟环境 | 不合适 | workspace 默认共享项目环境 |
| 仓库只是收集互不相关的脚本 | 通常不合适 | 独立 PEP 723 脚本更简单 |
| 成员必须独立授权、审计和发布 | 视情况 | 多仓库可能更符合组织边界 |
一个重要限制是:workspace 对所有成员计算 requires-python 的交集。若包 A 支持 Python 3.10,而包 B 只支持 3.12+,整个 workspace 的有效范围至少是 3.12+。你无法仅靠 uv run --package A 让统一锁文件恢复对 3.10 的完整覆盖。
6.3 设计目录结构
将前五篇的单包项目演进为:
weather-platform/
├── .python-version
├── .gitignore
├── README.md
├── pyproject.toml
├── uv.lock
├── packages/
│ ├── weather-core/
│ │ ├── pyproject.toml
│ │ ├── README.md
│ │ ├── src/weather_core/
│ │ │ ├── __init__.py
│ │ │ └── models.py
│ │ └── tests/test_models.py
│ └── weather-client/
│ ├── pyproject.toml
│ ├── README.md
│ ├── src/weather_client/
│ │ ├── __init__.py
│ │ └── client.py
│ └── tests/test_client.py
└── apps/
└── weather-cli/
├── pyproject.toml
├── README.md
├── src/weather_cli/
│ ├── __init__.py
│ └── cli.py
└── tests/test_cli.py
设计原则:
packages/放可复用或可发布库;apps/放最终应用和 CLI;- 每个成员拥有独立名称、版本、依赖和构建配置;
- 仓库根只保留一个
uv.lock和默认.venv; - 成员目录不各自提交锁文件;
- 测试靠近成员,跨包集成测试可放根目录
tests/integration/。
6.4 创建工作区根项目
从空目录开始:
New-Item -ItemType Directory weather-platform
Set-Location weather-platform
uv init --app --name weather-platform --python 3.12 .
根 pyproject.toml:
[project]
name = "weather-platform"
version = "0.1.0"
description = "Development workspace for the weather platform"
requires-python = ">=3.12"
dependencies = []
[dependency-groups]
dev = [
"ruff>=0.12,<1",
]
test = [
"pytest>=8,<9",
"pytest-cov>=6,<7",
]
typing = [
"mypy>=1.16,<2",
]
[tool.uv]
default-groups = ["dev", "test", "typing"]
[tool.uv.workspace]
members = [
"packages/*",
"apps/*",
]
exclude = [
"packages/experiments",
]
members 和 exclude 接受 glob。每个最终匹配且未排除的目录都必须包含 pyproject.toml,否则 workspace 发现会失败。
根项目本身也是 workspace 成员。即使根项目不发布,也应提供合法 [project] 元数据。根级 dependency groups 很适合保存全仓统一的 Ruff、pytest 和类型检查器版本。
6.5 创建成员项目
在 workspace 根目录执行:
uv init --lib packages/weather-core --name weather-core --python 3.12
uv init --lib packages/weather-client --name weather-client --python 3.12
uv init --package apps/weather-cli --name weather-cli --python 3.12
uv 在已有 workspace 内初始化项目时,通常能自动将成员加入根配置;仍应检查最终 members,不要假设自动发现覆盖了自定义目录。
6.5.1 weather-core
packages/weather-core/pyproject.toml:
[project]
name = "weather-core"
version = "0.1.0"
description = "Domain models for the weather platform"
readme = "README.md"
requires-python = ">=3.12"
dependencies = []
[build-system]
requires = ["uv_build>=0.11.29,<0.12.0"]
build-backend = "uv_build"
models.py:
from __future__ import annotations
from dataclasses import dataclass
@dataclass(frozen=True, slots=True)
class Weather:
city: str
temperature_c: float
condition: str
def __post_init__(self) -> None:
if not self.city.strip():
raise ValueError("city must not be empty")
6.5.2 weather-client
packages/weather-client/pyproject.toml:
[project]
name = "weather-client"
version = "0.1.0"
description = "HTTP client for weather services"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"httpx>=0.28,<1",
"weather-core",
]
[tool.uv.sources]
weather-core = { workspace = true }
[build-system]
requires = ["uv_build>=0.11.29,<0.12.0"]
build-backend = "uv_build"
6.5.3 weather-cli
apps/weather-cli/pyproject.toml:
[project]
name = "weather-cli"
version = "0.1.0"
description = "Command-line weather application"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"weather-client",
"weather-core",
]
[project.scripts]
weather = "weather_cli.cli:main"
[tool.uv.sources]
weather-client = { workspace = true }
weather-core = { workspace = true }
[build-system]
requires = ["uv_build>=0.11.29,<0.12.0"]
build-backend = "uv_build"
6.6 workspace = true 的真实含义
成员间依赖需要同时表达两件事:
[project]
dependencies = ["weather-core"]
[tool.uv.sources]
weather-core = { workspace = true }
第一行是标准、可发布的依赖元数据:发布 weather-client 后,用户仍需安装名为 weather-core 的发行包。第二行是 uv 开发期来源覆盖:在本仓库内从 workspace 成员提供它,而不是从 PyPI 下载。
这层分离非常关键:
[project.dependencies]面向所有标准构建与安装工具;[tool.uv.sources]面向 uv 本地开发工作流;- 成员间依赖默认 editable,源码修改会立即反映;
- 发布前必须证明关闭 uv sources 后包仍然可构建和安装。
为成员添加 workspace 依赖也可以使用:
uv add --package weather-client --workspace weather-core
uv add --package weather-cli --workspace weather-client
命令比手改 TOML 更不容易漏掉 sources 映射,但仍要评审生成结果。
6.7 根级 Sources 的继承
根项目的 [tool.uv.sources] 默认可影响成员:
[tool.uv.sources]
company-logging = { index = "internal" }
成员可为同一依赖提供自己的 source 进行覆盖。一旦成员声明了该依赖的 source,根级同名 source 会被忽略;即使成员 source 上带有当前平台不匹配的 marker,也不能简单假设会回退到根配置。
工程建议:
- workspace 成员映射写在直接使用它的成员中,依赖关系更清晰;
- 全仓统一的私有索引或 Git fork 可放根级;
- 平台条件来源必须在每个目标平台实测;
- 发布前用
--no-sources检查标准元数据闭环。
6.8 锁定、同步与运行
6.8.1 锁定始终面向整个 Workspace
uv lock
uv lock --check
无论从哪个成员触发,uv lock 都解析整个 workspace。一个成员的依赖变更可能改变统一锁文件,因此 PR 评审不能只看该成员目录。
6.8.2 默认操作根项目
uv sync
uv run python -V
默认针对 workspace 根项目。要定向成员:
uv sync --package weather-cli
uv run --package weather-cli weather Shanghai
uv run --package weather-core pytest packages/weather-core/tests
安装所有成员:
uv sync --all-packages
uv run --all-packages pytest
区别:
| 命令 | 锁定范围 | 安装/运行范围 |
|---|---|---|
uv lock |
整个 workspace | 不适用 |
uv sync |
校验整个锁文件 | 根项目及所选依赖 |
uv sync --package X |
校验整个锁文件 | 成员 X 及依赖 |
uv sync --all-packages |
校验整个锁文件 | 所有成员 |
uv run --package X |
校验整个锁文件 | 以成员 X 为目标运行 |
6.8.3 一个环境不等于依赖隔离
workspace 默认共享项目环境。如果先安装了所有成员,包 A 可能错误地导入只由包 B 声明的依赖,测试仍然通过。Python 本身不会阻止这种越界导入。
降低风险:
- 源代码评审确保每个成员声明自己的直接依赖;
- CI 分别执行
uv sync --package <member>后测试; - 发布包在
--no-project --isolated环境中测试; - 对库使用依赖分析工具检查未声明导入;
- 不把“全仓测试通过”当作单包元数据正确的证明。
6.9 构建和发布成员
构建单个成员:
uv build --package weather-core --clear --no-sources
uv build --package weather-client --clear --no-sources
uv build --package weather-cli --clear --no-sources
构建全部包:
uv build --all-packages --clear --no-sources
--no-sources 会忽略 workspace source。若 weather-client 的标准依赖 weather-core 尚未发布或无法从配置索引获取,隔离验证会失败。这是发布顺序必须解决的问题:
weather-core → weather-client → weather-cli
同仓不代表同步发版。每个成员可以有独立版本,但依赖约束应匹配发布策略:
dependencies = ["weather-core>=0.1,<1"]
发布检查至少包含:
- 从 sdist 重建 wheel;
- 单独安装成员 wheel;
- 从真实索引解析成员间依赖;
- 验证入口点和公开 API;
- 确认产物不包含其他成员源码。
6.10 测试策略
6.10.1 单元测试按成员执行
uv run --package weather-core pytest packages/weather-core/tests
uv run --package weather-client pytest packages/weather-client/tests
uv run --package weather-cli pytest apps/weather-cli/tests
6.10.2 集成测试安装所有成员
uv run --all-packages pytest tests/integration
6.10.3 最低依赖测试
统一解析最低直接版本:
uv lock --resolution lowest-direct
uv run --all-packages pytest
完成后恢复正常最高兼容解析并提交预期锁文件。不要在同一个持久分支上来回覆盖锁文件而不检查差异;CI 可在临时工作区完成最低版本验证。
6.10.4 Python 版本矩阵
workspace 有一个共同有效 Python 范围。CI 至少测试:
- 交集的最低支持版本;
- 当前团队默认版本;
- 当前稳定版本。
某个成员若必须测试 workspace 交集之外的 Python,应将其作为独立项目构建环境处理,或重新评估是否应留在 workspace。
6.11 CI 变更范围优化
小型仓库每次运行全量测试最可靠。大型 monorepo 可以按影响图优化,但不能只看“修改文件属于哪个目录”。例如 weather-core 的变化会影响所有下游:
安全的影响计算需要考虑:
- 直接修改的成员;
- 所有反向依赖成员;
- 根
pyproject.toml、uv.lock、CI 和工具配置; - 公共测试夹具、代码生成器和构建脚本;
- 发布工作流与成员版本变更。
拿不准时运行全量测试。节省几分钟不值得换来错误发布。
6.12 常见故障
故障一:成员目录存在,但 uv 不识别
检查:
uv tree
确认目录匹配 members、未被 exclude 排除,并且包含合法 pyproject.toml。
故障二:uv 从 PyPI 下载本应使用的本地成员
成员依赖缺少:
[tool.uv.sources]
weather-core = { workspace = true }
使用 uv add --workspace 修复,并检查发行名称完全一致。
故障三:requires-python 冲突
列出每个成员的范围并求交集。如果交集为空,workspace 无法形成统一解析。不要伪造更宽范围;调整成员支持策略或拆成独立项目。
故障四:包单测通过,发布后缺少依赖
全环境泄漏掩盖了未声明依赖。用定向同步、独立 wheel 安装和 --no-sources 构建复现。
故障五:Docker 依赖层使用 --locked 失败
若早期层只挂载根 pyproject.toml 和 uv.lock,uv 无法检查所有成员元数据。workspace Docker 分层应在早期使用:
RUN uv sync --frozen --no-install-workspace
复制完整源码和所有成员 pyproject.toml 后,再执行 uv sync --locked 完成严格校验。Part 7 将展开这一流程。
故障六:成员命令运行了错误项目
显式指定包:
uv run --package weather-cli weather --help
不要依赖当前目录碰巧被识别成目标成员。
6.13 Workspace 验收清单
- 根配置的
members和exclude与实际目录一致。 - 仓库只提交一个根
uv.lock。 - 每个成员具有独立、完整的
[project]和[build-system]。 - 成员间依赖同时出现在标准 dependencies 和 workspace sources。
- 全 workspace 的
requires-python交集符合真实支持范围。 - 单成员测试使用
--package,集成测试使用--all-packages。 - 每个可发布成员通过
uv build --package ... --no-sources。 - 每个成员 wheel 在隔离环境中能独立安装和运行。
- CI 影响分析包含反向依赖,而不是只按目录过滤。
- 私有索引、Git fork 和平台条件 source 已逐平台验证。
6.14 本篇小结
uv workspace 的本质是“多个独立项目,共享一个解析结果”。单锁文件让冲突更早暴露,workspace = true 让成员间 editable 协作更顺畅,而 --package/--all-packages 提供精确运行范围。与此同时,共享环境会产生依赖泄漏风险,发布前的 --no-sources 构建和隔离 wheel 测试不可省略。
下一篇将把单包与 workspace 项目放入 GitHub Actions 和 Docker,建立可重复、可缓存、最小权限的生产交付流程。
官方参考
更多推荐



所有评论(0)