Skip to content

fix(python): resolve cross-file inheritance by import (#2736) - #2737

Open
NithishKumar04 wants to merge 1 commit into
Graphify-Labs:v8from
NithishKumar04:fix/python-cross-file-inheritance-2736
Open

fix(python): resolve cross-file inheritance by import (#2736)#2737
NithishKumar04 wants to merge 1 commit into
Graphify-Labs:v8from
NithishKumar04:fix/python-cross-file-inheritance-2736

Conversation

@NithishKumar04

@NithishKumar04 NithishKumar04 commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Summary

  • preserve the explicit inherits [EXTRACTED] relationship for Python classes whose base is imported from another file
  • resolve aliases, duplicate class names, relative imports, package re-exports, and module-qualified/generic bases through exact import evidence
  • keep full and incremental extraction equivalent without copying unchanged context nodes into the fresh result
  • prevent the weaker legacy uses [INFERRED] edge from replacing or contradicting resolved inheritance

Fixes #2736.

Root cause

The per-file extractor already emitted an inherits [EXTRACTED] edge to a sourceless base stub. A later legacy Python import pass then emitted uses [INFERRED] for the same class/base pair. Graph assembly stores one edge per ordered pair, so the lexically later uses relationship replaced inherits and moved the evidence location from the class declaration to the import line.

The global unique-label rewire repaired only simple globally unique names. An import alias remained a ghost, while duplicate class names could leave inheritance unresolved and the legacy pass could choose the wrong module by first-writer order.

Approach

  • add a Python inheritance resolver beside the existing language-specific type resolvers, before the generic unique-label rewire
  • index only sourced Python class definitions and resolve each base through its exact module path and imported symbol
  • recursively follow package re-exports, retain qualified module names, and unwrap generic base subscriptions
  • use unchanged graph nodes and edges as lookup-only context during incremental extraction
  • repoint the existing extracted edge instead of adding a parallel edge, then remove a stub only when nothing still references it
  • refuse multiple definition candidates or competing conditional import origins rather than selecting an arbitrary target
  • suppress only the legacy inferred uses edge from the exact import statement that binds an inherited base

Regression coverage

Nine tests cover:

  1. the issue's direct-import repro through final graph construction
  2. from ... import ... as ... without a ghost node
  3. duplicate class names resolving to the explicitly imported module
  4. relative aliases through a package re-export
  5. module-qualified generic bases
  6. changed-child to unchanged-base incremental resolution
  7. ambiguous unimported bases remaining unresolved
  8. competing conditional imports remaining unresolved
  9. a same-named non-base import retaining its legitimate inferred uses edge

Verification

  • 4348 passed, 45 skipped - full suite with isolated Git configuration and the required optional dependency
  • focused regression suite passes on Python 3.10 and 3.12
  • ruff check graphify tests
  • all generated-artifact checks: --check, --audit-coverage, --schema-singleton, --monolith-roundtrip, and --always-on-roundtrip
  • repository graph refreshed successfully after the code changes

Related work

@NithishKumar04

Copy link
Copy Markdown
Contributor Author

@safishamsi This incorporates your review feedback on #1231 by repointing the existing inheritance edge and removing only the now-unreferenced ghost, rather than layering a second edge. The import resolver is conservative for duplicate definitions and competing conditional imports, and the workflow is currently awaiting maintainer approval to run. I would appreciate your review of the resolver placement and ambiguity guard.

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Graphify reviewed this change.

Worth a look — the grounded gate found no coupling regressions or blocking issues, but 1 advisory finding(s) below merit a look before merge.

Formal verification. No changes could be formally verified in this run.


Graphify review — findings

This PR adds a dedicated resolution pass for Python class inheritance so that base classes brought in via import/from ... import statements are linked to their exact definition nodes rather than falling back to bare-name stubs. It introduces _resolve_python_inheritance_references in the resolution module, a _python_base_reference helper in the engine to extract qualified/generic base expressions, and wires the new pass into extract.py ahead of the generic stub-rewire and cross-file import steps, with logic to prevent the weaker uses [INFERRED] edges from overwriting the inherits [EXTRACTED] edges. The surface area spans extract.py, extractors/engine.py, extractors/resolution.py, and a CHANGELOG entry. The changed symbol list also includes many unrelated changelog/rationale entries across other languages (Scala, ObjC, Swift, C++, TS, Java, etc.), though the visible diff centers on the Python inheritance change.

Worth a look

  • Label-match dedup can drop legitimate cross-file uses edgesgraphify/extract.py:6060 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review Execution auto-disposal is off for this run; enable it (with sandbox isolation) to have Graphify try to confirm or refute this automatically.
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 2048 functions depend on the 723 functions this change touches.

Health — this change adds coupling hotspots:

  • worse: extract() — 454 callers, 42 callees
  • worse: walk() — 1 callers, 55 callees
  • new: _resolve_python_inheritance_references() — 2 callers, 7 callees

Verification — 2048 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 1908 function(s) in the blast radius were not formally verified this run

Formal verification

Could not verify: Could not verify extract.

The verifier did not have enough to check extract, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `cache_root` is annotated `Path | None` — outside the synthesizable primitive/collection set

Could not verify: Could not verify \_extract\_generic.

The verifier did not have enough to check \_extract\_generic, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set

· 1 grounded finding(s) anchored inline below; 2 more finding(s) on lines outside this diff (see the check run).

_apply_symbol_resolution_facts(paths, nodes, edges, root, facts)


def _resolve_python_inheritance_references(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression_resolve_python_inheritance_references()

fans out to 7 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The seven callees are the existing parsing, module-resolution, and type-classification helpers used by this cohesive resolution pass. I did not introduce a wrapper or split the pass solely to change the coupling metric, since that would move rather than reduce the dependencies. The correctness advisory from the same review is addressed in 9ed6cc8 with focused regression coverage.

@NithishKumar04
NithishKumar04 force-pushed the fix/python-cross-file-inheritance-2736 branch from 5936018 to 9ed6cc8 Compare August 14, 2026 10:51
@NithishKumar04

Copy link
Copy Markdown
Contributor Author

I validated the same-label dedup advisory. The new regression imports two distinct classes both named Base, inherits one, and legitimately uses the other; it failed before the fix because the label-wide filter removed both inferred edges. In 9ed6cc8, suppression is keyed by source class, source file, import line, and imported symbol, so only the base-binding import is suppressed. The existing duplicate-name and competing-conditional-import protections remain intact. Verification: 9 focused regressions pass, the full suite passes with 4,348 passed / 45 skipped, and Ruff is clean.

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Graphify reviewed this change.

Looks safe to merge — no coupling regressions and no blocking issues, checked against the code graph (not a self-assessment).

Formal verification. No changes could be formally verified in this run.


Graphify review — findings

This PR adds Python class inheritance resolution so that classes inheriting from an imported base retain an inherits [EXTRACTED] edge to the actual base definition rather than falling back to a uses [INFERRED] edge. It introduces a new _resolve_python_inheritance_references resolver (wired into extract) that matches inherited base classes to their defining nodes using exact import evidence, adds a _python_base_reference helper in the engine to recognize bare/qualified/generic base expressions, and changes the cross-file import handling to suppress the legacy uses edges the new resolver has already claimed. A CHANGELOG entry and related tests are also included. The surface area spans extract.py (orchestration and edge suppression), extractors/engine.py (base-expression parsing), extractors/resolution.py (the new resolver, including handling of aliases, re-exports, and resolution-context lookups for incremental rebuilds), and Python inheritance resolution tests.

No blocking issues surfaced. 3 lower-confidence candidates did not survive cross-model review.

Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 2049 functions depend on the 724 functions this change touches.

Health — this change adds coupling hotspots:

  • worse: extract() — 455 callers, 42 callees
  • worse: walk() — 1 callers, 55 callees
  • new: _resolve_python_inheritance_references() — 2 callers, 8 callees

Verification — 2049 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 1909 function(s) in the blast radius were not formally verified this run

Formal verification

Could not verify: Could not verify extract.

The verifier did not have enough to check extract, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `cache_root` is annotated `Path | None` — outside the synthesizable primitive/collection set

Could not verify: Could not verify \_extract\_generic.

The verifier did not have enough to check \_extract\_generic, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `path` is annotated `Path` — outside the synthesizable primitive/collection set

· 1 grounded finding(s) anchored inline below; 2 more finding(s) on lines outside this diff (see the check run).

_apply_symbol_resolution_facts(paths, nodes, edges, root, facts)


def _resolve_python_inheritance_references(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Health regression_resolve_python_inheritance_references()

fans out to 8 callees (efferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Python: AST extractor misses ~53% of Python class inheritance edges (cross-file base classes)

1 participant