Skip to content

Commit 674a89f

Browse files
1 parent 0b83834 commit 674a89f

2 files changed

Lines changed: 58 additions & 2 deletions

File tree

src/codegraphcontext/tools/languages/python.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ def parse(self, path: Path, is_dependency: bool = False, is_notebook: bool = Fal
145145
classes = self._find_classes(root_node)
146146
imports = self._find_imports(root_node)
147147
function_calls = self._find_calls(root_node)
148+
self._attach_module_context(functions, function_calls, root_node, index_source)
148149
variables = self._find_variables(root_node)
149150

150151
return {
@@ -165,6 +166,41 @@ def parse(self, path: Path, is_dependency: bool = False, is_notebook: bool = Fal
165166
os.remove(temp_py_file)
166167
info_logger(f"Removed temporary file: {temp_py_file}")
167168

169+
def _attach_module_context(self, functions, function_calls, root_node, index_source: bool = False):
170+
"""Represent module-level executable code as Python's <module> frame."""
171+
module_level_calls = [
172+
call for call in function_calls
173+
if not call.get("context") or call["context"][0] is None
174+
]
175+
if not module_level_calls:
176+
return
177+
178+
has_module_frame = any(
179+
func.get("name") == "<module>" and func.get("line_number") == 1
180+
for func in functions
181+
)
182+
if not has_module_frame:
183+
module_func = {
184+
"name": "<module>",
185+
"line_number": 1,
186+
"end_line": root_node.end_point[0] + 1,
187+
"args": [],
188+
"cyclomatic_complexity": 1,
189+
"context": None,
190+
"context_type": "module",
191+
"class_context": None,
192+
"decorators": [],
193+
"lang": self.language_name,
194+
"is_dependency": False,
195+
}
196+
if index_source:
197+
module_func["source"] = self._get_node_text(root_node)
198+
module_func["docstring"] = self._get_docstring(root_node)
199+
functions.append(module_func)
200+
201+
for call in module_level_calls:
202+
call["context"] = ("<module>", "module", 1)
203+
168204
def _find_lambda_assignments(self, root_node, index_source: bool = False):
169205
functions = []
170206
query_str = PY_QUERIES.get('lambda_assignments')
@@ -573,4 +609,4 @@ def pre_scan_python(files: list[Path], parser_wrapper) -> dict:
573609
finally:
574610
if temp_py_file and temp_py_file.exists():
575611
os.remove(temp_py_file)
576-
return imports_map
612+
return imports_map

tests/unit/parsers/test_python_parser.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,27 @@ def test_parse_simple_function(self, parser, temp_test_dir):
4646
assert len(funcs) == 1
4747
assert funcs[0]["name"] == "hello"
4848

49+
def test_module_level_call_uses_module_context(self, parser, temp_test_dir):
50+
"""Top-level executable calls should be linked from a synthetic module frame."""
51+
code = "from pkg.utils import helper\n\nresult = helper()\n"
52+
f = temp_test_dir / "__main__.py"
53+
f.write_text(code)
54+
55+
result = parser.parse(str(f))
56+
57+
module_func = next(
58+
func for func in result["functions"]
59+
if func["name"] == "<module>"
60+
)
61+
helper_call = next(
62+
call for call in result["function_calls"]
63+
if call["name"] == "helper"
64+
)
65+
66+
assert module_func["line_number"] == 1
67+
assert module_func["context_type"] == "module"
68+
assert helper_call["context"] == ("<module>", "module", 1)
69+
4970
def test_parse_class_with_method(self, parser, temp_test_dir):
5071
"""Parse a class with a method."""
5172
code = """
@@ -67,4 +88,3 @@ def greet(self, name):
6788
# Depending on implementation, methods might be in 'functions' with parent info
6889
# or inside 'classes'.
6990
# Let's assume they are captured.
70-

0 commit comments

Comments
 (0)