fix(python): resolve submodule imports inside PEP 420 namespace packages - #3429
fix(python): resolve submodule imports inside PEP 420 namespace packages#3429flaukowski wants to merge 1 commit into
Conversation
`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
There was a problem hiding this comment.
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— impacttests/test_build.py— impacttests/test_cjs_module_extension.py— impacttests/test_cpp_nested_and_cli.py— impacttests/test_dotnet.py— impacttests/test_extract.py— impact, changed-testtests/test_forwarding_review_findings.py— impacttests/test_import_extension_resolution.py— impacttests/test_indirect_dispatch.py— impacttests/test_indirect_dispatch_assign_return.py— impacttests/test_indirect_dispatch_getattr.py— impacttests/test_js_exported_scalar_bindings.py— impacttests/test_languages.py— impacttests/test_multilang.py— impacttests/test_package_json_subpath_imports.py— impacttests/test_pascal.py— impacttests/test_pascal_resolution.py— impacttests/test_phantom_external_import.py— impacttests/test_python_import_resolution.py— impacttests/test_python_underscore_resolution.py— impacttests/test_rationale.py— impacttests/test_ruby_resolution.py— impacttests/test_scala_self_type.py— impacttests/test_src_layout_import_resolution.py— impacttests/test_swift_computed_properties.py— impacttests/test_ts_new_expression_calls.py— impacttests/test_typescript_module_extensions.py— impacttests/test_unmapped_at_alias_resolution.py— impacttests/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).
What
from . import brain, ledgerinside a directory with no__init__.py— a PEP 420 namespace package, whichpython -m pkg.modruns without complaint — produces no edges at all._resolve_python_module_pathreturnsNonewhen there is no module file to probe, and the #1146 submodule-import branch in_collect_python_symbol_resolution_factssits behind thatNone, so the whole statement is skipped. Everybrain.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__.pypresent 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_dirmirrors 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_factsuses it when the module path resolves to no file; the existing submodule probe then emits the sameimports_from(submodule_import) edges a regular package gets, and the module arm resolves the calls._SymbolImportFactfallback), 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:
callsedgesthink()callersremember()write()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 EXTRACTEDcallsedges plus theimports_fromedges), absolute + parent-relative forms (from pkg import a;from .. import afrom a nested namespace subpackage), and a negative (from . import data, missingwheredata/holds no module file → no resolved edge, no raise). The two positive tests fail onv8without 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