Skip to content

Commit 3d6e677

Browse files
committed
fix: add async discovery
1 parent 932e07a commit 3d6e677

File tree

2 files changed

+172
-2
lines changed

2 files changed

+172
-2
lines changed

src/code_index_mcp/indexing/strategies/python_strategy.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,10 @@ def visit_ClassDef(self, node: ast.ClassDef):
100100
old_class = self.current_class
101101
self.current_class = class_name
102102

103-
method_nodes: List[ast.FunctionDef] = []
103+
method_nodes = []
104104
# First pass: register methods so forward references resolve
105105
for child in node.body:
106-
if isinstance(child, ast.FunctionDef):
106+
if isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
107107
self._register_method(child, class_name)
108108
method_nodes.append(child)
109109
else:
@@ -118,6 +118,14 @@ def visit_ClassDef(self, node: ast.ClassDef):
118118

119119
def visit_FunctionDef(self, node: ast.FunctionDef):
120120
"""Visit function definition - extract symbol and track context."""
121+
self._process_function(node)
122+
123+
def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef):
124+
"""Visit async function definition - extract symbol and track context."""
125+
self._process_function(node)
126+
127+
def _process_function(self, node):
128+
"""Process both sync and async function definitions."""
121129
# Skip if this is a method (already handled by ClassDef)
122130
if self.current_class:
123131
return
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Test for Python symbol discovery including all symbol types.
4+
"""
5+
import pytest
6+
from textwrap import dedent
7+
8+
from code_index_mcp.indexing.strategies.python_strategy import PythonParsingStrategy
9+
10+
11+
@pytest.fixture
12+
def test_code_with_all_symbols():
13+
"""Fixture with variables, constants, functions, classes and methods."""
14+
return '''
15+
CONSTANT = 42
16+
variable = 'hello'
17+
18+
def sync_function():
19+
"""A regular synchronous function."""
20+
return "sync result"
21+
22+
async def async_function():
23+
"""An asynchronous function."""
24+
return "async result"
25+
26+
def top_level_function(x, y):
27+
"""Function without type hints."""
28+
return x + y
29+
30+
def function_with_types(name: str, age: int, active: bool = True) -> dict:
31+
"""
32+
Function with type hints and default values.
33+
34+
Args:
35+
name: The person's name
36+
age: The person's age
37+
active: Whether the person is active
38+
39+
Returns:
40+
A dictionary with person info
41+
"""
42+
return {"name": name, "age": age, "active": active}
43+
44+
def complex_function(items: list[str], *args: int, callback=None, **kwargs: str) -> tuple[int, str]:
45+
"""Function with complex signature including *args and **kwargs."""
46+
return len(items), str(args)
47+
48+
class TestClass:
49+
"""A test class with various methods."""
50+
CLASS_VAR = 123
51+
52+
def __init__(self, value: int):
53+
"""Initialize with a value."""
54+
self.value = value
55+
56+
def sync_method(self):
57+
"""A regular synchronous method."""
58+
return "sync method result"
59+
60+
async def async_method(self):
61+
"""An asynchronous method."""
62+
return "async method result"
63+
64+
def method(self):
65+
return self.value
66+
67+
def typed_method(self, x: float, y: float) -> float:
68+
"""Method with type hints.
69+
70+
Returns the sum of x and y.
71+
"""
72+
return x + y
73+
'''
74+
75+
76+
def test_python_symbol_discovery(test_code_with_all_symbols):
77+
"""Test that all Python symbol types are correctly discovered."""
78+
strategy = PythonParsingStrategy()
79+
symbols, file_info = strategy.parse_file("test.py", test_code_with_all_symbols)
80+
81+
# Create a lookup dict by symbol name for easier access
82+
# This will throw KeyError if a symbol is missing
83+
symbol_lookup = {}
84+
for symbol_id, symbol_info in symbols.items():
85+
# Extract the symbol name from the ID (format: "file.py::SymbolName")
86+
if '::' in symbol_id:
87+
name = symbol_id.split('::')[1]
88+
symbol_lookup[name] = symbol_info
89+
90+
# Verify all expected functions are in file_info
91+
discovered_functions = file_info.symbols.get('functions', [])
92+
expected_functions = ['sync_function', 'async_function', 'top_level_function',
93+
'function_with_types', 'complex_function']
94+
for func in expected_functions:
95+
assert func in discovered_functions, f"Function '{func}' not in file_info.symbols['functions']"
96+
97+
# Verify all expected methods are discovered
98+
expected_methods = ['TestClass.__init__', 'TestClass.sync_method',
99+
'TestClass.async_method', 'TestClass.method', 'TestClass.typed_method']
100+
for method in expected_methods:
101+
assert method in symbol_lookup, f"Method '{method}' not found in symbols"
102+
103+
# Verify class is discovered
104+
assert 'TestClass' in file_info.symbols.get('classes', [])
105+
assert 'TestClass' in symbol_lookup
106+
107+
# Check symbol types
108+
assert symbol_lookup['sync_function'].type == 'function'
109+
assert symbol_lookup['async_function'].type == 'function'
110+
assert symbol_lookup['top_level_function'].type == 'function'
111+
assert symbol_lookup['function_with_types'].type == 'function'
112+
assert symbol_lookup['complex_function'].type == 'function'
113+
assert symbol_lookup['TestClass'].type == 'class'
114+
assert symbol_lookup['TestClass.__init__'].type == 'method'
115+
assert symbol_lookup['TestClass.sync_method'].type == 'method'
116+
assert symbol_lookup['TestClass.async_method'].type == 'method'
117+
assert symbol_lookup['TestClass.method'].type == 'method'
118+
assert symbol_lookup['TestClass.typed_method'].type == 'method'
119+
120+
# Check docstrings explicitly
121+
assert symbol_lookup['sync_function'].docstring == "A regular synchronous function."
122+
assert symbol_lookup['async_function'].docstring == "An asynchronous function."
123+
assert symbol_lookup['top_level_function'].docstring == "Function without type hints."
124+
125+
expected_docstring = dedent("""
126+
Function with type hints and default values.
127+
128+
Args:
129+
name: The person's name
130+
age: The person's age
131+
active: Whether the person is active
132+
133+
Returns:
134+
A dictionary with person info
135+
""").strip()
136+
assert symbol_lookup['function_with_types'].docstring == expected_docstring
137+
138+
assert symbol_lookup['complex_function'].docstring == "Function with complex signature including *args and **kwargs."
139+
assert symbol_lookup['TestClass.__init__'].docstring == "Initialize with a value."
140+
assert symbol_lookup['TestClass.sync_method'].docstring == "A regular synchronous method."
141+
assert symbol_lookup['TestClass.async_method'].docstring == "An asynchronous method."
142+
assert symbol_lookup['TestClass.method'].docstring is None
143+
144+
expected_typed_method_docstring = dedent("""
145+
Method with type hints.
146+
147+
Returns the sum of x and y.
148+
""").strip()
149+
assert symbol_lookup['TestClass.typed_method'].docstring == expected_typed_method_docstring
150+
assert symbol_lookup['TestClass'].docstring == "A test class with various methods."
151+
152+
# Check signatures explicitly
153+
assert symbol_lookup['sync_function'].signature == "def sync_function():"
154+
assert symbol_lookup['async_function'].signature == "def async_function():"
155+
assert symbol_lookup['top_level_function'].signature == "def top_level_function(x, y):"
156+
assert symbol_lookup['function_with_types'].signature == "def function_with_types(name, age, active):"
157+
assert symbol_lookup['complex_function'].signature == "def complex_function(items, *args, **kwargs):"
158+
assert symbol_lookup['TestClass.__init__'].signature == "def __init__(self, value):"
159+
assert symbol_lookup['TestClass.sync_method'].signature == "def sync_method(self):"
160+
assert symbol_lookup['TestClass.async_method'].signature == "def async_method(self):"
161+
assert symbol_lookup['TestClass.method'].signature == "def method(self):"
162+
assert symbol_lookup['TestClass.typed_method'].signature == "def typed_method(self, x, y):"

0 commit comments

Comments
 (0)