Collect tests across a uv workspace by import name, not by path.
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.
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.
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.
uv add --dev pytest-uv-workspaceuv 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.
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.
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.
| 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 |
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__.pyThe __init__.py files are the important part — they are what let pytest derive a fully qualified
module name instead of guessing from the path.
MIT