Skip to content

fix(python): resolve nested scan-root imports via namespace projection (#3843) - #3867

Closed
nikhilsaxena04 wants to merge 1 commit into
Graphify-Labs:v8from
nikhilsaxena04:fix/python-scan-root-3843
Closed

nikhilsaxena04 wants to merge 1 commit into
Graphify-Labs:v8from
nikhilsaxena04:fix/python-scan-root-3843

Conversation

@nikhilsaxena04

Copy link
Copy Markdown
Contributor

What does this PR do?

Resolves #3843 (Monorepo Scan-Root Resolution)

This PR resolves absolute imports originating from within a namespace package when that package itself sits at the top of the scan root. Previously, absolute imports like from Company.Apps.Jobs.core import XYZ failed to resolve if Company.Apps.Jobs.Team was the namespace that the scanner was rooted in, resulting in dropped edges in cross-repo setups.

It implements a bottom-up __init__.py climb through _infer_scan_root_namespace() to logically infer the package prefix of the scan root itself. Absolute imports containing this prefix have it gracefully stripped during fallback resolution, correctly wiring the graph structure regardless of namespace depth.

Type of change

  • Bug fix
  • New feature
  • Documentation
  • Tests or CI
  • Refactor
  • Security fix

Verification & Invariants

Invariants Protected:

  • We ensure we do not infinitely climb into the generic filesystem (/); the climb cleanly bounds itself to explicit __init__.py package chains.

  • Cached inferences are strictly tied to the fully-resolved root and are cleanly reset during standard extract() passes to avoid stale state if project structure morphs mid-session.

  • Read the CONTRIBUTING.md guide.

  • Reproduced the issue and identified the invariant.

  • Made the smallest fix necessary.

  • Added a regression test (if bug fix) or isolated boundary test.

  • Kept the PR description synchronized with the final implementation.

  • Documented any limitations / unsupported cases explicitly.

How was this tested?

Added 6 specific regression tests that construct complex virtual namespaces and assert perfect edge tracking/fallback mapping.

pytest tests/test_python_import_resolution.py

@github-actions

Copy link
Copy Markdown

Thanks for the pull request, @nikhilsaxena04. A maintainer will review it soon.

Want to talk it through while it is in review? Come join us on our Discord server. For longer-form discussion there is also GitHub Discussions.

A couple of things that speed up review: make sure the test suite passes on Python 3.10 and 3.13, and that the change keeps extraction deterministic.

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


Graphify review — findings

Resolves absolute Python imports that reference the scan root's own modules when the scan root is nested inside a package hierarchy (#3843): _infer_scan_root_namespace walks upward from root collecting ancestor directory names while each has an __init__.py, and both _resolve_python_module_path and _resolve_python_namespace_dir now strip that inferred dotted prefix and re-probe relative to root when the direct probe misses. Namespace inference is memoized in _SCAN_ROOT_NAMESPACE_CACHE, keyed by resolved root path and cleared per run in extract. Third-party imports and partial-prefix matches are left unresolved, and a repo-root or non-package scan infers an empty namespace so its behaviour is unchanged.

Worth a look

  • Exact scan-root namespace imports are not projected — graphify/extractors/resolution.py:2404 · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Unsynchronized cache check-then-get can raise during concurrent cache clear — graphify/extractors/resolution.py:2350 · 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 — 2429 functions depend on the 438 functions this change touches.

Health — this change adds coupling hotspots:

  • new: extract() — 712 callers, 48 callees
  • new: _rebuild_code() — 144 callers, 55 callees
  • new: _extract_generic() — 18 callers, 29 callees
  • new: extract_js() — 87 callers, 4 callees
  • new: extract_xaml() — 19 callers, 17 callees
  • new: _resolve_js_module_path() — 34 callers, 9 callees
  • new: main() — 98 callers, 3 callees
  • new: dispatch_command() — 2 callers, 125 callees
  • …and 52 more — each is listed as a finding

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

Test selection

Test selection

132 of 302 test file(s) selected (44%) via static blast radius.

  • tests/test_astro_extraction.py — impact
  • tests/test_astro_import_ids.py — impact
  • tests/test_build.py — impact
  • tests/test_builtin_global_type_refs.py — impact
  • tests/test_case_sensitive_resolution.py — impact
  • tests/test_cjs_module_extension.py — impact
  • tests/test_cobol_extractor.py — impact
  • tests/test_cpp_nested_and_cli.py — impact
  • tests/test_cpp_objc_cross_file_calls.py — impact
  • tests/test_cross_extension_reexport_self_cycle.py — impact
  • tests/test_cross_language_call_resolution.py — impact
  • tests/test_cross_repo_external_call_guards.py — impact
  • tests/test_cross_repo_member_calls.py — impact
  • tests/test_csharp_call_site_generic_args.py — impact
  • tests/test_csharp_enum_members.py — impact
  • tests/test_csharp_field_generic_args.py — impact
  • tests/test_csharp_generic_callsites.py — impact
  • tests/test_csharp_interface_dispatch.py — impact
  • tests/test_csharp_member_calls.py — impact
  • tests/test_csharp_member_nodes.py — impact
  • tests/test_csharp_object_creation.py — impact
  • tests/test_csharp_partial_classes.py — impact
  • tests/test_csharp_type_resolution.py — impact
  • tests/test_definition_file_portability.py — impact
  • tests/test_detect.py — impact
  • tests/test_dotnet.py — impact
  • tests/test_duplicate_annotation_edges.py — impact
  • tests/test_elixir_import_resolution.py — impact
  • tests/test_erlang_extractor.py — impact
  • tests/test_extract.py — impact
  • tests/test_extract_cache_location.py — impact
  • tests/test_extract_php_closures.py — impact
  • tests/test_file_label_disambiguation.py — impact
  • tests/test_file_node_id_spec.py — impact
  • tests/test_forwarding_review_findings.py — impact
  • tests/test_go_builtin_call_targets.py — impact
  • tests/test_go_import_repoint.py — impact
  • tests/test_go_interface_methods.py — impact
  • tests/test_go_qualified_resolution.py — impact
  • tests/test_import_extension_resolution.py — impact
  • tests/test_import_self_loops.py — impact
  • tests/test_imported_export_forwarding.py — impact
  • tests/test_incremental.py — impact
  • tests/test_indirect_call_arrow_single_param_shadow.py — impact
  • tests/test_indirect_call_block_scoped_shadow.py — impact
  • tests/test_indirect_call_catch_binding_shadow.py — impact
  • tests/test_indirect_call_external_import_shadow.py — impact
  • tests/test_indirect_call_for_of_binding_shadow.py — impact
  • tests/test_indirect_call_function_expression_shadow.py — impact
  • tests/test_indirect_call_nested_closure_shadow.py — impact
  • … and 82 more

Selection is safe under the controlled-regression assumption; always-run tests + a periodic full run are the backstops. Advisory — it never changes the check verdict.

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

Graphify-Labs#3843)

When the scan root is a sub-directory of a monorepo package hierarchy
(e.g., root = Company/Apps/Jobs/Team/), absolute imports using the full
dotted namespace (from Company.Apps.Jobs.Team.lib import delivery) were
unresolved because the scan root probed root/Company/Apps/Jobs/Team/lib
which doesn't exist inside root.

_infer_scan_root_namespace walks above the scan root collecting ancestor
directory names that contain __init__.py, establishing the root's
namespace prefix. When an absolute import's module name starts with that
prefix, the prefix is stripped and the remainder is re-probed relative
to root.

The existing root/rel fast path, ancestor walk (Graphify-Labs#2072), ambiguous import
guard (Graphify-Labs#3729), and phantom-edge retraction (Graphify-Labs#3784) are all untouched.
Third-party imports cannot false-match because the prefix check requires
a strict dotted prefix plus a remaining local component.

Co-Authored-By: Google Antigravity <noreply@google.com>
@nikhilsaxena04
nikhilsaxena04 force-pushed the fix/python-scan-root-3843 branch from 1443af3 to 640e39e Compare September 26, 2026 19:35

@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).


Graphify review — findings

Resolves absolute Python imports when the scan root sits inside a package hierarchy: _infer_scan_root_namespace walks upward from the root collecting ancestor names while each has an __init__.py, and _resolve_python_module_path/_resolve_python_namespace_dir strip that dotted prefix and re-probe relative to the root, so from Company.Apps.Jobs.Team.lib import delivery resolves when only Team/ is scanned. Third-party imports and partial prefix matches (Company.AppService.foo) are left unresolved, and roots at or above the package boundary infer an empty namespace and behave as before. Inferred namespaces are cached per resolved root path in _SCAN_ROOT_NAMESPACE_CACHE, which extract clears each run.

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

Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 2429 functions depend on the 438 functions this change touches.

Health — this change adds coupling hotspots:

  • new: extract() — 712 callers, 48 callees
  • new: _rebuild_code() — 144 callers, 55 callees
  • new: _extract_generic() — 18 callers, 29 callees
  • new: extract_js() — 87 callers, 4 callees
  • new: extract_xaml() — 19 callers, 17 callees
  • new: _resolve_js_module_path() — 34 callers, 9 callees
  • new: main() — 98 callers, 3 callees
  • new: dispatch_command() — 2 callers, 125 callees
  • …and 52 more — each is listed as a finding

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

Test selection

Test selection

132 of 302 test file(s) selected (44%) via static blast radius.

  • tests/test_astro_extraction.py — impact
  • tests/test_astro_import_ids.py — impact
  • tests/test_build.py — impact
  • tests/test_builtin_global_type_refs.py — impact
  • tests/test_case_sensitive_resolution.py — impact
  • tests/test_cjs_module_extension.py — impact
  • tests/test_cobol_extractor.py — impact
  • tests/test_cpp_nested_and_cli.py — impact
  • tests/test_cpp_objc_cross_file_calls.py — impact
  • tests/test_cross_extension_reexport_self_cycle.py — impact
  • tests/test_cross_language_call_resolution.py — impact
  • tests/test_cross_repo_external_call_guards.py — impact
  • tests/test_cross_repo_member_calls.py — impact
  • tests/test_csharp_call_site_generic_args.py — impact
  • tests/test_csharp_enum_members.py — impact
  • tests/test_csharp_field_generic_args.py — impact
  • tests/test_csharp_generic_callsites.py — impact
  • tests/test_csharp_interface_dispatch.py — impact
  • tests/test_csharp_member_calls.py — impact
  • tests/test_csharp_member_nodes.py — impact
  • tests/test_csharp_object_creation.py — impact
  • tests/test_csharp_partial_classes.py — impact
  • tests/test_csharp_type_resolution.py — impact
  • tests/test_definition_file_portability.py — impact
  • tests/test_detect.py — impact
  • tests/test_dotnet.py — impact
  • tests/test_duplicate_annotation_edges.py — impact
  • tests/test_elixir_import_resolution.py — impact
  • tests/test_erlang_extractor.py — impact
  • tests/test_extract.py — impact
  • tests/test_extract_cache_location.py — impact
  • tests/test_extract_php_closures.py — impact
  • tests/test_file_label_disambiguation.py — impact
  • tests/test_file_node_id_spec.py — impact
  • tests/test_forwarding_review_findings.py — impact
  • tests/test_go_builtin_call_targets.py — impact
  • tests/test_go_import_repoint.py — impact
  • tests/test_go_interface_methods.py — impact
  • tests/test_go_qualified_resolution.py — impact
  • tests/test_import_extension_resolution.py — impact
  • tests/test_import_self_loops.py — impact
  • tests/test_imported_export_forwarding.py — impact
  • tests/test_incremental.py — impact
  • tests/test_indirect_call_arrow_single_param_shadow.py — impact
  • tests/test_indirect_call_block_scoped_shadow.py — impact
  • tests/test_indirect_call_catch_binding_shadow.py — impact
  • tests/test_indirect_call_external_import_shadow.py — impact
  • tests/test_indirect_call_for_of_binding_shadow.py — impact
  • tests/test_indirect_call_function_expression_shadow.py — impact
  • tests/test_indirect_call_nested_closure_shadow.py — impact
  • … and 82 more

Selection is safe under the controlled-regression assumption; always-run tests + a periodic full run are the backstops. Advisory — it never changes the check verdict.

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

@nikhilsaxena04

Copy link
Copy Markdown
Contributor Author

Hi @safishamsi Hope you're having a great day!

Just a heads-up that I force-pushed a quick update to address both of the advisory findings caught by the automated review agent:

  1. Switched the _SCAN_ROOT_NAMESPACE_CACHE lookup to use the thread-safe .get() method to prevent concurrent KeyErrors.
  2. Added an exact-match fallback for namespace projection so that imports of the exact scan-root package itself (e.g. import Company.Apps.Jobs.Team) correctly resolve to the root's __init__.py.

The bot should report a completely clean bill of health now! Let me know if you have any feedback on the implementation!

safishamsi added a commit that referenced this pull request Sep 27, 2026
Security: close the Fortran cpp #include arbitrary-file-read
(GHSA-pcc4-rvhr-2pr8), the last Aider/Devin monolith --watch shell sink
(#3852), and terraform name/value secret redaction (#3870). Plus Windows
watch rebuild locking (#3883), C# tuple element-name refs (#3877), JSX
component-usage calls (#3855), and nested scan-root Python import
projection (#3867).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@safishamsi

Copy link
Copy Markdown
Collaborator

Shipped in v0.9.70 (now on PyPI) via an authorship-preserving cherry-pick, so your commit keeps contributor-graph credit. Thanks @nikhilsaxena04! Nested scan-root absolute imports resolve via namespace projection.

Release: https://github.com/Graphify-Labs/graphify/releases/tag/v0.9.70

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: absolute imports drop all calls edges when the scan root is a subdirectory of the import root

2 participants