跳转至

使用工作区

Cargo 同名概念的启发,工作区是 “一组一个或多个包,称为 工作区成员,它们被一起管理”。

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

在工作区中,每个包都定义自己的 pyproject.toml,但工作区共享单个锁文件,以确保工作区使用一组一致的依赖项运行。

因此,uv lock 会一次性对整个工作区进行操作,而 uv runuv sync 默认在工作区根目录操作,不过这两个命令都接受 --package 参数,允许你从任何工作区目录在特定的工作区成员中运行命令。

入门

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

提示

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

在定义工作区时,必须指定 members(必填)和 exclude(选填)键,它们分别用于指示工作区将特定目录作为成员包含或排除,并接受通配符列表:

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 通配符包含(且未被 exclude 通配符排除)的每个目录都必须包含一个 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 = ["hatchling"]
build-backend = "hatchling.build"

在这个示例中,albatross 项目依赖 bird-feeder 项目,bird-feeder 是工作区的成员。tool.uv.sources 表中的 workspace = true 键值对表明,bird-feeder 依赖应由工作区提供,而不是从 PyPI 或其他注册中心获取。

注意

工作区成员之间的依赖是可编辑的。

工作区根目录中的任何 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 = ["hatchling"]
build-backend = "hatchling.build"

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

工作区布局

最常见的工作区布局可以看作是一个根项目以及一系列配套的库。

例如,继续以上面的示例来说,此工作区在 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 中,我们为核心库和命令行界面分别设置了软件包,这样我们就可以独立于命令行界面测试核心库,反之亦然。

工作区的其他常见用例包括: - 一个库,其中有一个对性能要求很高的子例程,通过扩展模块(Rust、C++ 等)实现。 - 一个带有插件系统的库,其中每个插件都是一个单独的工作区软件包,依赖于根目录。

工作区适用于成员之间有冲突需求,或者希望每个成员都有单独的虚拟环境的情况。在这种情况下,路径依赖通常更合适。例如,与其将 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 = ["hatchling"]
build-backend = "hatchling.build"

这种方法有许多相同的优点,但可以对依赖项解析和虚拟环境管理进行更细粒度的控制(缺点是 uv run --package 不再可用;相反,必须从相关的软件包目录运行命令)。

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

注意

由于 Python 不提供依赖隔离,uv 无法确保一个软件包仅使用其声明的依赖项。具体到工作区,uv 无法确保软件包不会导入其他工作区成员声明的依赖项。