Files
graphify/tests/test_python_import_never_resolves_to_itself.py
T
rohit-jsfreakyandClaude Opus 4.8 ab3c3553cc fix(python): never resolve an absolute import to the module that contains it (#4225)
A directory-qualified stem fallback could resolve an absolute import
(from werkzeug.wrappers import Request) onto a local wrappers.py, rewiring a
class to inherit from itself. Exclude the importing file's own stems; a
same-named module in another package still resolves by exact/suffix/bare stem.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-08 14:28:20 +01:00

101 lines
4.4 KiB
Python

"""An absolute import never resolves to the module that contains it.
`_resolve_cross_file_imports` resolves `from a.b.c import X` by exact path, then by a
unique path suffix, then by a bare-stem index of the last part (`c`). For a package
outside the corpus, the bare-stem step can land on the importing file itself:
`from werkzeug.wrappers import Request as RequestBase` inside flask's own
`wrappers.py` matched `wrappers.py`, so `class Request(RequestBase)` was rewired to
inherit from itself and every use of the alias became an edge to the local class.
"""
from __future__ import annotations
from pathlib import Path
from graphify.extract import extract
_WRAPPERS = (
"from werkzeug.wrappers import Request as RequestBase\n"
"\n\n"
"class Request(RequestBase):\n"
" pass\n"
"\n\n"
"def wrap(req: RequestBase) -> RequestBase:\n"
" return RequestBase(req)\n"
)
def _build(tmp_path: Path, files: dict[str, str]):
paths = []
for name, body in {"pkg/__init__.py": "", **files}.items():
p = tmp_path / name
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(body, encoding="utf-8")
paths.append(p)
r = extract(paths, root=tmp_path, cache_root=tmp_path / "graphify-out", parallel=False)
return {n["id"]: n for n in r["nodes"]}, r["edges"]
def test_external_base_aliased_in_a_same_named_module_is_not_a_self_loop(tmp_path):
nodes, edges = _build(tmp_path, {"pkg/wrappers.py": _WRAPPERS})
assert not [e for e in edges if e["source"] == e["target"]]
inherits = [e for e in edges if e["relation"] == "inherits"]
assert len(inherits) == 1
base = nodes[inherits[0]["target"]]
assert base["label"] == "RequestBase" and not base.get("source_file")
def test_uses_of_the_alias_do_not_point_at_the_local_class(tmp_path):
nodes, edges = _build(tmp_path, {"pkg/wrappers.py": _WRAPPERS})
local_request = next(nid for nid, n in nodes.items() if n["label"] == "Request" and n.get("source_file"))
wrong = [e for e in edges if e["target"] == local_request and e["relation"] in ("uses", "references")]
assert wrong == []
def test_a_real_in_repo_import_still_resolves(tmp_path):
nodes, edges = _build(tmp_path, {
"pkg/base.py": "class Request:\n pass\n",
"pkg/wrappers.py": "from pkg.base import Request as RequestBase\n\n\nclass Request(RequestBase):\n pass\n",
})
inherits = [e for e in edges if e["relation"] == "inherits"]
assert [nodes[e["target"]].get("source_file") for e in inherits] == ["pkg/base.py"]
def test_warm_rebuild_keeps_the_fix(tmp_path):
first = _build(tmp_path, {"pkg/wrappers.py": _WRAPPERS})[1]
warm = _build(tmp_path, {"pkg/wrappers.py": _WRAPPERS})[1]
key = lambda e: (e["source"], e["target"], e["relation"]) # noqa: E731
assert sorted(map(key, first)) == sorted(map(key, warm))
assert not [e for e in warm if e["source"] == e["target"]]
def test_a_same_named_module_in_another_package_still_resolves(tmp_path):
"""The guard compares directory-qualified stems: `a/utils.py` importing from
`b/utils.py` is a different module and must keep resolving, by exact path, by
suffix, and through the bare-stem fallback, whichever file is listed first."""
for i, importer in enumerate((
"from b.utils import Helper\n",
"from proj.b.utils import Helper\n",
"from installed.x.utils import Helper\n",
)):
for b_first in (False, True):
root = tmp_path / f"case{i}_{int(b_first)}"
files = {
"proj/__init__.py": "",
"proj/a/__init__.py": "",
"proj/b/__init__.py": "",
"proj/b/utils.py": "class Helper:\n pass\n",
"proj/a/utils.py": importer + "\n\nclass Local(Helper):\n pass\n",
}
paths = []
for name, body in files.items():
p = root / name
p.parent.mkdir(parents=True, exist_ok=True)
p.write_text(body, encoding="utf-8")
paths.append(p)
if b_first:
paths[-2], paths[-1] = paths[-1], paths[-2]
r = extract(paths, root=root, cache_root=root / "graphify-out", parallel=False)
nodes = {n["id"]: n for n in r["nodes"]}
bases = [nodes[e["target"]].get("source_file") for e in r["edges"] if e["relation"] == "inherits"]
assert bases == ["proj/b/utils.py"], (importer, b_first, bases)