Skip to content

fix(python): resolve aliased imports in subdirectories - #2946

Open
hopstreax wants to merge 2 commits into
Graphify-Labs:v8from
hopstreax:fix/2943-aliased-import-calls
Open

fix(python): resolve aliased imports in subdirectories#2946
hopstreax wants to merge 2 commits into
Graphify-Labs:v8from
hopstreax:fix/2943-aliased-import-calls

Conversation

@hopstreax

@hopstreax hopstreax commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #2943.

Cross-module calls edges were being dropped for Python aliased imports when
the imported module lived in a loose subdirectory.

For example:

# scripts/caller.py
import mod_a as m

def go_alias():
    return m.target()

with scripts/mod_a.py defining target().

The import was resolved, but the imported module remained represented by the
syntactic target mod_a instead of the canonical file node
scripts_mod_a. As a result, Python member-call resolution could not connect
m.target() to the target function.

Root Cause

Graphify already performs corpus-level canonicalization for Python package
imports, but loose-script imports from sibling modules were not being
repointed to their canonical file nodes.

The initial implementation attempted to resolve these imports directly
during AST extraction. Full-suite testing revealed that this could incorrectly
repoint ambiguous package imports and create self-loops for imports such as
import builtins.

The implementation was therefore moved to a dedicated post-extraction
repointing pass.

Implementation

Added _repoint_python_script_imports() to canonicalize bare imports from
non-package directories.

The extraction pipeline is now:

AST extraction
    ↓
id_remap
    ↓
package import repointing
    ↓
loose-script import repointing
    ↓
language resolvers

For:

scripts/caller.py
scripts/mod_a.py

with:

import mod_a as m

the import is repointed to:

scripts_mod_a

allowing the existing member-call resolver to produce:

scripts_caller_go_alias
    --calls-->
scripts_mod_a_target

The loose-script pass is directory-scoped and does not globally match module
filenames.

It also:

  • skips package directories handled by the existing package resolver;
  • avoids self-import resolution;
  • preserves existing ambiguous package import behavior;
  • uses the existing canonical file-node ID mechanism;
  • leaves imports_from handling to the existing symbol-resolution pipeline.

Tests

Added regression coverage for:

  • aliased imports from subdirectories;
  • unaliased module calls from subdirectories;
  • duplicate module names in separate loose-script directories;
  • warm-cache aliased imports;
  • from ... import ... behavior;
  • external/self-basename imports;
  • existing root-level aliased imports.

Real Behavior Proof

Exact command / steps:

uv run --frozen pytest tests/test_extract.py -k "alias or python_import or test_python_subdirectory_" -v

uv run --frozen pytest tests/test_src_layout_import_resolution.py::test_ambiguous_package_alias_is_not_repointed -vv

uv run --frozen pytest tests/test_import_self_loops.py::test_python_external_import_matching_current_basename_has_no_self_loop -vv

uv run --frozen pytest tests/test_extract.py -q

uv run ruff check graphify/ tests/

Minimal reproduction:

scripts/caller_alias.py
scripts/mod_a.py
# caller_alias.py
import mod_a as m

def go_alias():
    return m.target()

Observed result:

scripts_caller_alias --imports--> scripts_mod_a
scripts_caller_alias_go_alias --calls--> scripts_mod_a_target

The calls edge is emitted with EXTRACTED confidence.

No target_file or local_alias hints leak into the final graph.

Focused validation:

  • Python import/extraction tests: 13 passed
  • Ambiguous package alias regression: passed
  • External/self-basename regression: passed
  • tests/test_extract.py: 197 passed, 4 skipped
  • Ruff: all checks passed

The full repository suite was also run locally:

uv run --frozen pytest tests/ -q --tb=short

Result:

4835 passed, 41 skipped, 20 failed

The 20 failures were Windows/environment-specific tests involving Unix-only
primitives, Windows symlink privileges, encoding, file locking, and Windows
path-length behavior. No failures were observed in the Python import
resolution, extraction, build, merge, caching, or related regression areas.

Not tested

  • A Linux CI-equivalent full repository run was not available locally.

@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 3 advisory finding(s) below merit a look before merge.

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


Graphify review — findings

Resolves plain import mod statements in _import_python to the actual module file path (via _resolve_python_module_path) so subdirectory imports point at the canonical module node instead of a bare-name id, attaching a target_file hint when the file exists. Adds tests covering aliased/unaliased subdirectory imports, same-stem disambiguation across sibling directories, and warm-cache survival without transient target_file/local_alias hints leaking into output.

Worth a look

  • _import_python references str_path which may be undefinedgraphify/extract.py:327 · Escalate · high
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Python import edge target IDs became filesystem-path dependentgraphify/extract.py:328 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Python import resolution now depends on process-global extract rootgraphify/extract.py:328 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 1767 functions depend on the 575 functions this change touches.

Health — this change adds coupling hotspots:

  • new: extract() — 478 callers, 42 callees
  • new: _rebuild_code() — 98 callers, 50 callees
  • new: extract_xaml() — 19 callers, 17 callees
  • new: extract_js() — 80 callers, 3 callees
  • new: dispatch_command() — 2 callers, 119 callees
  • new: _get_extractor() — 26 callers, 6 callees
  • new: run_pipeline() — 8 callers, 13 callees
  • new: collect_files() — 17 callers, 6 callees
  • …and 23 more — each is listed as a finding

Verification — 1767 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: 1619 function(s) in the blast radius were not formally verified this run

Formal verification

Could not verify: Could not verify \_import\_python.

The verifier did not have enough to check \_import\_python, 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: not verifiable: all 200 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly AttributeError — names the real obstacle, not a sampling gap)

· 31 more finding(s) on lines outside this diff (see the check run).

@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

Adds _repoint_python_script_imports and wires it into extract() after the package-import repointing pass, redirecting bare import mod_a targets in non-package directories to their canonical sibling file nodes (e.g. scripts_mod_a) so member-call resolvers can bind cross-module calls edges (#2943). Guards against repointing inside package dirs, self-imports, and imports_from edges. Covers the new behavior with tests for aliased/unaliased subdir imports, same-stem disambiguation across directories, warm-cache survival, and imports_from being left untouched.

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

Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 1771 functions depend on the 579 functions this change touches.

Health — this change adds coupling hotspots:

  • new: extract() — 479 callers, 43 callees
  • new: _rebuild_code() — 98 callers, 50 callees
  • new: extract_xaml() — 19 callers, 17 callees
  • new: extract_js() — 80 callers, 3 callees
  • new: dispatch_command() — 2 callers, 119 callees
  • new: _get_extractor() — 26 callers, 6 callees
  • new: run_pipeline() — 8 callers, 13 callees
  • new: collect_files() — 17 callers, 6 callees
  • …and 23 more — each is listed as a finding

Verification — 1771 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: 1623 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

· 31 more finding(s) on lines outside this diff (see the check run).

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.

Cross-module calls edge is dropped for aliased imports when the modules live in a subdirectory

1 participant