pytest-uv-workspace
Collect tests across a uv workspace by import name, not by path.
The problem
In a Python monorepo, both of pytest's import modes break down:
Default (prepend/append). A test file that isn't inside an importable package is named
after its own basename. Two members with tests/integration/conftest.py and no __init__.py
both want to be the top-level module conftest:
import file mismatch:
imported module 'conftest' has this __file__ attribute:
/repo/libs/pkg-a/tests/integration/conftest.py
which is not the same as the test file we want to collect:
/repo/libs/pkg-b/tests/integration/conftest.py
--import-mode=importlib. When a test file isn't inside a package, pytest falls back to a
rootdir-relative dotted name. Every intermediate directory becomes a module name component, so
a directory named platform/ (or profile/, test/, types/) shadows the stdlib module of that
name and collection blows up somewhere unrelated.
The fix
Keep tests inside the package, and address them by module name:
libs/my-package/
└── src/
└── my_package/
├── __init__.py
└── tests/
├── __init__.py
└── test_thing.py
Now the module is my_package.tests.test_thing under both import modes — globally unique, and
free of any intermediate directory name. Nothing in the path can collide or shadow.
Getting there means running pytest --pyargs my_package other_package ... and keeping that list
in sync by hand. This plugin does it for you.
What it does
On a bare pytest invocation it runs uv workspace metadata, maps each member to its import name,
checks that name actually imports from inside the member's directory, and appends the survivors as
--pyargs targets:
$ uv run pytest
uv workspace: collecting 3 package(s): my_package, other_package, shared_utils
...
That is the whole feature. It does not change your import mode, add fixtures, or touch collection in any other way.
Install
uv add --dev pytest-uv-workspace
uv must be on PATH (set UV to point elsewhere). If you'd rather pin it as a dependency,
install pytest-uv-workspace[uv].
Requires Python 3.11+ and pytest 8.1.1+. The pytest floor matters: 8.0's
--import-mode=importlib is not package-aware and still names modules from their
rootdir-relative path, so intermediate directories leak in even when your tests live
inside a package.
When it stays out of the way
Injection happens only on a bare invocation. The plugin does nothing if:
- you pass any path, node id, or
--pyargsmodule yourself —pytest tests/test_x.pybehaves exactly as it always did; testpathsis set in your config — that's a deliberate target list, so it wins;uvisn't onPATH, or the directory isn't a uv workspace;- you pass
--no-uv-workspace, or setuv_workspace = falsein your ini.
Which members get collected
A member is collected when its distribution name maps to an importable module by the usual
convention — my-package → my_package — and that module resolves to a location inside the
member's own directory. The location check is what stops an unrelated PyPI package of the same
name from being collected in its place.
Members that don't resolve are skipped and reported:
$ uv run pytest -v
uv workspace: collecting 2 package(s): my_package, shared_utils
uv workspace: skipped 1 member(s)
odd-member -> odd_member: not importable (is the workspace synced?)
So a member whose import name doesn't follow the convention (vedana-solar shipping solar_etl),
or one that exposes several top-level packages, opts out simply by not resolving. To include those,
give them a package matching their distribution name, or list them explicitly in testpaths.
If a workspace is found but nothing resolves, the plugin raises a UsageError rather than
letting pytest quietly fall back to collecting the entire monorepo — usually it means you need to
run uv sync.
Configuration
| Option | Default | Meaning |
|---|---|---|
--no-uv-workspace |
— | Disable discovery for this run |
uv_workspace (ini) |
true |
Disable discovery permanently |
UV (env) |
— | Path to the uv binary |
Migrating an existing monorepo
If your tests currently live in libs/my-package/tests/, move them under the package and add
__init__.py files:
git mv libs/my-package/tests libs/my-package/src/my_package/tests
touch libs/my-package/src/my_package/tests/__init__.py
The __init__.py files are the important part — they are what let pytest derive a fully qualified
module name instead of guessing from the path.
License
MIT
Release files for pytest-uv-workspace 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pytest_uv_workspace-0.1.0.tar.gz | 22.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pytest_uv_workspace-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 32.3 kB
Release files / pytest_uv_workspace-0.1.0.tar.gz
| Download URL | pytest_uv_workspace-0.1.0.tar.gz |
|---|---|
| Size | 22.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
5466ab8882c1b407b75a80e03ba6813a2117e0dfdc21261b53cdea9da3cca929
|
|
BLAKE2b-256 checksum How to use checksums |
19a505f1e1882fc29c580dc76686b6e189c958a4e10cd090bac8f94d0bcd7d4a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.9 {"installer":{"name":"uv","version":"0.12.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / pytest_uv_workspace-0.1.0-py3-none-any.whl
| Download URL | pytest_uv_workspace-0.1.0-py3-none-any.whl |
|---|---|
| Size | 9.9 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ee37e4ac0fd28beea72faaf9c428c80ff697d6297d166ad02174a96cc6b161ef
|
|
BLAKE2b-256 checksum How to use checksums |
15db23a60720add94c3f1965e8f028fbfcd2655b8d2bb5b12bbf376ae89c8ea6
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.9 {"installer":{"name":"uv","version":"0.12.9","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|