python: resolve calls through an imported module name (M.f() / from a import b as M) - #334
python: resolve calls through an imported module name (M.f() / from a import b as M)#334DavinchiPes wants to merge 1 commit into
Conversation
`from analysis import pump_signature as P` followed by `P.find_pumps()` is the dominant call style in numeric Python (numpy's `import x as np`, generalised to a project's own modules). The extractor saw receiver `P`, found no type binding for it and dropped the edge — the right rule for an object of unknown class (trailhq#35), the wrong one for a module: `graft callers find_pumps` reported "no indexed callers" for a function with thirteen call sites. A module receiver is not an unknown object: the import statement names the exact file. So: - extract.ts collects, per Python file, every import that binds a MODULE to a local name (`import a.b as M`, `from a import b as M`, `from a import b`, `import a.b`, relative `from . import b`). A member call whose receiver did not resolve to a type but IS one of those names is emitted as a plain call carrying the module's dotted path as `specifier`. A receiver that resolved to a type keeps the typed-member path (a class alias is not a module). - resolve.ts resolves that specifier to a file (`a/b.py`, then `a/b/__init__.py`, under each ancestor directory of the caller nearest-first, so a `src/` layout needs no build-file parsing; relative imports climb from the caller's directory) and looks the name up inside that file only — a same-named function elsewhere in the repo can never become a false edge, and an ambiguous or missing name drops, as before. - pyReceiver also spells a pure dotted chain (`pkg.mod.f()`) so the `import pkg.mod` form matches. test/graph-python-module-alias.test.ts pins the four spellings plus the package-`__init__` case, and that neither a same-named decoy nor an untyped receiver gains an edge. Full suite: unchanged apart from the two new tests (claude-shim-resolve's four machine-dependent failures are pre-existing on main and untouched).
🌱 graft blast radius1 area changed → 16 areas can be affected. 226 dependent symbols, depth 2. flowchart TB
A0(("Graph Building<br/>65 symbols"))
A1(("Claude Integration<br/>24 symbols"))
A2(("Host Configuration<br/>24 symbols"))
A3(("CLI Orchestration<br/>17 symbols"))
A4(("Context Caching<br/>16 symbols"))
AX(("11 smaller areas<br/>80 symbols"))
classDef reached fill:#D9EDF3,stroke:#3AA7C9,stroke-width:1.5px,color:#0E313C;
class A0,A1,A2,A3,A4 reached;
classDef tail fill:#EEF2F3,stroke:#9AA4A9,stroke-width:1px,color:#3A4247;
class AX tail;
Who knows this code — 5 people across 17 areas
Ownership is git history over each area's own files, weighted towards recent work (120-day half-life). Merge commits and bots are dropped, and you are dropped from your own PR. A name with no All 226 dependent symbols, grouped by areaGraph Building — 65 symbols in 17 files
…165 further symbols not listed. Test signal per changed area — 1 ✓Reached = a node under a test path has a resolved edge into the changed symbol. It undercounts anything called indirectly — through a CLI, a spawned process or a dynamic import — so read a low ratio as “look here”, never as a coverage gate.
99 test suites also reference this code249 symbols, kept out of the diagram and the table so they cannot crowd out the areas a reviewer has to look at.
Open the interactive graph → — click an area to see its dependent symbols at file:line. |
Problem
from analysis import pump_signature as Pfollowed byP.find_pumps()is the dominant call style in numeric Python codebases (numpy'simport x as np, generalised to a project's own modules). The extractor saw receiverP, found no type binding for it and dropped the edge — the right rule for an object of unknown class (#35), the wrong one for a module. On our repograft callers find_pumpsreported "no indexed callers" for a function with thirteen call sites across 32 files that import this way;graft grepfound them all.Fix
A module receiver is not an unknown object: the import statement names the exact file.
import a.b as M,from a import b as M,from a import b,import a.b, relativefrom . import b). A member call whose receiver did not resolve to a type but is one of those names is emitted as a plain call carrying the module's dotted path asspecifier. A receiver that resolved to a type keeps the typed-member path (a class alias is not a module).a/b.py, thena/b/__init__.py, under each ancestor directory of the caller nearest-first, so asrc/layout needs no build-file parsing; relative imports climb from the caller's directory) and look the name up inside that file only. A same-named function elsewhere in the repo can never become a false edge; an ambiguous or missing name drops, as before — precision rule frommapclusters one entry per file instead of per directory #35 preserved.pyReceiveralso spells a pure dotted chain (pkg.mod.f()) so theimport pkg.modform matches.Tests
test/graph-python-module-alias.test.ts(TDD: written first, failed on main, passes now):__init__.pycase all produce anextractedcall edge into the module's file;obj.find_pumps()) still drops.Full suite: 1218/1222 — the four
claude-shim-resolvefailures are pre-existing onmain(machine-dependent global-install lookup) and untouched.Result on a real repo
Rebuilt the graph on a ~1 900-file Python repo:
graft callers find_pumps0 → 6 calling functions (13 sites),graft callers compute_all1 → 2; call counts in modules that use direct imports unchanged.🤖 Generated with Claude Code