Skip to main content

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 file 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

Metadata

Release files for gerenuk 0.4.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for gerenuk 0.4.0
File Size Uploaded
gerenuk-0.4.0.tar.gz 1.1 MB Details

Built distributions (wheels)

Table of built distributions (wheels) for gerenuk 0.4.0
File Interpreter ABI Platform
gerenuk-0.4.0-py3-none-win_amd64.whl Python 3 none Windows x86-64 Details
gerenuk-0.4.0-py3-none-manylinux_2_28_x86_64.whl Python 3 none Linux glibc 2.28+ x86-64 Details
gerenuk-0.4.0-py3-none-macosx_11_0_arm64.whl Python 3 none macOS 11.0+ ARM64 Details

Total release size: 4.8 MB

Release files / gerenuk-0.4.0.tar.gz

Download URL gerenuk-0.4.0.tar.gz
Size 1.1 MB
Tags Source
SHA-256 checksum
How to use checksums
2d8b764d1e5cb37e9ebff1f91d2f055b81bf72d1165bf1cab8984bbc3de68c99
BLAKE2b-256 checksum
How to use checksums
301f949638be7b94c82b2cbd87efd23c74243a5d5d53c3523b0ed28f9d915783
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 8, 2026.

Transparency log

Release files / gerenuk-0.4.0-py3-none-win_amd64.whl

Download URL gerenuk-0.4.0-py3-none-win_amd64.whl
Size 1.2 MB
Tags Python 3 Windows x86-64
SHA-256 checksum
How to use checksums
45de303a205b579dae9fcd3bb7287f79df5a2935e2777044953ba440f1879e85
BLAKE2b-256 checksum
How to use checksums
24f432011453efbb8a79a1398a30afd0ede1cf5cf413c2bf6a32e0b069557ad7
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 8, 2026.

Transparency log

Release files / gerenuk-0.4.0-py3-none-manylinux_2_28_x86_64.whl

Download URL gerenuk-0.4.0-py3-none-manylinux_2_28_x86_64.whl
Size 1.3 MB
Tags Linux glibc 2.28+ x86-64 Python 3
SHA-256 checksum
How to use checksums
df61163de15697b51542719df0cf4fb1a33ddc4d22c2eb32c92e5d3a0aeb6e9b
BLAKE2b-256 checksum
How to use checksums
6ae4b436a97d254bf14ee9bffb1f74464f20e1dc8880f3c5f294507040082814
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 8, 2026.

Transparency log

Release files / gerenuk-0.4.0-py3-none-macosx_11_0_arm64.whl

Download URL gerenuk-0.4.0-py3-none-macosx_11_0_arm64.whl
Size 1.2 MB
Tags Python 3 macOS 11.0+ ARM64
SHA-256 checksum
How to use checksums
323348fcaae785a44e888db70754bf146da58efe894e3da020a8eba7e792130d
BLAKE2b-256 checksum
How to use checksums
626a32d9850ac5a2340fbb29f428f58cc3c817859b957cc75cca5a5d2d125cfb
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 8, 2026.

Transparency log

Release history Release notifications | RSS feed

0.5.0

4 release files

This release

0.4.0 This release

4 release files

0.3.1

4 release files

0.3.0

4 release files

0.2.0

4 release files

0.1.0

4 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page