Skip to content

fix(python): resolve submodule imports inside PEP 420 namespace packages - #3429

Open
flaukowski wants to merge 1 commit into
Graphify-Labs:v8from
flaukowski:namespace-package-submodule-imports
Open

fix(python): resolve submodule imports inside PEP 420 namespace packages#3429
flaukowski wants to merge 1 commit into
Graphify-Labs:v8from
flaukowski:namespace-package-submodule-imports

Conversation

@flaukowski

Copy link
Copy Markdown

What

from . import brain, ledger inside a directory with no __init__.py — a PEP 420 namespace package, which python -m pkg.mod runs without complaint — produces no edges at all. _resolve_python_module_path returns None when there is no module file to probe, and the #1146 submodule-import branch in _collect_python_symbol_resolution_facts sits behind that None, so the whole statement is skipped. Every brain.think() / ledger.write() call in such a repo is therefore invisible to the #1883 module-call arm, and the most-called functions carry in-degree 0.

With an __init__.py present the same lines resolve correctly (checked the relative, absolute, aliased, parent-relative and subpackage spellings), so this is specific to namespace packages.

Fix

  • _resolve_python_namespace_dir mirrors the module-path walk (relative base → scan root → sys.path-root ancestors, per Scan root silently changes the graph: src-layout project loses most 'imports' edges, same node count, no warning #2072) and returns only a directory that exists inside the scan root and has no __init__.py.
  • _collect_python_symbol_resolution_facts uses it when the module path resolves to no file; the existing submodule probe then emits the same imports_from (submodule_import) edges a regular package gets, and the module arm resolves the calls.
  • A name that is not a module file on disk still emits nothing, and a namespace package binds no symbols of its own (no _SymbolImportFact fallback), so nothing is fabricated.

Evidence

Same graphify version (0.9.56), with and without the change, on a 275-node Python repo laid out this way:

calls edges think() callers remember() ledger write()
unpatched 201 0 0 0
patched 307 15 10 20

Node count unchanged (275 → 275).

Tests

Three new tests in tests/test_extract.py, next to the #2082 tests: relative form (from . import a, b → two EXTRACTED calls edges plus the imports_from edges), absolute + parent-relative forms (from pkg import a; from .. import a from a nested namespace subpackage), and a negative (from . import data, missing where data/ holds no module file → no resolved edge, no raise). The two positive tests fail on v8 without the fix. pytest tests/test_extract.py -k python: 21 → 24 passed; the rest of the suite is unchanged.

No CHANGELOG entry, following the pattern of other fix PRs — happy to add one if you prefer contributors to.

🤖 Generated with Claude Code

https://claude.ai/code/session_01KTNEBgonu6ASph6w8R2Wcp

`from . import brain, ledger` in a directory with no __init__.py — a
namespace package, which `python -m pkg.mod` runs without complaint —
emitted nothing: _resolve_python_module_path returns None when there is
no module file to probe, and the Graphify-Labs#1146 submodule branch sits behind that
None. So every `brain.think()` / `ledger.write()` call in such a repo was
invisible to the Graphify-Labs#1883 module arm, and the most-called functions carried
in-degree 0 in the graph.

_resolve_python_namespace_dir mirrors the module-path walk (relative
base, scan root, then sys.path-root ancestors per Graphify-Labs#2072) and returns only
a directory that exists inside the root and has no __init__.py. The fact
collector uses it when the module path resolves to no file; the existing
submodule probe then emits the same imports_from edges a regular package
gets. A name that is not a module file on disk still emits nothing, and
a namespace package binds no symbols of its own.

Measured on a 275-node Python repo laid out this way, same graphify
version with and without the change: calls 201 -> 307, nodes unchanged;
think() 0 -> 15 callers, remember() 0 -> 10, ledger write() 0 -> 20.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KTNEBgonu6ASph6w8R2Wcp

@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

No blocking issues surfaced.

Analysis details — impact, health, verification

Impact & health

Graphify review

Impact — 2067 functions depend on the 542 functions this change touches.

Health — this change adds coupling hotspots:

  • new: extract() — 562 callers, 43 callees
  • new: _rebuild_code() — 115 callers, 51 callees
  • new: _extract_generic() — 18 callers, 26 callees
  • new: extract_js() — 85 callers, 4 callees
  • new: extract_xaml() — 19 callers, 17 callees
  • new: _resolve_js_module_path() — 34 callers, 9 callees
  • new: dispatch_command() — 2 callers, 124 callees
  • new: extract_objc() — 27 callers, 9 callees
  • …and 35 more — each is listed as a finding

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

Test selection

Test selection

29 of 259 test file(s) selected (11%) via static blast radius.

  • tests/test_astro_extraction.py — impact
  • tests/test_build.py — impact
  • tests/test_cjs_module_extension.py — impact
  • tests/test_cpp_nested_and_cli.py — impact
  • tests/test_dotnet.py — impact
  • tests/test_extract.py — impact, changed-test
  • tests/test_forwarding_review_findings.py — impact
  • tests/test_import_extension_resolution.py — impact
  • tests/test_indirect_dispatch.py — impact
  • tests/test_indirect_dispatch_assign_return.py — impact
  • tests/test_indirect_dispatch_getattr.py — impact
  • tests/test_js_exported_scalar_bindings.py — impact
  • tests/test_languages.py — impact
  • tests/test_multilang.py — impact
  • tests/test_package_json_subpath_imports.py — impact
  • tests/test_pascal.py — impact
  • tests/test_pascal_resolution.py — impact
  • tests/test_phantom_external_import.py — impact
  • tests/test_python_import_resolution.py — impact
  • tests/test_python_underscore_resolution.py — impact
  • tests/test_rationale.py — impact
  • tests/test_ruby_resolution.py — impact
  • tests/test_scala_self_type.py — impact
  • tests/test_src_layout_import_resolution.py — impact
  • tests/test_swift_computed_properties.py — impact
  • tests/test_ts_new_expression_calls.py — impact
  • tests/test_typescript_module_extensions.py — impact
  • tests/test_unmapped_at_alias_resolution.py — impact
  • tests/test_vue_extraction.py — impact

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.

Formal verification

Could not verify: Could not verify \_collect\_python\_symbol\_resolution\_facts.

The verifier did not have enough to check \_collect\_python\_symbol\_resolution\_facts, 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 `root` is annotated `Path` — outside the synthesizable primitive/collection set

· 43 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.

1 participant