mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-16 08:59:26 +00:00
fix(python): keep missing relative-import targets out of the checkout path
A relative import of a module with no file behind it (`from .absent
import x`) built its target from the attempted absolute path, so the id
became e.g. `c_users_<user>_..._pkg_absent_py`. The root-relative id
remap never rewrites it (there is no node to anchor on), so the checkout
location and the OS username ended up in graph.json and every clone got
a different id.
Use the dotted module name relative to the scan root instead
(`pkg_absent`), the id an unresolved absolute import of the same module
already gets, so `from .absent` and `from pkg.absent` now meet on one
node. An import that climbs above the scan root falls back to a `ref`
id. Imports that resolve, and attempted paths that exist, are unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B8gwEpjajiWHVW3c9smuKf
(cherry picked from commit 8cdbb0ea4f)
This commit is contained in:
1 parent
ac467b0c00
commit
0083ee78e5
2 files changed
+54
-1
No files matched your search
+29
-1
@@ -674,6 +674,23 @@ def _python_import_bindings(node, source: bytes) -> list[tuple[str, str]]:
|
||||
return bindings
|
||||
|
||||
|
||||
def _missing_relative_module_name(attempted: Path, root: Path, raw: str) -> str:
|
||||
"""Dotted module name, relative to ``root``, of an unresolved relative import.
|
||||
|
||||
``attempted`` is the file the import would have named (``pkg/missing.py`` or
|
||||
``pkg/__init__.py``). Falls back to ``ref:`` + the raw specifier when the
|
||||
import climbs above the scan root, so the id never carries an absolute path.
|
||||
"""
|
||||
try:
|
||||
rel = attempted.relative_to(root)
|
||||
except ValueError:
|
||||
return f"ref:{raw}"
|
||||
parts = list(rel.with_suffix("").parts)
|
||||
if parts and parts[-1] == "__init__":
|
||||
parts.pop()
|
||||
return ".".join(parts) if parts else f"ref:{raw}"
|
||||
|
||||
|
||||
def _import_python(
|
||||
node, source: bytes, file_nid: str, stem: str, edges: list, str_path: str,
|
||||
scope_stack: list[str] | None = None, scan_root: Path | None = None,
|
||||
@@ -749,13 +766,24 @@ def _import_python(
|
||||
target_path = _resolve_python_module_path(
|
||||
module_name, current_path, root, level=dots
|
||||
)
|
||||
missing = False
|
||||
if target_path is None:
|
||||
base = current_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
|
||||
tgt_nid = _make_id(str(target_path))
|
||||
missing = not target_path.is_file()
|
||||
if missing:
|
||||
# No file behind the import: minting the id from the
|
||||
# attempted absolute path bakes the checkout location (and
|
||||
# OS username) into the graph, and the root-relative id
|
||||
# remap never rewrites it. Use the dotted module name
|
||||
# relative to the scan root, the id an unresolved absolute
|
||||
# import of the same module already gets.
|
||||
tgt_nid = _make_id(_missing_relative_module_name(target_path, root, raw))
|
||||
else:
|
||||
tgt_nid = _make_id(str(target_path))
|
||||
else:
|
||||
# Use the shared scan-root-aware resolver for absolute imports.
|
||||
# It stops at the corpus boundary and handles package roots the
|
||||
|
||||
@@ -147,6 +147,31 @@ def test_overdeep_relative_import_is_unresolved_not_fatal(tmp_path: Path):
|
||||
assert _node_id(result, "ok()", "pkg/mod.py")
|
||||
|
||||
|
||||
def _missing_import_targets(checkout: Path) -> set[str]:
|
||||
_write(checkout / "pkg" / "__init__.py", "")
|
||||
source = _write(
|
||||
checkout / "pkg" / "app.py",
|
||||
"from .absent import helper\n"
|
||||
"from pkg.absent import other\n\n"
|
||||
"def run():\n"
|
||||
" return helper(), other()\n",
|
||||
)
|
||||
result = extract([source, checkout / "pkg" / "__init__.py"], cache_root=checkout)
|
||||
return {e["target"] for e in result["edges"] if e["relation"] == "imports_from"}
|
||||
|
||||
|
||||
def test_missing_relative_import_target_id_does_not_depend_on_the_checkout(tmp_path: Path):
|
||||
"""A relative import of a module with no file behind it minted its target id
|
||||
from the attempted absolute path (``..._pkg_absent_py``), which the
|
||||
root-relative remap never rewrites: the checkout location and OS username
|
||||
ended up in the graph. It now gets the dotted module name, the id an
|
||||
unresolved absolute import of the same module already gets."""
|
||||
first = _missing_import_targets(tmp_path / "clone_one")
|
||||
second = _missing_import_targets(tmp_path / "elsewhere" / "clone_two")
|
||||
|
||||
assert first == second == {"pkg_absent"}
|
||||
|
||||
|
||||
def test_ordinary_relative_import_still_resolves(tmp_path: Path):
|
||||
target = _write(tmp_path / "pkg" / "sibling.py", "def helper():\n return 1\n")
|
||||
source = _write(tmp_path / "pkg" / "mod.py", "from .sibling import helper\n")
|
||||
|
||||
Reference in new issue
Block a user