fix(python): resolve relative subpackage imports to their __init__ (#2688)

This commit is contained in:
Ousama Ben Younes
2026-08-13 13:30:24 +01:00
committed by safishamsi
parent a05b4084d9
commit 51cf1481b6
2 changed files with 51 additions and 2 deletions
+15 -2
View File
@@ -124,6 +124,7 @@ from graphify.extractors.resolution import ( # noqa: E402,F401
_resolve_js_import_target,
_resolve_js_module_path,
_resolve_lua_import_target,
_probe_python_module_candidate,
_resolve_python_module_path,
_resolve_tsconfig_alias,
_resolve_workspace_import,
@@ -350,8 +351,20 @@ def _import_python(node, source: bytes, file_nid: str, stem: str, edges: list, s
base = Path(str_path).parent
for _ in range(dots - 1):
base = base.parent
rel = (module_name.replace(".", "/") + ".py") if module_name else "__init__.py"
target_path = base / rel
# A relative import can name a subpackage (a directory with an
# __init__.py), not a module file. Probing the candidate on disk
# (mirroring the companion `imports` edge's
# _resolve_python_module_path) resolves `graphs` -> graphs/__init__.py
# instead of a nonexistent graphs.py: without it the target keeps an
# absolute-path-derived slug that the target_file stamp below can't
# heal, so it dangles per-checkout (#2455).
candidate = base / module_name.replace(".", "/") if module_name else base
resolved = _probe_python_module_candidate(candidate)
if resolved is not None:
target_path = resolved
else:
rel = (module_name.replace(".", "/") + ".py") if module_name else "__init__.py"
target_path = base / rel
tgt_nid = _make_id(str(target_path))
else:
tgt_nid = _make_id(raw)
+36
View File
@@ -60,6 +60,42 @@ def test_ordinary_relative_import_still_resolves(tmp_path: Path):
assert _has_edge(result, source_file, target_symbol, "imports")
def test_relative_subpackage_import_from_targets_package_init(tmp_path: Path):
# `from ...graphs import build_graph` where `graphs/` is a package (a dir
# with __init__.py, not a graphs.py module). The imports_from edge must
# target the package's __init__.py file node, matching the companion
# `imports` edge — not an absolute-scan-path slug for a nonexistent
# graphs.py that dangles per-checkout (#2455).
init = _write(tmp_path / "src/mypkg/__init__.py", "")
api_init = _write(tmp_path / "src/mypkg/api/__init__.py", "")
routes_init = _write(tmp_path / "src/mypkg/api/routes/__init__.py", "")
graphs_init = _write(
tmp_path / "src/mypkg/graphs/__init__.py",
"def build_graph():\n return {}\n",
)
health = _write(
tmp_path / "src/mypkg/api/routes/health.py",
"from ...graphs import build_graph\n",
)
result = extract(
[init, api_init, routes_init, graphs_init, health], cache_root=tmp_path
)
health_file = _node_id(result, "health.py", "src/mypkg/api/routes/health.py")
graphs_pkg = _node_id(result, "__init__.py", "src/mypkg/graphs/__init__.py")
assert _has_edge(result, health_file, graphs_pkg, "imports_from")
# No imports_from edge out of health may carry an unresolved absolute-path
# slug (the pre-fix `<scan>_src_mypkg_graphs_py` target).
health_targets = [
e["target"]
for e in result["edges"]
if e["source"] == health_file and e["relation"] == "imports_from"
]
assert all(t.endswith("graphs_init") for t in health_targets), health_targets
def test_python_package_reexport_resolves_import_and_call_to_origin_symbol(tmp_path: Path):
origin = _write(tmp_path / "pkg/foo.py", "def Foo():\n return 1\n")
barrel = _write(tmp_path / "pkg/__init__.py", "from .foo import Foo as PublicFoo\n")