Metadata-Version: 2.4
Name: assert-pytest-fixture-name-is-needed
Version: 20260825093937
Summary: CLI tool to assert that every pytest fixture filed under a second name needs one
Author-email: 10U Labs <dev@10ulabs.com>
License-Expression: Apache-2.0
Project-URL: Homepage, https://github.com/10U-Labs/assert-pytest-fixture-name-is-needed
Project-URL: Repository, https://github.com/10U-Labs/assert-pytest-fixture-name-is-needed
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-pytest-fixture-name-is-needed

Assert that every pytest fixture filed under a name of its own needs one.

## Why

A `name` argument on a fixture decorator files that fixture under a name
other than its function's, and there is one reason to write it. pylint
objects when a name defined at the top of a module is bound again inside
a function in it, and a test asks for a fixture by taking a parameter of
the fixture's name, so renaming the function ends the clash.

Where nothing in the file binds that name there is no clash, and the
argument costs twice over. Whoever has one file open copies it into the
next, so the shape spreads. And it hides the fixture from every check
that reads for the name of a function, which is how a fixture nothing
calls any more stays in the tree: the check searches for the name of the
function and never for the name a test asks for.

So this tool reads the argument as a claim about the file it sits in and
checks it. A file whose fixtures all need their names passes silently.

## Installation

```bash
pip install assert-pytest-fixture-name-is-needed
```

## Usage

```bash
# Every test tree this repository publishes
assert-pytest-fixture-name-is-needed test

# The same, leaving out tests somebody else wrote
assert-pytest-fixture-name-is-needed 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 | Every filed name is needed |
| 1 | A filed name is needed by nothing |
| 2 | A tree was missing, unreadable, or would not parse |

## What counts as a fixture

A decorator that calls something named `fixture`, so `@pytest.fixture`,
a plain `@fixture` imported from pytest, and the same factory a plugin
re-exports are all read. A bare decorator with no call carries no
argument to be needless and is left alone, as is a `name` computed
rather than written out, which cannot be checked against anything.

A fixture filed under the name it already has is left alone too. The
argument there is redundant whatever the file binds, so it says nothing
about the bindings and this tool has nothing to report about it.

## What counts as a binding

Every name bound anywhere below the top level of the file, because
pylint objects to all of them alike:

| Bound by | Example |
| --- | --- |
| A parameter | `def test_x(config):` |
| An assignment | `config = build()` |
| A loop variable | `for config in rows:` |
| A context manager target | `with open(p) as config:` |
| A comprehension variable | `[config for config in rows]` |
| A caught exception | `except KeyError as config:` |
| An import | `from etc import config` |
| A match capture | `case {"a": config}:` |
| A nested definition | `def config():` |

The set has to be that wide. Narrowing it to parameters is the mistake
this tool exists to stop somebody making: two helpers assigning a local
named `config` beside a fixture filed as `config` are a real clash, and
a tool that asked only about parameters would tell the next reader to
rename the function and take two new pylint findings for it.

A comprehension and a lambda are scopes wherever they are written, so
what they bind counts even at the top level of the file. Everything else
written at the top level is the fixture's own neighbourhood rather than
an inner scope, and binds nothing this tool counts.

## 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.

## GitHub Actions

```yaml
- name: Assert every fixture name is needed
  uses: 10U-Labs/assert-pytest-fixture-name-is-needed@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
