Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

gerenuk changed-symbols

Map the working tree’s diff to the Python symbols it changed.

gerenuk changed-symbols [--base <REF>]

Unlike audit, this command needs only git — no tyf, no ty, no Python environment. It is the first stage of impact-based test selection: impacted-tests walks the reference graph from these symbols out to the tests that reach them.

git is taken from PATH unless GERENUK_GIT points at a specific binary.

Example

$ 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

ignored symbols (1)
  modified  function  mypkg.daily:normalize_prices         src/mypkg/daily.py  (@transformation)

module-level changes (1)
  mypkg.pipelines.enrich  src/mypkg/pipelines/enrich.py

non-Python changes (1)
  schema.sql

--format json emits the same information as a single object:

{
  "base": "main",
  "merge_base": "5ddda1f2cd66afdf789771b20a3bf667df78f050",
  "changed_symbols": [
    {
      "symbol": "mypkg.pipelines.enrich:Enricher.run",
      "file": "src/mypkg/pipelines/enrich.py",
      "kind": "method",
      "line": 42,
      "column": 9,
      "change": "modified"
    }
  ],
  "ignored_symbols": [
    {
      "symbol": "mypkg.daily:normalize_prices",
      "file": "src/mypkg/daily.py",
      "kind": "function",
      "line": 17,
      "column": 5,
      "change": "modified",
      "ignored_by": "transformation"
    }
  ],
  "module_level_changes": [
    {
      "module": "mypkg.pipelines.enrich",
      "file": "src/mypkg/pipelines/enrich.py"
    }
  ],
  "non_python_changes": ["schema.sql"],
  "test_files_changed": [],
  "errors": []
}

kind is function, method or class; change is added, modified or deleted; line and column are where the definition’s name starts, not its first decorator, and both are 1-based. Every array is sorted, so two runs on the same tree produce byte-identical output.

The schema is the interface between the phases: impacted-tests --changed report.json replays a saved report instead of diffing the working tree, which is why each entry carries enough to be walked from — a definition’s exact position, and a file for every module-level change. Every field above is required: a saved report is parsed strictly, so one that omits column is rejected rather than half-read.

What gets diffed

The range is merge-base(HEAD, <base>)working tree: staged and unstaged edits together, plus untracked files that .gitignore does not cover. Taking the merge base rather than the branch tip means work landing on main after you branched is not attributed to you.

--base defaults to the first of origin/main, main, master that exists. A --base you pass explicitly is used as given; if it does not resolve, the run fails with exit code 2 rather than falling back.

How lines become symbols

Changed line ranges come from git diff -U0, so there is no context to misattribute. Each line is mapped to the innermost definition that encloses it, parsed with tree-sitter-python.

Changed lineAttributed to
Anywhere in a function or method body, its signature, or its default valuesthat function or method
A decoratorthe definition it decorates
A class body, outside any method (attributes, dataclass fields)the class
Inside a nested function or closurethe enclosing named function
Inside a nested classOuter.Inner, and its methods Outer.Inner.method
Module level — imports, constants, module-level statementsmodule_level_changes

Blank lines are the one exception: they carry no meaning, and appending a function inserts two of them, so they are not treated as module-level changes. Docstring and comment edits are attributed normally — phase 1 does not try to detect semantic no-ops.

Symbols are named module.path:QualName. The module path is the unbroken chain of directories above the file that contain __init__.py, which resolves src-layout and flat layout identically. PEP 420 namespace packages fall back to the file path with a leading src/ removed.

Added, modified, deleted

A symbol’s verdict comes from whether it exists on each side of the diff, not from which side the hunk touched:

Old sideNew sideVerdict
presentpresentmodified
presentabsentdeleted
absentpresentadded

So deleting lines from a function that still exists is a modification — its callers were never orphaned. Deletions are found by parsing the old blob out of git, which is why a removed function can still be named.

Renames are not paired. git detects them, but a moved module is a new module: every symbol in the old path is reported deleted and every symbol in the new path added, so phase 2 still revisits the callers of the old name.

Files that are not analysed

Checked in this order, so the first row that matches wins:

KindWhere it goes
Anything that is not .py, including .pyi stubs and binariesnon_python_changes
Test files — a tests/test directory, or test_*.py / *_test.pytest_files_changed
Files that do not parseerrors, and the module is reported as module-level

The order matters for the phases downstream: a non-Python file under tests/ — a fixture .json, say — is a non-Python change, which makes impacted-tests answer run_all. A changed test file needs no symbol analysis: it will select itself later.

Ignoring registry decorators

Registry-style functions — registered by a decorator and invoked by a runner — are neither called directly nor covered by targeted tests, so chasing their callers is wasted work. List their decorators in the repository root’s pyproject.toml:

[tool.gerenuk]
ignore-decorators = ["transformation", "registry.task"]

Matching is syntactic dotted-suffix matching on the decorator expression, with or without call parentheses:

Config entryMatchesDoes not match
transformation@transformation, @transformation(...), @registry.transformation@transform
registry.transformation@registry.transformation, @a.registry.transformation@transformation

A matching decorator moves the symbol to ignored_symbols with the entry that matched, rather than dropping it. One match is enough: a symbol carrying both a matching and a non-matching decorator is still ignored. Ignoring applies to the decorated definition only, never to its neighbours.

Limitation: import aliases are not resolved. from registry import transformation as t followed by @t will not match, because the check never leaves the file’s syntax.

Exit codes

0 on any successful run, including one that reports hundreds of symbols — the output is an inventory, not a verdict. 2 when the run could not complete: not a git repository, or a --base that does not resolve.