跳到内容

使用工作区

Cargo 同名概念的启发,工作空间是“一个或多个包(称为工作空间成员)的集合,它们被统一管理。”

工作空间通过将大型代码库拆分为具有共同依赖项的多个包来组织代码。例如:一个基于 FastAPI 的 Web 应用程序,以及一系列作为独立 Python 包进行版本控制和维护的库,它们都位于同一个 Git 仓库中。

在工作空间中,每个包都定义了自己的 pyproject.toml,但整个工作空间共享一个锁文件(lockfile),从而确保工作空间在一致的依赖项环境下运行。

因此,uv lock 会一次性作用于整个工作空间,而 uv runuv sync 默认作用于工作空间根目录。不过,两者都接受 --package 参数,允许你在任何工作空间目录下针对特定的工作空间成员运行命令。

快速入门

要创建工作空间,需在 pyproject.toml 中添加一个 tool.uv.workspace 表,这将隐式创建一个以该包为根的工作空间。

提示

默认情况下,在现有包内运行 uv init 会将新创建的成员添加到工作空间中;如果工作空间根目录尚不存在 tool.uv.workspace 表,则会自动创建。

在定义工作空间时,必须指定 members(必需)和 exclude(可选)键。它们分别指示工作空间应包含或排除哪些特定目录作为成员,并接受 glob 模式列表。

pyproject.toml
[project]
name = "albatross"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["bird-feeder", "tqdm>=4,<5"]

[tool.uv.sources]
bird-feeder = { workspace = true }

[tool.uv.workspace]
members = ["packages/*"]
exclude = ["packages/seeds"]

members glob 所包含(且未被 exclude glob 排除)的每个目录都必须包含一个 pyproject.toml 文件。然而,工作空间成员既可以是应用程序,也可以是;在工作空间上下文中,两者均受支持。

每个工作空间都需要一个根目录,它也同时是工作空间成员。在上面的示例中,albatross 是工作空间根目录,而工作空间成员包括 packages 目录下的所有项目,seeds 除外。

默认情况下,uv runuv sync 在工作空间根目录下操作。例如在上述示例中,uv runuv run --package albatross 是等效的,而 uv run --package bird-feeder 则会在 bird-feeder 包中运行命令。

工作空间来源

在工作空间内,对工作空间成员的依赖通过 tool.uv.sources 来实现,例如:

pyproject.toml
[project]
name = "albatross"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["bird-feeder", "tqdm>=4,<5"]

[tool.uv.sources]
bird-feeder = { workspace = true }

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

[build-system]
requires = ["uv_build>=0.10.9,<0.11.0"]
build-backend = "uv_build"

在此示例中,albatross 项目依赖于 bird-feeder 项目,后者是该工作空间的成员。tool.uv.sources 表中的 workspace = true 键值对表明 bird-feeder 依赖项应由工作空间提供,而不是从 PyPI 或其他仓库获取。

注意

工作空间成员之间的依赖关系是可编辑的(editable)。

工作空间根目录中的任何 tool.uv.sources 定义都适用于所有成员,除非在特定成员的 tool.uv.sources 中进行了覆盖。例如,对于以下 pyproject.toml

pyproject.toml
[project]
name = "albatross"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["bird-feeder", "tqdm>=4,<5"]

[tool.uv.sources]
bird-feeder = { workspace = true }
tqdm = { git = "https://github.com/tqdm/tqdm" }

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

[build-system]
requires = ["uv_build>=0.10.9,<0.11.0"]
build-backend = "uv_build"

默认情况下,每个工作空间成员都会从 GitHub 安装 tqdm,除非某个成员在其自己的 tool.uv.sources 表中覆盖了 tqdm 条目。

注意

如果某个工作空间成员为某个依赖项提供了 tool.uv.sources,它将忽略工作空间根目录中针对该依赖项的任何 tool.uv.sources 定义,即使该成员的来源受标记 (marker) 限制且该标记与当前平台不匹配,也是如此。

工作空间布局

最常见的工作空间布局可以被看作是一个根项目附带一系列配套库。

例如,延续上面的例子,该工作空间在 albatross 处有一个显式根目录,并在 packages 目录下有两个库(bird-feederseeds):

albatross
├── packages
│   ├── bird-feeder
│   │   ├── pyproject.toml
│   │   └── src
│   │       └── bird_feeder
│   │           ├── __init__.py
│   │           └── foo.py
│   └── seeds
│       ├── pyproject.toml
│       └── src
│           └── seeds
│               ├── __init__.py
│               └── bar.py
├── pyproject.toml
├── README.md
├── uv.lock
└── src
    └── albatross
        └── main.py

由于 seedspyproject.toml 中被排除,该工作空间总共有两个成员:albatross(根)和 bird-feeder

何时(不)使用工作空间

工作空间旨在促进在单个仓库内开发多个相互关联的包。随着代码库复杂性的增加,将其拆分为更小、可组合的包(每个包都有自己的依赖项和版本限制)会非常有帮助。

工作空间有助于加强隔离和关注点分离。例如,在 uv 中,核心库和命令行界面有独立的包,这使我们能够独立于 CLI 测试核心库,反之亦然。

工作空间的其他常见用例包括:

  • 具有在扩展模块(Rust、C++ 等)中实现的性能关键型子程序的库。
  • 具有插件系统的库,其中每个插件都是一个独立的、依赖于根的工作空间包。

如果成员之间存在冲突的要求,或者希望为每个成员使用单独的虚拟环境,则不适合使用工作空间。在这种情况下,通常更推荐使用路径依赖 (path dependencies)。例如,与其将 albatross 及其成员组合在工作空间中,你始终可以将每个包定义为独立的自身项目,并在 tool.uv.sources 中将包间依赖定义为路径依赖。

pyproject.toml
[project]
name = "albatross"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = ["bird-feeder", "tqdm>=4,<5"]

[tool.uv.sources]
bird-feeder = { path = "packages/bird-feeder" }

[build-system]
requires = ["uv_build>=0.10.9,<0.11.0"]
build-backend = "uv_build"

这种方法具有许多相同的优点,但允许对依赖解析和虚拟环境管理进行更精细的控制(缺点是无法再使用 uv run --package;相反,命令必须在相关的包目录中运行)。

最后,uv 的工作空间对整个工作空间强制执行单一的 requires-python,取所有成员 requires-python 值的交集。如果你需要在一个工作空间其余部分不支持的 Python 版本上测试某个成员,你可能需要使用 uv pip 在单独的虚拟环境中安装该成员。

注意

由于 Python 不提供依赖隔离,uv 无法确保包仅使用其声明的依赖项。对于工作空间,uv 无法确保包不会导入其他工作空间成员声明的依赖项。