Metadata-Version: 2.5
Name: pytest-uv-workspace
Version: 0.1.0
Summary: Collect tests across a uv workspace by import name, not by path
Project-URL: Homepage, https://github.com/elephantum/pytest-uv-workspace
Project-URL: Changelog, https://github.com/elephantum/pytest-uv-workspace/blob/main/CHANGELOG.md
Author-email: Andrey Tatarinov <a@tatarinov.co>
License-Expression: MIT
License-File: LICENSE
Keywords: monorepo,pyargs,pytest,uv,workspace
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: Pytest
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Topic :: Software Development :: Testing
Requires-Python: >=3.11
Requires-Dist: pytest>=8.1.1
Provides-Extra: uv
Requires-Dist: uv>=0.11.26; extra == 'uv'
Description-Content-Type: text/markdown

# pytest-uv-workspace

Collect tests across a [uv workspace](https://docs.astral.sh/uv/concepts/projects/workspaces/)
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:

```console
$ 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

```console
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-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:

```console
$ 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:

```console
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
