Coverage for src / mcp_server_langgraph / builder / importer / ast_parser.py: 67%

92 statements  

« prev     ^ index     » next       coverage.py v7.12.0, created at 2025-12-03 00:43 +0000

1""" 

2AST Parser for Python Code Import 

3 

4Parses Python source code into Abstract Syntax Tree for analysis. 

5 

6Key capabilities: 

7- Parse Python code safely 

8- Extract function definitions 

9- Find class definitions 

10- Identify LangGraph API calls 

11- Extract configuration values 

12 

13Example: 

14 from mcp_server_langgraph.builder.importer import PythonCodeParser 

15 

16 parser = PythonCodeParser() 

17 ast_tree = parser.parse_file("agent.py") 

18 

19 # Extract all function calls 

20 calls = parser.find_function_calls(ast_tree, "add_node") 

21""" 

22 

23import ast 

24from typing import Any 

25 

26 

27class PythonCodeParser: 

28 """ 

29 Parser for Python source code using AST. 

30 

31 Safely analyzes Python code to extract structure without execution. 

32 """ 

33 

34 def __init__(self) -> None: 

35 """Initialize parser.""" 

36 self.ast_tree: ast.Module | None = None 

37 

38 def parse_code(self, code: str) -> ast.Module: 

39 """ 

40 Parse Python code string into AST. 

41 

42 Args: 

43 code: Python source code 

44 

45 Returns: 

46 AST Module 

47 

48 Raises: 

49 SyntaxError: If code is invalid Python 

50 

51 Example: 

52 >>> parser = PythonCodeParser() 

53 >>> tree = parser.parse_code("def foo(): pass") 

54 >>> isinstance(tree, ast.Module) 

55 True 

56 """ 

57 self.ast_tree = ast.parse(code) 

58 return self.ast_tree 

59 

60 def parse_file(self, file_path: str) -> ast.Module: 

61 """ 

62 Parse Python file into AST. 

63 

64 Args: 

65 file_path: Path to Python file 

66 

67 Returns: 

68 AST Module 

69 

70 Example: 

71 >>> parser = PythonCodeParser() 

72 >>> tree = parser.parse_file("agent.py") 

73 """ 

74 with open(file_path) as f: 

75 code = f.read() 

76 

77 return self.parse_code(code) 

78 

79 def find_function_calls(self, tree: ast.Module | None = None, function_name: str | None = None) -> list[dict[str, Any]]: 

80 """ 

81 Find all function calls in AST. 

82 

83 Args: 

84 tree: AST tree (uses self.ast_tree if None) 

85 function_name: Optional filter for specific function name 

86 

87 Returns: 

88 List of function call information 

89 

90 Example: 

91 >>> calls = parser.find_function_calls(tree, "add_node") 

92 >>> len(calls) 

93 3 

94 """ 

95 tree = tree or self.ast_tree 

96 if not tree: 96 ↛ 97line 96 didn't jump to line 97 because the condition on line 96 was never true

97 return [] 

98 

99 calls = [] 

100 

101 for node in ast.walk(tree): 

102 if isinstance(node, ast.Call): 

103 # Get function name 

104 if isinstance(node.func, ast.Name): 

105 func_name = node.func.id 

106 elif isinstance(node.func, ast.Attribute): 106 ↛ 109line 106 didn't jump to line 109 because the condition on line 106 was always true

107 func_name = node.func.attr 

108 else: 

109 continue 

110 

111 # Filter if function_name specified 

112 if function_name and func_name != function_name: 

113 continue 

114 

115 # Extract arguments 

116 args = [] 

117 for arg in node.args: 

118 args.append(self._extract_value(arg)) 

119 

120 # Extract keyword arguments 

121 kwargs = {} 

122 for keyword in node.keywords: 122 ↛ 123line 122 didn't jump to line 123 because the loop on line 122 never started

123 if keyword.arg: 

124 kwargs[keyword.arg] = self._extract_value(keyword.value) 

125 

126 calls.append({"function": func_name, "args": args, "kwargs": kwargs, "lineno": node.lineno}) 

127 

128 return calls 

129 

130 def find_class_definitions(self, tree: ast.Module | None = None) -> list[dict[str, Any]]: 

131 """ 

132 Find all class definitions. 

133 

134 Args: 

135 tree: AST tree 

136 

137 Returns: 

138 List of class information 

139 

140 Example: 

141 >>> classes = parser.find_class_definitions(tree) 

142 >>> [c["name"] for c in classes] 

143 ['AgentState', 'MyClass'] 

144 """ 

145 tree = tree or self.ast_tree 

146 if not tree: 146 ↛ 147line 146 didn't jump to line 147 because the condition on line 146 was never true

147 return [] 

148 

149 classes = [] 

150 

151 for node in ast.walk(tree): 

152 if isinstance(node, ast.ClassDef): 

153 # Extract base classes 

154 bases = [] 

155 for base in node.bases: 

156 if isinstance(base, ast.Name): 156 ↛ 155line 156 didn't jump to line 155 because the condition on line 156 was always true

157 bases.append(base.id) 

158 

159 # Extract class docstring 

160 docstring = ast.get_docstring(node) 

161 

162 classes.append({"name": node.name, "bases": bases, "docstring": docstring, "lineno": node.lineno}) 

163 

164 return classes 

165 

166 def find_variable_assignments( 

167 self, tree: ast.Module | None = None, variable_name: str | None = None 

168 ) -> list[dict[str, Any]]: 

