This is an example repo of how to use a uv Workspace effectively.
It's a companion to the blog post Beyond Hypermodern: Python is easy now.
And builds on the single-package repo example at carderne/postmodern-python.
- Hit the green
Use this template
button up on the right next to the stars - Give your new repository a name and then clone it to your dev environment.
- Run around renaming stuff in the pyproject.toml files.
- Run
uv sync --all-packages
- Have a look at the stuff below here, try out some commands and edit this README as you like!
Clone:
git clone [email protected]:carderne/postmodern-mono.git
Using uv for development:
curl -LsSf https://astral.sh/uv/install.sh | sh
Install Python and dependencies:
uv sync --all-packages
Read on below...
There are three packages split into libs and apps:
- libs: importable packages, never run independently, do not have entry points
- apps: have entry points, never imported
Note that neither of these definitions are enforced by anything in Python or uv
.
> tree
.
├── pyproject.toml # root pyproject
├── uv.lock
├── libs
│ └── greeter
│ ├── pyproject.toml # package dependencies here
│ └── postmodern # all packages are namespaced
│ └── greeter
│ └── __init__.py
└── apps
├── server
│ ├── pyproject.toml # this one depends on libs/greeter
│ ├── Dockerfile # and it gets a Dockerfile
│ └── postmodern
│ └── server
│ └── __init__.py
└── mycli
├── pyproject.toml # this one has a cli entrypoint
└── postmodern
└── mycli
└── __init__.py
The Dockerfile is at apps/server/Dockerfile.
Build the Docker image from the workspace root, so that it has access to all libraries:
docker build --tag=postmodern-server -f apps/server/Dockerfile .
And run it:
docker run --rm -it postmodern-server
To make life easier while you're working across the workspace, you should run:
uv sync --all-packages
uv's sync behaviour is as follows:
- If you're in the workspace root and you run
uv sync
, it will sync only the dependencies of the root workspace, which for this kind of monorepo should be bare. This is not very useful. - If you're in eg
apps/myserver
and runuv sync
, it will sync only that package. - You can run
uv sync --package=postmodern-server
to sync only that package. - You can run
uv sync --all-packages
to sync all packages.
You can add an alias to your .bashrc
/.zshrc
if you like:
alias uvs="uv sync --all-packages
You'll notice that apps/mycli
has urllib3
as a dependency.
Because of this, every package in the workspace is able to import urllib3
in local development,
even though they don't include it as a direct or transitive dependency.
This can make it possible to import stuff and write passing tests, only to have stuff fail in production, where presumably you've only got the necessary dependencies installed.
There are two ways to guard against this:
-
If you're working only on eg
libs/server
, you can sync only that package (see above). This will make your LSP shout and your tests fail, if you try to import something that isn't available to that package. -
Your CI (see example in .github/workflows) should do the same.
This repo has all the global dev dependencies (pyright
, pytest
, ruff
etc) in the root
pyproject.toml, and then repeated again in each package without their version specifiers.
It's annoying to have to duplicate them, but at least excluding the versions makes it easier to keep things in sync.
The reason they have to be repeated, is that if you follow the example above and install only a specific package, it won't include anything specified in the root package.
Poe the Poet is used until uv includes its own task runner.
Tasks are defined in the root pyproject.toml, mostly running again ${PWD}
so that if
you run a task from within a package, it'll only run for that package.
You can run the tasks as follows:
uv run poe fmt
lint
check
test
# or to run them all
uv run poe all
If you run any of these from the workspace root, it will run for all packages, whereas if you run them inside a package, they'll run for only that package.
This repo includes a simple pytest test for each package.
To test all packages:
uv sync --all-packages
uv run poe test
To test a single package:
cd apps/server
uv sync
uv run poe test
(This repo actually uses basedpyright.
The following needs to be included with every package pyproject.toml
:
[tool.pyright]
venvPath = "../.." # point to the workspace root where the venv is
venv = ".venv"
strict = ["**/*.py"]
pythonVersion = "3.13"
Then you can run uv run poe check
as for tests.