Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions graphify/extract.py
Original file line number Diff line number Diff line change
Expand Up @@ -4245,12 +4245,17 @@ def _module_stem_key(nid: str) -> str:
method_class[tgt] = src

def _file_node(nid: str) -> "str | None":
# Climb `contains` / `method` parents to the top, which is the file: a
# nested function's `contains` parent is its enclosing function, not the
# file, so stopping at the first parent compared a function to a file.
seen: set[str] = set()
parent: "str | None" = None
while nid and nid not in seen:
seen.add(nid)
if nid in file_of_node:
return file_of_node[nid]
nid = method_class.get(nid, "")
up = file_of_node.get(nid) or method_class.get(nid)
if not up:
return parent
parent = nid = up
return None

def _emit_call(
Expand Down
21 changes: 21 additions & 0 deletions tests/test_python_typed_receiver_calls.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,27 @@ def test_class_not_visible_from_caller_emits_no_edge(tmp_path):
assert not _has(calls, "run")


def test_nested_functions_and_nested_class_methods_reach_the_file(tmp_path):
# A nested def's `contains` parent is its enclosing function, and a nested
# class's is its outer class: the visibility check must climb to the file.
calls, _, _ = _calls(tmp_path, {
"client.py": _CLIENT,
"use.py": (
"from .client import Client\n\n"
"def outer():\n def inner(c: Client):\n return c.send()\n return inner\n\n"
"class K:\n def run(self):\n def callback(c: Client):\n return c.send()\n"
" return callback\n\n"
"def a():\n def b():\n def deep():\n c = Client()\n return c.send()\n"
" return deep\n return b\n\n"
"class Outer:\n class Inner:\n def go(self, c: Client):\n return c.send()\n\n"
"def hidden():\n def nope(c: 'Unknown'):\n return c.send()\n return nope\n"
),
})
for caller in ("inner", "callback", "deep", ".go"): # methods are labeled .name()
assert _has(calls, caller), caller
assert not _has(calls, "nope")


def test_duplicate_class_name_emits_no_edge(tmp_path):
calls, _, _ = _calls(tmp_path, {
"a/__init__.py": "",
Expand Down
Loading