gerenuk
Impact-based pytest selection for Python, powered by ty-find.
A diff comes in; gerenuk run runs exactly the tests that diff impacts, and
nothing else:
$ gerenuk run -- -q
gerenuk: 5 node id(s) from 1 origin(s) in 152 ms — details: gerenuk impacted-tests
......... [100%]
9 passed in 0.02s
It gets there by asking ty's type checker, through tyf, which symbols the
working tree changed and which tests can reach them — so the selection follows
Python's actual name resolution, not a name match. Anything gerenuk cannot see
through becomes "run the whole suite" rather than a confident short list.
📖 Documentation · 🤖 llms.txt
Install
uv add --dev "gerenuk[ty]"
ty-find comes with gerenuk — tyf is
what it drives. The [ty] extra adds ty
itself; without it tyf falls back to uvx ty, which works whenever uv is
around but fetches the checker on first use.
Verify the setup:
gerenuk doctor
Usage
gerenuk changed-symbols # what the working tree changed
gerenuk impacted-tests # and which tests that reaches
gerenuk run -- -q # and run exactly those, under pytest
gerenuk run --dry-run # the decision and the argv, no pytest
gerenuk audit pkg/module.py # separately: what nothing references
gerenuk guide # agent-facing instructions for where you stand
Not sure where to start? uvx gerenuk guide prints the setup instructions
without installing anything; once a madoqua step names gerenuk, the same
command prints how to read a report. Every page is under 60 lines.
The three selection stages are one pipeline, and run computes the whole thing
in-process — the first two are there to be read when a selection surprises you.
changed-symbols
Maps the working tree's diff to the Python symbols it changed — the first stage
of impact-based test selection. It needs only git; no tyf, no ty, no
Python environment.
$ gerenuk changed-symbols
base main (merge-base 5ddda1f)
changed symbols (2)
modified method mypkg.pipelines.enrich:Enricher.run src/mypkg/pipelines/enrich.py
added function mypkg.utils:parse_date src/mypkg/utils.py
module-level changes (1)
mypkg.pipelines.enrich src/mypkg/pipelines/enrich.py
--base defaults to the first of origin/main, main, master that exists;
the diff runs from merge-base(HEAD, base) to the working tree, staged and
unstaged alike. --format json emits the same data for scripting. Registry
decorators can be filtered out via [tool.gerenuk] ignore-decorators in
pyproject.toml. See the
documentation.
impacted-tests
Walks the reference graph from those changed symbols out to the tests that
reach them — the selection, minus the pytest invocation, which is
run's job.
$ gerenuk impacted-tests
base main (merge-base 5ddda1f)
verdict selected
impacted tests (2)
tests/test_enrich.py::test_run_enriches
← mypkg.pipelines.enrich:Enricher.run
tests/test_api.py::test_endpoint_returns_lines
← mypkg.api:enrich_endpoint ← mypkg.pipelines.enrich:Enricher.run
41 symbol(s) visited, 5 tyf call(s), 640 ms
The ← chain is the point: when a selection looks wrong, it names the edge to
blame, and tyf refs <symbol> confirms it by hand.
Every run emits a verdict. selected means the list is the answer;
run_all means run the whole suite, with a machine-readable reason — a
non-Python file changed, a file did not parse, tyf was unavailable or failed,
or a budget tripped. Both exit 0. Under-selecting silently is the one outcome
that would make the tool worse than not having it, so anything gerenuk cannot
see through becomes "run everything" rather than a short list.
See the documentation.
run
Turns that selection into a pytest invocation and runs it. gerenuk says one line for itself and then becomes pytest, so Ctrl-C, colours and the exit code are pytest's own.
$ gerenuk run -- -q
gerenuk: 5 node id(s) from 1 origin(s) in 152 ms — details: gerenuk impacted-tests
......... [100%]
9 passed in 0.02s
gerenuk owns the invocation rather than printing node ids for a shell to
interpolate, because a selection has three answers and an argument list has only
two: an empty pytest argv is "run everything". So pytest $(gerenuk …)
would run the entire suite in precisely the best case — the diff that impacts no
tests. Here that case spawns nothing and exits 0.
run is also where pytest's own rules get applied. A recorded symbol is often a
helper or a fixture rather than a test, so each name is checked against pytest's
collection conventions and trimmed — or widened to the whole file — until it is
one pytest will accept. And because pytest injects fixtures by name, which no
type checker can follow, conftest.py is parsed for its fixtures and their
consumers; the expansion keeps its audit trail:
tests/test_service.py::test_summary
← tests.conftest:shelter ← sample_pkg.service:describe
--dry-run prints the decision and the exact argv without spawning anything.
pytest-command = ["uv", "run", "pytest"] under [tool.gerenuk] names the
runner. See the
documentation.
Dead code: audit
The same reference graph answers the opposite question — what does nothing
reach? — so gerenuk audit asks it for every symbol in a file:
$ gerenuk audit sample_pkg/service.py
warn sample_pkg/service.py:43 func `legacy_export` has no references
note sample_pkg/service.py:34 method `ShelterService.seniors` is referenced only from tests (1)
1 file(s), 7 symbol(s) checked — 1 warn, 1 note
| Severity | Rule |
|---|---|
warn |
The symbol has no references anywhere |
note |
Every reference lives in a test file |
Only callable symbols are audited; _private and dunder names are skipped.
--format json emits the findings for scripting.
It is a verifier, not a sweep
vulture is the cheap repo-wide sweep for dead code: one pass over a whole project, no type checker involved. It works on names, so it cannot tell two same-named symbols apart, it flags dynamic and framework code that is very much alive, and it cannot tell you who references a symbol.
gerenuk audit asks ty's type checker instead, through tyf. References
resolve the way Python resolves them — docstrings, comments and same-named
symbols in other modules do not count — and each one comes back as a file and a
line you can open. That costs one tyf refs call per symbol and needs tyf
installed, which is why it takes the files you name rather than a whole
repository. It is shaped for confirming a specific suspicion.
The two fit together in that order:
vulture src/ # sweep: what might be dead
gerenuk audit src/suspect.py # confirm the file against resolved references
tyf refs one_symbol # or confirm a single name
# then delete
Exit codes carry the verdict: 0 nothing flagged, 1 findings reported, 2
the run could not complete. The split between 1 and 2 is what makes it
usable in CI — a failing check and a broken setup are different problems.
Referenced only from tests
The note is the finding a name-based scanner cannot produce at all: every
reference to the symbol exists, and every one of them is in a test file.
Production stopped calling it and its own tests are what keep it alive —
usually the residue of an unfinished refactor, and exactly the code that
survives a dead-code sweep forever. That finding is why audit earns a place
next to a repo-wide scanner rather than deferring to one entirely.
What it cannot see
gerenuk reports static references, plus the three edges a type checker
cannot draw and gerenuk models explicitly: conftest.py fixtures, registering
decorators, and renaming imports (from x import y as z, which tyf answers
for under y while the code says z). Everything else dynamic — registries
populated at runtime, getattr dispatch, __all__ star re-exports — stays
invisible.
So findings are leads, not a delete list. Confirm with tyf refs <symbol>
before deleting anything.
Registered functions
Nothing references a @app.command() or a @router.get() — the framework
holds the only handle. So gerenuk follows the decorator to its registrar (app,
router) and treats what reaches the registrar as reaching the function. A
decorator that only wraps (@property, @functools.wraps) is left alone, and a
registrar that cannot be resolved gives run_all with
reason: "decorator_dispatch" rather than a confident empty answer.
See ADR 0012.
Usage with Claude Code
Add this to your project's CLAUDE.md. Two lines is the whole of it — the exit
codes, the run_all verdict and the caveat above are in gerenuk --help and on
the documentation site, which is where an
agent that needs them should go:
### `gerenuk` — test selection and dead code
- `gerenuk run -- -q` runs only the tests the working tree's diff impacts
(`--dry-run` to inspect; `gerenuk impacted-tests` explains why).
- `gerenuk audit <file>` confirms a symbol vulture flagged is really unused;
its `only tests reach it` findings are ones vulture cannot produce.
Development
make review # fmt + clippy + rust tests + fixture pytest + audit + deny
make review-quick # skip the network checks
make test-impact # impacted-tests against the fixture, with a real tyf
make test-run # the whole pipeline, with a real tyf and a real pytest
make docs # build the mdBook site and llms.txt into docs/book/html
The test suite is hermetic: tests/common/mod.rs stubs tyf and pytest with
shell scripts and points GERENUK_TYF and GERENUK_PYTEST at them, so none of
tyf, ty or pytest is needed to run cargo test. See docs/dev/ARCHITECTURE.md
for the shape and docs/adr/ for why it is that shape.
License
MIT
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distributions
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file gerenuk-0.3.1.tar.gz.
File metadata
- Download URL: gerenuk-0.3.1.tar.gz
- Upload date:
- Size: 1.1 MB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0938a056fc46440f3b3a83c465526247f1815f05ed0aab26401cd8bdd8b3b727
|
|
| MD5 |
15baf8c7cc4767fd8e64231f37e98aab
|
|
| BLAKE2b-256 |
b6d730c04740c694ebe0a6bfd17ecb90553ee41274033adeae787cfb369ce31d
|
Provenance
The following attestation bundles were made for gerenuk-0.3.1.tar.gz:
Publisher:
release.yml on mojzis/gerenuk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
gerenuk-0.3.1.tar.gz -
Subject digest:
0938a056fc46440f3b3a83c465526247f1815f05ed0aab26401cd8bdd8b3b727 - Sigstore transparency entry: 2737424375
- Sigstore integration time:
-
Permalink:
mojzis/gerenuk@3f46746e6df3a08efe5f196a6651ab5251552da1 -
Branch / Tag:
refs/tags/v0.3.1 - Owner: https://github.com/mojzis
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@3f46746e6df3a08efe5f196a6651ab5251552da1 -
Trigger Event:
push
-
Statement type:
File details
Details for the file gerenuk-0.3.1-py3-none-win_amd64.whl.
File metadata
- Download URL: gerenuk-0.3.1-py3-none-win_amd64.whl
- Upload date:
- Size: 1.2 MB
- Tags: Python 3, Windows x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
82e47509c9f101298d2a3c865fa3a9c4977f7fa632b11be6e190c4dbb79d76e9
|
|
| MD5 |
ca9cb84c8963720a0845d881bcf20cf6
|
|
| BLAKE2b-256 |
e91e8039f351bcbc75f4b648ac2a98c58e832a3b41bbf354751b64300820fa5b
|
Provenance
The following attestation bundles were made for gerenuk-0.3.1-py3-none-win_amd64.whl:
Publisher:
release.yml on mojzis/gerenuk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
gerenuk-0.3.1-py3-none-win_amd64.whl -
Subject digest:
82e47509c9f101298d2a3c865fa3a9c4977f7fa632b11be6e190c4dbb79d76e9 - Sigstore transparency entry: 2737424484
- Sigstore integration time:
-
Permalink:
mojzis/gerenuk@3f46746e6df3a08efe5f196a6651ab5251552da1 -
Branch / Tag:
refs/tags/v0.3.1 - Owner: https://github.com/mojzis
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@3f46746e6df3a08efe5f196a6651ab5251552da1 -
Trigger Event:
push
-
Statement type:
File details
Details for the file gerenuk-0.3.1-py3-none-manylinux_2_28_x86_64.whl.
File metadata
- Download URL: gerenuk-0.3.1-py3-none-manylinux_2_28_x86_64.whl
- Upload date:
- Size: 1.3 MB
- Tags: Python 3, manylinux: glibc 2.28+ x86-64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d80528e96c195133a9cc2c7f0470413c97dceab86afd1437a213e96e78479c55
|
|
| MD5 |
71aa4dcf91c2ee18c131e8d8b161a3fc
|
|
| BLAKE2b-256 |
1bb92644c11616691979f0025be61269f4be04442c79fbd7d814b26b1ac59f32
|
Provenance
The following attestation bundles were made for gerenuk-0.3.1-py3-none-manylinux_2_28_x86_64.whl:
Publisher:
release.yml on mojzis/gerenuk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
gerenuk-0.3.1-py3-none-manylinux_2_28_x86_64.whl -
Subject digest:
d80528e96c195133a9cc2c7f0470413c97dceab86afd1437a213e96e78479c55 - Sigstore transparency entry: 2737424415
- Sigstore integration time:
-
Permalink:
mojzis/gerenuk@3f46746e6df3a08efe5f196a6651ab5251552da1 -
Branch / Tag:
refs/tags/v0.3.1 - Owner: https://github.com/mojzis
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@3f46746e6df3a08efe5f196a6651ab5251552da1 -
Trigger Event:
push
-
Statement type:
File details
Details for the file gerenuk-0.3.1-py3-none-macosx_11_0_arm64.whl.
File metadata
- Download URL: gerenuk-0.3.1-py3-none-macosx_11_0_arm64.whl
- Upload date:
- Size: 1.2 MB
- Tags: Python 3, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e33289016c58ec81b1afbac0c1ae59ce2caddde2264a29f46d70bce7f7030af2
|
|
| MD5 |
3decd70316fcc791bfaa74431322bb7c
|
|
| BLAKE2b-256 |
1d5a97341a2ab50e0fd73b812878d459bceec2edd54359555ba0ee4e4ce7afc2
|
Provenance
The following attestation bundles were made for gerenuk-0.3.1-py3-none-macosx_11_0_arm64.whl:
Publisher:
release.yml on mojzis/gerenuk
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
gerenuk-0.3.1-py3-none-macosx_11_0_arm64.whl -
Subject digest:
e33289016c58ec81b1afbac0c1ae59ce2caddde2264a29f46d70bce7f7030af2 - Sigstore transparency entry: 2737424443
- Sigstore integration time:
-
Permalink:
mojzis/gerenuk@3f46746e6df3a08efe5f196a6651ab5251552da1 -
Branch / Tag:
refs/tags/v0.3.1 - Owner: https://github.com/mojzis
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@3f46746e6df3a08efe5f196a6651ab5251552da1 -
Trigger Event:
push
-
Statement type: