Skip to content

bstaber/uv-monorepo-example

Repository files navigation

Why a monorepo?

Using uv workspaces gives us a few nice benefits:

  • Shared lockfile: one uv.lock tracks all dependencies across apps and libs, ensuring consistent environments.
  • Local inter-dependencies: apps can depend on local libraries (dummy-lib, …) without publishing to PyPI. Dependencies are wired via [tool.uv.sources] and resolved automatically.
  • Targeted commands: run or sync in the scope of one member:
    uv run --package dummy-app python -m entry-point
    uv sync --package dummy-lib
    or apply tools across the whole repo:
    uvx ruff check .
  • Common tooling: formatters, linters, and test runners can be defined once (e.g. ruff, pytest) and applied everywhere.
  • Easy scaling: add more apps under apps/ or libs under libs/ / packages/ without extra boilerplate.
  • Release flexibility: you can:
    • develop locally with workspace resolution,
    • build wheels/sdists with uv build --package <name> for external distribution.

Structure

spincycle/
├── apps/
│   └── lab/
│       ├── pyproject.toml
│       └── src/lab/__main__.py
├── libs/
│   └── dummy_lib/
│       ├── pyproject.toml
│       └── src/dummy_lib/__init__.py
├── pyproject.toml
└── uv.lock

How it works

  • The root pyproject.toml defines a uv workspace with members under apps/* and libs/*.
  • Dependencies between members are declared normally (e.g. dependencies = ["dummy-lib"]), and resolved locally thanks to:
[tool.uv.sources]
"dummy-lib" = { workspace = true }
  • One shared uv.lock tracks versions for all members.
  • uv lets us target a specific member with --package.

You can add more libs by running

uv init libs/chocolatine --lib

Then add "chocolatine" to [tool.uv.sources] if other apps/libs could depend on it.

You can also add more apps by running: uv init apps/airfryer.

Usage

Install & lock

uv lock                        # update lockfile for all workspace members
uv sync --package dummy-app    # install only the "lab" app (and its deps)

Run an app

uv run --package dummy-app python -m entry_point

Test importing a library

uv run --package dummy-app python -c "import dummy_lib; print('ok', dummy_lib.__name__)"

Add dependencies

uv add --package dummy-app rich>=13,<14
uv add --package dummy-lib numpy>=2,<3

Testing with pytest

Add this file under libs/dummy_lib/tests/test_basic.py:

from dummy_lib import __name__

def test_import():
    assert __name__ == "dummy_lib"

Run tests for the library:

uv run --package dummy-lib pytest

Expected output:

collected 1 item
libs/dummy_lib/tests/test_basic.py .                      [100%]

1 passed in 0.02s

We can also run commands for the whole workspace:

uv run --all-packages pytest

Building

Similarly, we can build each target:

uv build --package dummy-lib

Output:

❯ uv build --package dummy-lib
Building source distribution...
Building wheel from source distribution...
Successfully built dist/dummy_lib-0.1.0.tar.gz
Successfully built dist/dummy_lib-0.1.0-py3-none-any.whl

Or all at once

uv build --all-packages

Output:

uv build --all-packages
[dummy-lib] Building source distribution...
[spincycle] Building source distribution...
[dummy-lib] Building wheel from source distribution...
[spincycle] Building wheel from source distribution...
Successfully built dist/dummy_lib-0.1.0.tar.gz
Successfully built dist/dummy_lib-0.1.0-py3-none-any.whl
Successfully built dist/spincycle-0.1.0.tar.gz
Successfully built dist/spincycle-0.1.0-py3-none-any.whl

About

Python monorepo example with uv

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages