Metadata-Version: 2.4
Name: assert-no-pytest-plugin-declarations
Version: 20260902062339
Summary: CLI tool to assert that no conftest declares a pytest plugin
Author-email: 10U Labs <dev@10ulabs.com>
License-Expression: Apache-2.0
Project-URL: Homepage, https://github.com/10U-Labs/assert-no-pytest-plugin-declarations
Project-URL: Repository, https://github.com/10U-Labs/assert-no-pytest-plugin-declarations
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Quality Assurance
Classifier: Topic :: Software Development :: Testing
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE.txt
Dynamic: license-file

# assert-no-pytest-plugin-declarations

Assert that no conftest declares a pytest plugin.

## Why

pytest loads a plugin once. Whether a given `pytest_plugins` line is
doing anything is therefore decided by files other than the one it is
written in — by a module in another directory, or by a conftest further
up the path that has already asked for the same module. Reading the
line tells you nothing about whether it is load-bearing, and no amount
of reading the file around it will either.

A fixture has two ways to reach a suite that do not have that problem.
It can be inherited, by sitting in a conftest above the suite that wants
it. Or it can be imported, by a `from ... import ...` written in the
conftest that needs it. Both are ordinary Python that ordinary tools
follow: pylint reports the import nothing uses, mypy resolves it, and
the sibling tool `assert-pytest-fixture-name-is-needed` reads the name
it binds. A `pytest_plugins` line is a second mechanism for the same
job that none of the three can see through.

It also holds package dependencies up, silently. A declaration that
registers no fixture at all still imports its module, and so keeps that
module's own imports load-bearing with nothing anywhere saying so. The
line cannot be deleted without a reader first working out, across the
whole tree, what would stop being imported if it went.

So the rule is the whole of it. Not a redeclaration, not a duplicate of
one further up: any declaration at all.

## Installation

```bash
pip install assert-no-pytest-plugin-declarations
```

## Usage

```bash
# Every test tree this repository publishes
assert-no-pytest-plugin-declarations test

# The same, leaving out tests somebody else wrote
assert-no-pytest-plugin-declarations test \
  --exclude 'test/vendor/*'
```

### Options

| Option | Effect |
| --- | --- |
| `--exclude PATTERNS` | Comma-separated globs to leave out. |
| `--annotate` | Print each finding as a GitHub Actions `::error` annotation. |
| `--quiet` | Print nothing; report through the exit code. |
| `--count` | Print only how many findings there were. |
| `--verbose` | Print the files read, the findings and a summary. |
| `--fail-fast` | Stop at the first finding. |
| `--warn-only` | Always exit 0. |

### Exit codes

| Code | Meaning |
| --- | --- |
| 0 | No file declares a pytest plugin |
| 1 | A file declares a pytest plugin |
| 2 | A tree was missing, unreadable, or would not parse |

## What counts as a declaration

Any assignment to the name `pytest_plugins`, whatever it is assigned:

| Written as | Example |
| --- | --- |
| A list | `pytest_plugins = ["fixtures.aws"]` |
| A string | `pytest_plugins = "fixtures.aws"` |
| An annotation | `pytest_plugins: list[str] = []` |
| An addition | `pytest_plugins += ["fixtures.aws"]` |
| One target of several | `pytest_plugins = names = []` |

The value is never inspected, because the declaration this tool most
wants to refuse is the one whose value cannot be worked out by reading:
a name built elsewhere, or a list a loop appends to. An empty list is a
finding like any other. It registers nothing today and is an invitation
to the next person to add an entry rather than a conftest.

## What is walked

A directory argument is read recursively for `*.py` files. `.git`,
`__pycache__` and `node_modules` are skipped, and everything else you
want left out goes in `--exclude`. Each file is read on its own and
needs nothing from any other, so a single file is as valid an argument
as a tree.

Every Python file is read, not only files named `conftest.py`. pytest
honours the name in a conftest and in a test module, and a line that
sits anywhere else is one that starts working the day the file is
renamed or its contents are moved.

## GitHub Actions

```yaml
- name: Assert no conftest declares a pytest plugin
  uses: 10U-Labs/assert-no-pytest-plugin-declarations@latest
  with:
    trees: test
```

`annotate` defaults to true there, so each finding lands on the line it
names in the diff.

## License

Apache-2.0
