mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-13 15:40:05 +00:00
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>
101 lines
4.4 KiB
Python
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)
|