mirror of
https://github.com/safishamsi/graphify.git
synced 2026-10-13 15:40:05 +00:00
Co-authored-by: Codex <noreply@openai.com>
(cherry picked from commit e85a748445)
84 lines
3.6 KiB
Python
84 lines
3.6 KiB
Python
"""Nested Python class definitions must keep their lexical owner (#4220)."""
|
|
|
|
from graphify.extract import extract
|
|
|
|
|
|
SOURCE = (
|
|
"class Forward:\n"
|
|
" class Params:\n"
|
|
" def value(self): return 1\n"
|
|
" class Wrapper:\n"
|
|
" class Params:\n"
|
|
" def value(self): return 2\n"
|
|
"class Backward:\n"
|
|
" class Params:\n"
|
|
" def value(self): return 3\n"
|
|
)
|
|
|
|
|
|
def _extract(root, source=SOURCE):
|
|
root.mkdir(parents=True, exist_ok=True)
|
|
path = root / "scheduler.py"
|
|
path.write_text(source, encoding="utf-8")
|
|
return extract([path], root=root, cache_root=root / "cache", parallel=False)
|
|
|
|
|
|
def test_same_named_nested_classes_keep_nodes_methods_and_parent(tmp_path):
|
|
result = _extract(tmp_path)
|
|
params = [node for node in result["nodes"] if node["label"] == "Params"]
|
|
assert len(params) == 3
|
|
assert len({node["id"] for node in params}) == 3
|
|
assert {node["source_location"] for node in params} == {"L2", "L5", "L8"}
|
|
owners = {node["id"]: node["label"] for node in result["nodes"]}
|
|
expected = {"L2": "Forward", "L5": "Wrapper", "L8": "Backward"}
|
|
for node in params:
|
|
parents = [edge["source"] for edge in result["edges"]
|
|
if edge["relation"] == "contains" and edge["target"] == node["id"]]
|
|
assert len(parents) == 1
|
|
assert owners[parents[0]] == expected[node["source_location"]]
|
|
methods = [edge["target"] for edge in result["edges"]
|
|
if edge["relation"] == "method" and edge["source"] == node["id"]]
|
|
assert len(methods) == 1
|
|
assert owners[methods[0]] == ".value()"
|
|
assert len([node for node in result["nodes"] if node["label"] == ".value()"]) == 3
|
|
|
|
|
|
def test_same_name_as_parent_has_distinct_identity(tmp_path):
|
|
result = _extract(tmp_path, "class Params:\n class Params:\n pass\n")
|
|
params = [node for node in result["nodes"] if node["label"] == "Params"]
|
|
assert len(params) == 2
|
|
outer, inner = sorted(params, key=lambda node: node["source_location"])
|
|
assert outer["id"] == "scheduler_params"
|
|
assert outer["id"] != inner["id"]
|
|
assert any(edge["source"] == outer["id"] and edge["target"] == inner["id"]
|
|
and edge["relation"] == "contains" for edge in result["edges"])
|
|
|
|
|
|
def test_nested_class_identity_is_stable_across_cache_and_checkout(tmp_path):
|
|
first = _extract(tmp_path / "first")
|
|
warm = _extract(tmp_path / "first")
|
|
relocated = _extract(tmp_path / "second")
|
|
assert first["nodes"] == warm["nodes"] == relocated["nodes"]
|
|
assert first["edges"] == warm["edges"] == relocated["edges"]
|
|
|
|
|
|
def test_nested_class_decorators_and_docstrings_have_real_owners(tmp_path):
|
|
result = _extract(tmp_path, (
|
|
"def decorate(cls): return cls\n"
|
|
"class Outer:\n"
|
|
" @decorate\n"
|
|
" class Inner:\n"
|
|
" \"\"\"This class holds the nested parameters for the outer class.\"\"\"\n"
|
|
" def value(self):\n"
|
|
" \"\"\"Return the value owned by this nested class instance.\"\"\"\n"
|
|
" return 1\n"
|
|
))
|
|
nodes = {node["id"]: node for node in result["nodes"]}
|
|
inner = next(node["id"] for node in nodes.values() if node["label"] == "Inner")
|
|
method = next(node["id"] for node in nodes.values() if node["label"] == ".value()")
|
|
assert any(edge["source"] == inner and edge.get("context") == "decorator"
|
|
for edge in result["edges"])
|
|
rationales = [edge for edge in result["edges"] if edge["relation"] == "rationale_for"]
|
|
assert {edge["target"] for edge in rationales} == {inner, method}
|
|
assert all(edge["source"] in nodes and edge["target"] in nodes for edge in rationales)
|