Skip to content

Repository files navigation

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 --pyargs module yourself — pytest tests/test_x.py behaves exactly as it always did;
  • testpaths is set in your config — that's a deliberate target list, so it wins;
  • uv isn't on PATH, or the directory isn't a uv workspace;
  • you pass --no-uv-workspace, or set uv_workspace = false in your ini.

Which members get collected

A member is collected when its distribution name maps to an importable module by the usual convention — my-packagemy_packageand 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

About

Collect tests across a uv workspace by import name, not by path

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages