核心目标:理解 uv workspace 的单锁文件模型,能够设计根项目与成员包、声明成员间依赖,并正确执行定向同步、运行、测试和构建。

前置知识:已掌握 pyproject.tomluv.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

依赖

依赖

依赖

weather-platform
Workspace 根项目

uv.lock
全工作区统一解析

weather-core
领域模型

weather-client
API 客户端

weather-cli
终端应用

workspace 带来的核心收益:

  1. 成员间修改立即以 editable 方式联调;
  2. 所有成员在同一个解析图中,版本冲突提前暴露;
  3. 从仓库任意位置可按包名执行命令;
  4. 每个成员仍能独立构建和发布;
  5. 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",
]

membersexclude 接受 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 本身不会阻止这种越界导入。

降低风险:

  1. 源代码评审确保每个成员声明自己的直接依赖;
  2. CI 分别执行 uv sync --package <member> 后测试;
  3. 发布包在 --no-project --isolated 环境中测试;
  4. 对库使用依赖分析工具检查未声明导入;
  5. 不把“全仓测试通过”当作单包元数据正确的证明。

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 的变化会影响所有下游:

weather-core 变化

测试 weather-client

测试 weather-cli

仅文档变化

文档与链接检查

安全的影响计算需要考虑:

  • 直接修改的成员;
  • 所有反向依赖成员;
  • pyproject.tomluv.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.tomluv.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 验收清单

  • 根配置的 membersexclude 与实际目录一致。
  • 仓库只提交一个根 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,建立可重复、可缓存、最小权限的生产交付流程。

官方参考

Logo

小龙虾开发者社区是 CSDN 旗下专注 OpenClaw 生态的官方阵地,聚焦技能开发、插件实践与部署教程,为开发者提供可直接落地的方案、工具与交流平台,助力高效构建与落地 AI 应用

更多推荐