mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-19 19:25:08 +00:00
259 lines
9.3 KiB
Markdown
259 lines
9.3 KiB
Markdown
# Workspaces
|
|
|
|
Inspired by the [Cargo](https://doc.rust-lang.org/cargo/reference/workspaces.html) concept of the
|
|
same name, a workspace is "a collection of one or more packages, called _workspace members_, that
|
|
are managed together."
|
|
|
|
Workspaces organize large codebases by splitting them into multiple packages with common
|
|
dependencies. Think: a FastAPI-based web application, alongside a series of libraries that are
|
|
versioned and maintained as separate Python packages, all in the same Git repository.
|
|
|
|
In a workspace, each package defines its own `pyproject.toml`, but the workspace shares a single
|
|
lockfile, ensuring that the workspace operates with a consistent set of dependencies.
|
|
|
|
As such, `uv lock` operates on the entire workspace at once, while `uv run` and `uv sync` operate on
|
|
the workspace root by default, though both accept a `--package` argument, allowing you to run a
|
|
command in a particular workspace member from any workspace directory.
|
|
|
|
## Getting started
|
|
|
|
To create a workspace, add a `tool.uv.workspace` table to a `pyproject.toml`, which will implicitly
|
|
create a workspace rooted at that package.
|
|
|
|
!!! tip
|
|
|
|
By default, running `uv init` inside an existing package will add the newly created member to the workspace, creating a `tool.uv.workspace` table in the workspace root if it doesn't already exist.
|
|
|
|
In defining a workspace, you must specify the `members` (required) and `exclude` (optional) keys,
|
|
which direct the workspace to include or exclude specific directories as members respectively, and
|
|
accept lists of globs:
|
|
|
|
```toml title="pyproject.toml"
|
|
[tool.uv.workspace]
|
|
members = ["packages/*", "examples/*"]
|
|
exclude = ["example/excluded_example"]
|
|
```
|
|
|
|
In this example, the workspace includes all packages in the `packages` directory and all examples in
|
|
the `examples` directory, with the exception of the `example/excluded_example` directory.
|
|
|
|
Every directory included by the `members` globs (and not excluded by the `exclude` globs) must
|
|
contain a `pyproject.toml` file; in other words, every member must be a valid Python package, or
|
|
workspace discovery will raise an error.
|
|
|
|
## Workspace roots
|
|
|
|
Every workspace needs a workspace root, which can either be explicit or "virtual".
|
|
|
|
An explicit root is a directory that is itself a valid Python package, and thus a valid workspace
|
|
member, as in:
|
|
|
|
```toml title="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"
|
|
```
|
|
|
|
A virtual root is a directory that is _not_ a valid Python package, but contains a `pyproject.toml`
|
|
with a `tool.uv.workspace` table. In other words, the `pyproject.toml` exists to define the
|
|
workspace, but does not itself define a package, as in:
|
|
|
|
```toml title="pyproject.toml"
|
|
[tool.uv.workspace]
|
|
members = ["packages/*"]
|
|
```
|
|
|
|
A virtual root _must not_ contain a `[project]` table, as the inclusion of a `[project]` table
|
|
implies the directory is a package, and thus an explicit root. As such, virtual roots cannot define
|
|
their own dependencies; however, they _can_ define development dependencies as in:
|
|
|
|
```toml title="pyproject.toml"
|
|
[tool.uv.workspace]
|
|
members = ["packages/*"]
|
|
|
|
[tool.uv]
|
|
dev-dependencies = ["ruff==0.5.0"]
|
|
```
|
|
|
|
By default, `uv run` and `uv sync` operates on the workspace root, if it's explicit. For example, in
|
|
the above example, `uv run` and `uv run --package albatross` would be equivalent. For virtual
|
|
workspaces, `uv run` and `uv sync` instead sync all workspace members, since the root is not a
|
|
member itself.
|
|
|
|
## Workspace sources
|
|
|
|
Within a workspace, dependencies on workspace members are facilitated via
|
|
[`tool.uv.sources`](./dependencies.md), as in:
|
|
|
|
```toml title="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"
|
|
```
|
|
|
|
In this example, the `albatross` package depends on the `bird-feeder` package, which is a member of
|
|
the workspace. The `workspace = true` key-value pair in the `tool.uv.sources` table indicates the
|
|
`bird-feeder` dependency should be provided by the workspace, rather than fetched from PyPI or
|
|
another registry.
|
|
|
|
Any `tool.uv.sources` definitions in the workspace root apply to all members, unless overridden in
|
|
the `tool.uv.sources` of a specific member. For example, given the following `pyproject.toml`:
|
|
|
|
```toml title="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"
|
|
```
|
|
|
|
Every workspace member would, by default, install `tqdm` from GitHub, unless a specific member
|
|
overrides the `tqdm` entry in its own `tool.uv.sources` table.
|
|
|
|
## Workspace layouts
|
|
|
|
In general, there are two common layouts for workspaces, which map to the two kinds of workspace
|
|
roots: a **root package with helpers** (for explicit roots) and a **flat workspace** (for virtual
|
|
roots).
|
|
|
|
In the former case, the workspace includes an explicit workspace root, with peripheral packages or
|
|
libraries defined in `packages`. For example, here, `albatross` is an explicit workspace root, and
|
|
`bird-feeder` and `seeds` are workspace members:
|
|
|
|
```text
|
|
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
|
|
```
|
|
|
|
In the latter case, _all_ members are located in the `packages` directory, and the root
|
|
`pyproject.toml` comprises a virtual root:
|
|
|
|
```text
|
|
albatross
|
|
├── packages
|
|
│ ├── albatross
|
|
│ │ ├── pyproject.toml
|
|
│ │ └── src
|
|
│ │ └── albatross
|
|
│ │ ├── __init__.py
|
|
│ │ └── foo.py
|
|
│ ├── 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
|
|
```
|
|
|
|
## When (not) to use workspaces
|
|
|
|
Workspaces are intended to facilitate the development of multiple interconnected packages within a
|
|
single repository. As a codebase grows in complexity, it can be helpful to split it into smaller,
|
|
composable packages, each with their own dependencies and version constraints.
|
|
|
|
Workspaces help enforce isolation and separation of concerns. For example, in uv, we have separate
|
|
packages for the core library and the command-line interface, enabling us to test the core library
|
|
independently of the CLI, and vice versa.
|
|
|
|
Other common use cases for workspaces include:
|
|
|
|
- A library with a performance-critical subroutine implemented in an extension module (Rust, C++,
|
|
etc.).
|
|
- A library with a plugin system, where each plugin is a separate workspace package with a
|
|
dependency on the root.
|
|
|
|
Workspaces are _not_ suited for cases in which members have conflicting requirements, or desire a
|
|
separate virtual environment for each member. In this case, path dependencies are often preferable.
|
|
For example, rather than grouping `albatross` and its members in a workspace, you can always define
|
|
each package as its own independent project, with inter-package dependencies defined as path
|
|
dependencies in `tool.uv.sources`:
|
|
|
|
```toml title="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"
|
|
```
|
|
|
|
This approach conveys many of the same benefits, but allows for more fine-grained control over
|
|
dependency resolution and virtual environment management (with the downside that `uv run --package`
|
|
is no longer available; instead, commands must be run from the relevant package directory).
|
|
|
|
Finally, uv's workspaces enforce a single `requires-python` for the entire workspace, taking the
|
|
intersection of all members' `requires-python` values. If you need to support testing a given member
|
|
on a Python version that isn't supported by the rest of the workspace, you may need to use `uv pip`
|
|
to install that member in a separate virtual environment.
|
|
|
|
!!! note
|
|
|
|
As Python does not provide dependency isolation, uv can't ensure that a package uses its declared dependencies and nothing else. For workspaces specifically, uv can't ensure that packages don't import dependencies declared by another workspace member.
|