169 """ 

170 Find variable assignments. 

171 

172 Args: 

173 tree: AST tree 

174 variable_name: Optional filter for specific variable 

175 

176 Returns: 

177 List of assignments 

178 

179 Example: 

180 >>> assignments = parser.find_variable_assignments(tree, "graph") 

181 >>> assignments[0]["value"] 

182 'StateGraph(...)' 

183 """ 

184 tree = tree or self.ast_tree 

185 if not tree: 185 ↛ 186line 185 didn't jump to line 186 because the condition on line 185 was never true

186 return [] 

187 

188 assignments = [] 

189 

190 for node in ast.walk(tree): 

191 if isinstance(node, ast.Assign): 

192 for target in node.targets: 

193 if isinstance(target, ast.Name): 193 ↛ 192line 193 didn't jump to line 192 because the condition on line 193 was always true

194 var_name = target.id 

195 

196 # Filter if variable_name specified 

197 if variable_name and var_name != variable_name: 197 ↛ 198line 197 didn't jump to line 198 because the condition on line 197 was never true

198 continue 

199 

200 value = self._extract_value(node.value) 

201 

202 assignments.append({"variable": var_name, "value": value, "lineno": node.lineno}) 

203 

204 return assignments 

205 

206 def _extract_value(self, node: ast.AST) -> Any: 

207 """ 

208 Extract value from AST node. 

209 

210 Handles constants, names, lists, dicts, calls, etc. 

211 

212 Args: 

213 node: AST node 

214 

215 Returns: 

216 Extracted value (str representation for complex types) 

217 """ 

218 if isinstance(node, ast.Constant): 

219 return node.value 

220 elif isinstance(node, ast.Name): 

221 return node.id 

222 elif isinstance(node, ast.List): 222 ↛ 223line 222 didn't jump to line 223 because the condition on line 222 was never true

223 return [self._extract_value(elt) for elt in node.elts] 

224 elif isinstance(node, ast.Dict): 224 ↛ 225line 224 didn't jump to line 225 because the condition on line 224 was never true

225 return { 

226 self._extract_value(k): self._extract_value(v) 

227 for k, v in zip(node.keys, node.values, strict=False) 

228 if k is not None 

229 } 

230 elif isinstance(node, ast.Call): 

231 # Return string representation of call 

232 if isinstance(node.func, ast.Name): 232 ↛ 234line 232 didn't jump to line 234 because the condition on line 232 was always true

233 return f"{node.func.id}(...)" 

234 elif isinstance(node.func, ast.Attribute): 

235 return f"{node.func.attr}(...)" 

236 return "call(...)" 

237 else: 

238 # Return string representation for complex types 

239 return ast.unparse(node) if hasattr(ast, "unparse") else "..." 

240 

241 def get_imports(self, tree: ast.Module | None = None) -> list[str]: 

242 """ 

243 Extract all imports from code. 

244 

245 Args: 

246 tree: AST tree 

247 

248 Returns: 

249 List of imported modules 

250 

251 Example: 

252 >>> imports = parser.get_imports(tree) 

253 >>> "langgraph" in imports 

254 True 

255 """ 

256 tree = tree or self.ast_tree 

257 if not tree: 

258 return [] 

259 

260 imports = [] 

261 

262 for node in ast.walk(tree): 

263 if isinstance(node, ast.Import): 

264 for alias in node.names: 

265 imports.append(alias.name) 

266 elif isinstance(node, ast.ImportFrom) and node.module: 

267 imports.append(node.module) 

268 

269 return imports 

270 

271 

272# ============================================================================== 

273# Example Usage 

274# ============================================================================== 

275 

276if __name__ == "__main__": 

277 # Example Python code 

278 sample_code = """ 

279from langgraph.graph import StateGraph 

280 

281class MyState(TypedDict): 

282 query: str 

283 result: str 

284 

285graph = StateGraph(MyState) 

286graph.add_node("search", search_function) 

287graph.add_node("summarize", summarize_function) 

288graph.add_edge("search", "summarize") 

289graph.set_entry_point("search") 

290 

291app = graph.compile() 

292""" 

293 

294 # Parse code 

295 parser = PythonCodeParser() 

296 tree = parser.parse_code(sample_code) 

297 

298 print("=" * 80) 

299 print("AST PARSER - TEST RUN") 

300 print("=" * 80) 

301 

302 # Find imports 

303 imports = parser.get_imports(tree) 

304 print(f"\nImports found: {imports}") 

305 

306 # Find add_node calls 

307 add_node_calls = parser.find_function_calls(tree, "add_node") 

308 print(f"\nadd_node calls: {len(add_node_calls)}") 

309 for call in add_node_calls: 

310 print(f" - Line {call['lineno']}: add_node({call['args']})") 

311 

312 # Find add_edge calls 

313 add_edge_calls = parser.find_function_calls(tree, "add_edge") 

314 print(f"\nadd_edge calls: {len(add_edge_calls)}") 

315 for call in add_edge_calls: 

316 print(f" - Line {call['lineno']}: add_edge({call['args']})") 

317 

318 # Find class definitions 

319 classes = parser.find_class_definitions(tree) 

320 print(f"\nClasses found: {len(classes)}") 

321 for cls in classes: 

322 print(f" - {cls['name']} (bases: {cls['bases']})") 

323 

324 print("\n" + "=" * 80)