aloop 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of aloop might be problematic. Click here for more details.
- agent/__init__.py +0 -0
- agent/agent.py +182 -0
- agent/base.py +406 -0
- agent/context.py +126 -0
- agent/todo.py +149 -0
- agent/tool_executor.py +54 -0
- agent/verification.py +135 -0
- aloop-0.1.0.dist-info/METADATA +246 -0
- aloop-0.1.0.dist-info/RECORD +62 -0
- aloop-0.1.0.dist-info/WHEEL +5 -0
- aloop-0.1.0.dist-info/entry_points.txt +2 -0
- aloop-0.1.0.dist-info/licenses/LICENSE +21 -0
- aloop-0.1.0.dist-info/top_level.txt +9 -0
- cli.py +19 -0
- config.py +146 -0
- interactive.py +865 -0
- llm/__init__.py +51 -0
- llm/base.py +26 -0
- llm/compat.py +226 -0
- llm/content_utils.py +309 -0
- llm/litellm_adapter.py +450 -0
- llm/message_types.py +245 -0
- llm/model_manager.py +265 -0
- llm/retry.py +95 -0
- main.py +246 -0
- memory/__init__.py +20 -0
- memory/compressor.py +554 -0
- memory/manager.py +538 -0
- memory/serialization.py +82 -0
- memory/short_term.py +88 -0
- memory/token_tracker.py +203 -0
- memory/types.py +51 -0
- tools/__init__.py +6 -0
- tools/advanced_file_ops.py +557 -0
- tools/base.py +51 -0
- tools/calculator.py +50 -0
- tools/code_navigator.py +975 -0
- tools/explore.py +254 -0
- tools/file_ops.py +150 -0
- tools/git_tools.py +791 -0
- tools/notify.py +69 -0
- tools/parallel_execute.py +420 -0
- tools/session_manager.py +205 -0
- tools/shell.py +147 -0
- tools/shell_background.py +470 -0
- tools/smart_edit.py +491 -0
- tools/todo.py +130 -0
- tools/web_fetch.py +673 -0
- tools/web_search.py +61 -0
- utils/__init__.py +15 -0
- utils/logger.py +105 -0
- utils/model_pricing.py +49 -0
- utils/runtime.py +75 -0
- utils/terminal_ui.py +422 -0
- utils/tui/__init__.py +39 -0
- utils/tui/command_registry.py +49 -0
- utils/tui/components.py +306 -0
- utils/tui/input_handler.py +393 -0
- utils/tui/model_ui.py +204 -0
- utils/tui/progress.py +292 -0
- utils/tui/status_bar.py +178 -0
- utils/tui/theme.py +165 -0
tools/code_navigator.py
ADDED
|
@@ -0,0 +1,975 @@
|
|
|
1
|
+
"""Code navigation tool using AST analysis for fast and accurate code location.
|
|
2
|
+
|
|
3
|
+
This tool provides intelligent code navigation capabilities:
|
|
4
|
+
- Find function/class definitions quickly across multiple languages
|
|
5
|
+
- Show file structure (imports, classes, functions)
|
|
6
|
+
- Find usages of functions/classes
|
|
7
|
+
- Supports Python, JavaScript/TypeScript, Go, Rust, Java, Kotlin, C/C++
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import ast
|
|
11
|
+
import asyncio
|
|
12
|
+
import warnings
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Dict, List, Optional, TypedDict
|
|
15
|
+
|
|
16
|
+
import aiofiles
|
|
17
|
+
import aiofiles.os
|
|
18
|
+
|
|
19
|
+
from tools.base import BaseTool
|
|
20
|
+
|
|
21
|
+
# Try to import tree-sitter-languages for multi-language support
|
|
22
|
+
try:
|
|
23
|
+
# tree_sitter_languages may trigger a FutureWarning from tree_sitter about
|
|
24
|
+
# Language(path, name) being deprecated. This is a dependency-level warning
|
|
25
|
+
# and is safe to suppress locally to keep test output clean.
|
|
26
|
+
with warnings.catch_warnings():
|
|
27
|
+
warnings.filterwarnings(
|
|
28
|
+
"ignore",
|
|
29
|
+
category=FutureWarning,
|
|
30
|
+
module=r"^tree_sitter(\.|$)",
|
|
31
|
+
)
|
|
32
|
+
from tree_sitter_languages import get_language, get_parser
|
|
33
|
+
|
|
34
|
+
HAS_TREE_SITTER = True
|
|
35
|
+
except ImportError:
|
|
36
|
+
HAS_TREE_SITTER = False
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class _FunctionResult(TypedDict):
|
|
40
|
+
file: str
|
|
41
|
+
line: int
|
|
42
|
+
signature: str
|
|
43
|
+
docstring: str
|
|
44
|
+
decorators: List[str]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class _ClassResult(TypedDict):
|
|
48
|
+
file: str
|
|
49
|
+
line: int
|
|
50
|
+
bases: List[str]
|
|
51
|
+
methods: List[str]
|
|
52
|
+
docstring: str
|
|
53
|
+
decorators: List[str]
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# Language extension mapping
|
|
57
|
+
EXTENSION_TO_LANGUAGE = {
|
|
58
|
+
".py": "python",
|
|
59
|
+
".js": "javascript",
|
|
60
|
+
".jsx": "javascript",
|
|
61
|
+
".ts": "typescript",
|
|
62
|
+
".tsx": "typescript",
|
|
63
|
+
".go": "go",
|
|
64
|
+
".rs": "rust",
|
|
65
|
+
".java": "java",
|
|
66
|
+
".kt": "kotlin",
|
|
67
|
+
".kts": "kotlin",
|
|
68
|
+
".cpp": "cpp",
|
|
69
|
+
".cc": "cpp",
|
|
70
|
+
".cxx": "cpp",
|
|
71
|
+
".c": "c",
|
|
72
|
+
".h": "c",
|
|
73
|
+
".hpp": "cpp",
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
# Tree-sitter query patterns for function definitions by language
|
|
77
|
+
FUNCTION_QUERIES = {
|
|
78
|
+
"python": "(function_definition name: (identifier) @name)",
|
|
79
|
+
"javascript": """[
|
|
80
|
+
(function_declaration name: (identifier) @name)
|
|
81
|
+
(method_definition name: (property_identifier) @name)
|
|
82
|
+
(arrow_function) @arrow
|
|
83
|
+
]""",
|
|
84
|
+
"typescript": """[
|
|
85
|
+
(function_declaration name: (identifier) @name)
|
|
86
|
+
(method_definition name: (property_identifier) @name)
|
|
87
|
+
(arrow_function) @arrow
|
|
88
|
+
]""",
|
|
89
|
+
"go": "(function_declaration name: (identifier) @name)",
|
|
90
|
+
"rust": "(function_item name: (identifier) @name)",
|
|
91
|
+
"java": "(method_declaration name: (identifier) @name)",
|
|
92
|
+
"kotlin": "(function_declaration (simple_identifier) @name)",
|
|
93
|
+
"cpp": "(function_definition declarator: (function_declarator declarator: (_) @name))",
|
|
94
|
+
"c": "(function_definition declarator: (function_declarator declarator: (_) @name))",
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# Tree-sitter query patterns for class definitions by language
|
|
98
|
+
CLASS_QUERIES = {
|
|
99
|
+
"python": "(class_definition name: (identifier) @name)",
|
|
100
|
+
"javascript": "(class_declaration name: (identifier) @name)",
|
|
101
|
+
"typescript": """[
|
|
102
|
+
(class_declaration name: (type_identifier) @name)
|
|
103
|
+
(interface_declaration name: (type_identifier) @name)
|
|
104
|
+
]""",
|
|
105
|
+
"go": "(type_declaration (type_spec name: (type_identifier) @name))",
|
|
106
|
+
"rust": """[
|
|
107
|
+
(struct_item name: (type_identifier) @name)
|
|
108
|
+
(impl_item type: (type_identifier) @name)
|
|
109
|
+
(trait_item name: (type_identifier) @name)
|
|
110
|
+
]""",
|
|
111
|
+
"java": """[
|
|
112
|
+
(class_declaration name: (identifier) @name)
|
|
113
|
+
(interface_declaration name: (identifier) @name)
|
|
114
|
+
]""",
|
|
115
|
+
"kotlin": "(class_declaration (type_identifier) @name)",
|
|
116
|
+
"cpp": """[
|
|
117
|
+
(class_specifier name: (type_identifier) @name)
|
|
118
|
+
(struct_specifier name: (type_identifier) @name)
|
|
119
|
+
]""",
|
|
120
|
+
"c": "(struct_specifier name: (type_identifier) @name)",
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
# File patterns by language for iteration
|
|
124
|
+
LANGUAGE_FILE_PATTERNS = {
|
|
125
|
+
"python": ["*.py"],
|
|
126
|
+
"javascript": ["*.js", "*.jsx"],
|
|
127
|
+
"typescript": ["*.ts", "*.tsx"],
|
|
128
|
+
"go": ["*.go"],
|
|
129
|
+
"rust": ["*.rs"],
|
|
130
|
+
"java": ["*.java"],
|
|
131
|
+
"kotlin": ["*.kt", "*.kts"],
|
|
132
|
+
"cpp": ["*.cpp", "*.cc", "*.cxx", "*.hpp"],
|
|
133
|
+
"c": ["*.c", "*.h"],
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def detect_language(file_path: Path) -> Optional[str]:
|
|
138
|
+
"""Detect language from file extension."""
|
|
139
|
+
return EXTENSION_TO_LANGUAGE.get(file_path.suffix.lower())
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def get_supported_languages() -> List[str]:
|
|
143
|
+
"""Return list of supported languages."""
|
|
144
|
+
return list(FUNCTION_QUERIES.keys())
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class CodeNavigatorTool(BaseTool):
|
|
148
|
+
"""Navigate code using AST analysis - fast and accurate, multi-language support."""
|
|
149
|
+
|
|
150
|
+
def __init__(self):
|
|
151
|
+
self.cache_dir = Path(".aloop/cache")
|
|
152
|
+
self.symbol_cache = {} # {symbol_name: [(file, line, type, info)]}
|
|
153
|
+
self.cache_loaded = False
|
|
154
|
+
self._tree_sitter_available = HAS_TREE_SITTER
|
|
155
|
+
|
|
156
|
+
def _get_tree_sitter_parser_and_language(self, lang: str):
|
|
157
|
+
"""Get a tree-sitter parser and language.
|
|
158
|
+
|
|
159
|
+
tree_sitter_languages currently triggers a FutureWarning via tree_sitter
|
|
160
|
+
(Language(path, name) deprecation). This is dependency-level noise, so we
|
|
161
|
+
suppress it locally around the calls that instantiate Language objects.
|
|
162
|
+
"""
|
|
163
|
+
with warnings.catch_warnings():
|
|
164
|
+
warnings.filterwarnings(
|
|
165
|
+
"ignore",
|
|
166
|
+
category=FutureWarning,
|
|
167
|
+
module=r"^tree_sitter(\.|$)",
|
|
168
|
+
)
|
|
169
|
+
return get_parser(lang), get_language(lang)
|
|
170
|
+
|
|
171
|
+
@property
|
|
172
|
+
def name(self) -> str:
|
|
173
|
+
return "code_navigator"
|
|
174
|
+
|
|
175
|
+
@property
|
|
176
|
+
def description(self) -> str:
|
|
177
|
+
langs = ", ".join(get_supported_languages()) if HAS_TREE_SITTER else "Python"
|
|
178
|
+
return f"""Fast code navigation using AST analysis (MUCH better than grep for code).
|
|
179
|
+
|
|
180
|
+
This tool understands code structure and can quickly find definitions and usages.
|
|
181
|
+
Supported languages: {langs}
|
|
182
|
+
|
|
183
|
+
Search types:
|
|
184
|
+
1. find_function: Find function definitions by name
|
|
185
|
+
- Returns: file path, line number, function signature, docstring
|
|
186
|
+
- Example: code_navigator(target="compress", search_type="find_function")
|
|
187
|
+
|
|
188
|
+
2. find_class: Find class definitions by name
|
|
189
|
+
- Returns: file path, line number, base classes, methods list
|
|
190
|
+
- Example: code_navigator(target="BaseAgent", search_type="find_class")
|
|
191
|
+
|
|
192
|
+
3. show_structure: Show structure of a specific file
|
|
193
|
+
- Returns: imports, classes, functions in tree format
|
|
194
|
+
- Example: code_navigator(target="agent/base.py", search_type="show_structure")
|
|
195
|
+
|
|
196
|
+
4. find_usages: Find where a function/class is called or used
|
|
197
|
+
- Returns: all usage locations (file + line number + context)
|
|
198
|
+
- Example: code_navigator(target="_react_loop", search_type="find_usages")
|
|
199
|
+
|
|
200
|
+
WHY USE THIS INSTEAD OF GREP:
|
|
201
|
+
- 10x faster for finding code elements
|
|
202
|
+
- Understands code structure (not just text matching)
|
|
203
|
+
- Returns exact line numbers and signatures
|
|
204
|
+
- No false positives from comments or strings
|
|
205
|
+
- Can distinguish between definitions and usages
|
|
206
|
+
|
|
207
|
+
WHEN TO USE:
|
|
208
|
+
- Finding where a function is defined: use find_function
|
|
209
|
+
- Finding where a class is defined: use find_class
|
|
210
|
+
- Understanding file structure: use show_structure
|
|
211
|
+
- Finding all places a function is called: use find_usages
|
|
212
|
+
|
|
213
|
+
WHEN TO USE GREP INSTEAD:
|
|
214
|
+
- Searching for string literals or text content
|
|
215
|
+
- Finding TODO/FIXME comments
|
|
216
|
+
- Searching in non-supported languages"""
|
|
217
|
+
|
|
218
|
+
@property
|
|
219
|
+
def parameters(self) -> Dict[str, Any]:
|
|
220
|
+
return {
|
|
221
|
+
"target": {
|
|
222
|
+
"type": "string",
|
|
223
|
+
"description": "What to search for: function name, class name, or file path (for show_structure)",
|
|
224
|
+
},
|
|
225
|
+
"search_type": {
|
|
226
|
+
"type": "string",
|
|
227
|
+
"description": "Type of search: find_function, find_class, show_structure, or find_usages",
|
|
228
|
+
"enum": ["find_function", "find_class", "show_structure", "find_usages"],
|
|
229
|
+
},
|
|
230
|
+
"path": {
|
|
231
|
+
"type": "string",
|
|
232
|
+
"description": "Optional: limit search to specific directory (default: current directory)",
|
|
233
|
+
},
|
|
234
|
+
"language": {
|
|
235
|
+
"type": "string",
|
|
236
|
+
"description": "Optional: limit search to specific language (e.g., 'python', 'javascript', 'go')",
|
|
237
|
+
},
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async def execute(
|
|
241
|
+
self, target: str, search_type: str, path: str = ".", language: str = None, **kwargs
|
|
242
|
+
) -> str:
|
|
243
|
+
"""Execute code navigation search."""
|
|
244
|
+
try:
|
|
245
|
+
base_path = Path(path)
|
|
246
|
+
if not await aiofiles.os.path.exists(str(base_path)):
|
|
247
|
+
return f"Error: Path does not exist: {path}"
|
|
248
|
+
|
|
249
|
+
if search_type == "find_function":
|
|
250
|
+
return await self._find_function(target, base_path, language)
|
|
251
|
+
elif search_type == "find_class":
|
|
252
|
+
return await self._find_class(target, base_path, language)
|
|
253
|
+
elif search_type == "show_structure":
|
|
254
|
+
return await self._show_structure(target)
|
|
255
|
+
elif search_type == "find_usages":
|
|
256
|
+
return await self._find_usages(target, base_path, language)
|
|
257
|
+
else:
|
|
258
|
+
return f"Error: Unknown search_type '{search_type}'"
|
|
259
|
+
|
|
260
|
+
except Exception as e:
|
|
261
|
+
return f"Error executing code_navigator: {str(e)}"
|
|
262
|
+
|
|
263
|
+
async def _iter_source_files(
|
|
264
|
+
self, base_path: Path, language: Optional[str] = None
|
|
265
|
+
) -> List[Path]:
|
|
266
|
+
"""Iterate over source files, optionally filtered by language."""
|
|
267
|
+
files = []
|
|
268
|
+
|
|
269
|
+
if language:
|
|
270
|
+
# Filter by specific language
|
|
271
|
+
patterns = LANGUAGE_FILE_PATTERNS.get(language, [])
|
|
272
|
+
for pattern in patterns:
|
|
273
|
+
matches = await asyncio.to_thread(
|
|
274
|
+
lambda pattern=pattern: list(base_path.rglob(pattern))
|
|
275
|
+
)
|
|
276
|
+
files.extend(matches)
|
|
277
|
+
else:
|
|
278
|
+
# All supported languages
|
|
279
|
+
for patterns in LANGUAGE_FILE_PATTERNS.values():
|
|
280
|
+
for pattern in patterns:
|
|
281
|
+
matches = await asyncio.to_thread(
|
|
282
|
+
lambda pattern=pattern: list(base_path.rglob(pattern))
|
|
283
|
+
)
|
|
284
|
+
files.extend(matches)
|
|
285
|
+
|
|
286
|
+
# Deduplicate and exclude common non-code directories
|
|
287
|
+
seen = set()
|
|
288
|
+
result = []
|
|
289
|
+
exclude_dirs = {".git", "node_modules", "__pycache__", ".venv", "venv", "target", "build"}
|
|
290
|
+
|
|
291
|
+
for f in files:
|
|
292
|
+
if f in seen:
|
|
293
|
+
continue
|
|
294
|
+
seen.add(f)
|
|
295
|
+
|
|
296
|
+
# Skip excluded directories
|
|
297
|
+
skip = False
|
|
298
|
+
for part in f.parts:
|
|
299
|
+
if part in exclude_dirs:
|
|
300
|
+
skip = True
|
|
301
|
+
break
|
|
302
|
+
if not skip:
|
|
303
|
+
result.append(f)
|
|
304
|
+
|
|
305
|
+
return sorted(result)
|
|
306
|
+
|
|
307
|
+
async def _find_function_with_tree_sitter(
|
|
308
|
+
self, name: str, file_path: Path, lang: str
|
|
309
|
+
) -> List[Dict]:
|
|
310
|
+
"""Find functions using tree-sitter."""
|
|
311
|
+
results = []
|
|
312
|
+
|
|
313
|
+
if lang not in FUNCTION_QUERIES:
|
|
314
|
+
return results
|
|
315
|
+
|
|
316
|
+
try:
|
|
317
|
+
parser, language = self._get_tree_sitter_parser_and_language(lang)
|
|
318
|
+
query = language.query(FUNCTION_QUERIES[lang])
|
|
319
|
+
|
|
320
|
+
async with aiofiles.open(file_path, "rb") as f:
|
|
321
|
+
code = await f.read()
|
|
322
|
+
tree = parser.parse(code)
|
|
323
|
+
captures = query.captures(tree.root_node)
|
|
324
|
+
|
|
325
|
+
for node, capture_name in captures:
|
|
326
|
+
if capture_name == "name":
|
|
327
|
+
func_name = node.text.decode("utf-8")
|
|
328
|
+
if func_name == name:
|
|
329
|
+
# Get the full function node (parent)
|
|
330
|
+
func_node = node.parent
|
|
331
|
+
while func_node and not func_node.type.endswith(
|
|
332
|
+
(
|
|
333
|
+
"function_definition",
|
|
334
|
+
"function_declaration",
|
|
335
|
+
"function_item",
|
|
336
|
+
"method_declaration",
|
|
337
|
+
"method_definition",
|
|
338
|
+
)
|
|
339
|
+
):
|
|
340
|
+
func_node = func_node.parent
|
|
341
|
+
|
|
342
|
+
start_line = node.start_point[0] + 1
|
|
343
|
+
end_line = func_node.end_point[0] + 1 if func_node else start_line
|
|
344
|
+
|
|
345
|
+
# Try to extract signature (first line of function)
|
|
346
|
+
lines = code.decode("utf-8", errors="replace").splitlines()
|
|
347
|
+
signature = (
|
|
348
|
+
lines[start_line - 1].strip() if start_line <= len(lines) else ""
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
results.append(
|
|
352
|
+
{
|
|
353
|
+
"file": str(file_path),
|
|
354
|
+
"line": start_line,
|
|
355
|
+
"end_line": end_line,
|
|
356
|
+
"signature": signature,
|
|
357
|
+
"docstring": "(tree-sitter: no docstring extraction)",
|
|
358
|
+
"decorators": [],
|
|
359
|
+
"language": lang,
|
|
360
|
+
}
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
except Exception:
|
|
364
|
+
pass
|
|
365
|
+
|
|
366
|
+
return results
|
|
367
|
+
|
|
368
|
+
async def _find_class_with_tree_sitter(
|
|
369
|
+
self, name: str, file_path: Path, lang: str
|
|
370
|
+
) -> List[Dict]:
|
|
371
|
+
"""Find classes using tree-sitter."""
|
|
372
|
+
results = []
|
|
373
|
+
|
|
374
|
+
if lang not in CLASS_QUERIES:
|
|
375
|
+
return results
|
|
376
|
+
|
|
377
|
+
try:
|
|
378
|
+
parser, language = self._get_tree_sitter_parser_and_language(lang)
|
|
379
|
+
query = language.query(CLASS_QUERIES[lang])
|
|
380
|
+
|
|
381
|
+
async with aiofiles.open(file_path, "rb") as f:
|
|
382
|
+
code = await f.read()
|
|
383
|
+
tree = parser.parse(code)
|
|
384
|
+
captures = query.captures(tree.root_node)
|
|
385
|
+
|
|
386
|
+
for node, capture_name in captures:
|
|
387
|
+
if capture_name == "name":
|
|
388
|
+
class_name = node.text.decode("utf-8")
|
|
389
|
+
if class_name == name:
|
|
390
|
+
start_line = node.start_point[0] + 1
|
|
391
|
+
|
|
392
|
+
# Get the class node for methods extraction
|
|
393
|
+
class_node = node.parent
|
|
394
|
+
while class_node and not class_node.type.endswith(
|
|
395
|
+
(
|
|
396
|
+
"class_definition",
|
|
397
|
+
"class_declaration",
|
|
398
|
+
"class_specifier",
|
|
399
|
+
"struct_item",
|
|
400
|
+
"struct_specifier",
|
|
401
|
+
"type_spec",
|
|
402
|
+
"impl_item",
|
|
403
|
+
"trait_item",
|
|
404
|
+
"interface_declaration",
|
|
405
|
+
)
|
|
406
|
+
):
|
|
407
|
+
class_node = class_node.parent
|
|
408
|
+
|
|
409
|
+
results.append(
|
|
410
|
+
{
|
|
411
|
+
"file": str(file_path),
|
|
412
|
+
"line": start_line,
|
|
413
|
+
"bases": [],
|
|
414
|
+
"methods": [],
|
|
415
|
+
"docstring": "(tree-sitter: no docstring extraction)",
|
|
416
|
+
"decorators": [],
|
|
417
|
+
"language": lang,
|
|
418
|
+
}
|
|
419
|
+
)
|
|
420
|
+
|
|
421
|
+
except Exception:
|
|
422
|
+
pass
|
|
423
|
+
|
|
424
|
+
return results
|
|
425
|
+
|
|
426
|
+
async def _find_function(
|
|
427
|
+
self, name: str, base_path: Path, language: Optional[str] = None
|
|
428
|
+
) -> str:
|
|
429
|
+
"""Find all function definitions matching the name."""
|
|
430
|
+
results: List[Dict] = []
|
|
431
|
+
|
|
432
|
+
for source_file in await self._iter_source_files(base_path, language):
|
|
433
|
+
lang = detect_language(source_file)
|
|
434
|
+
if not lang:
|
|
435
|
+
continue
|
|
436
|
+
|
|
437
|
+
# Use Python AST for Python files (more detailed output)
|
|
438
|
+
if lang == "python":
|
|
439
|
+
try:
|
|
440
|
+
async with aiofiles.open(source_file, encoding="utf-8") as f:
|
|
441
|
+
content = await f.read()
|
|
442
|
+
tree = ast.parse(content, filename=str(source_file))
|
|
443
|
+
|
|
444
|
+
for node in ast.walk(tree):
|
|
445
|
+
if isinstance(node, ast.FunctionDef) and node.name == name:
|
|
446
|
+
args = self._format_function_args(node.args)
|
|
447
|
+
signature = f"def {node.name}({args})"
|
|
448
|
+
|
|
449
|
+
if node.returns:
|
|
450
|
+
try:
|
|
451
|
+
return_type = ast.unparse(node.returns)
|
|
452
|
+
signature += f" -> {return_type}"
|
|
453
|
+
except Exception:
|
|
454
|
+
pass
|
|
455
|
+
|
|
456
|
+
docstring = ast.get_docstring(node)
|
|
457
|
+
decorators = [self._format_decorator(d) for d in node.decorator_list]
|
|
458
|
+
|
|
459
|
+
try:
|
|
460
|
+
rel_path = str(source_file.relative_to(base_path))
|
|
461
|
+
except ValueError:
|
|
462
|
+
rel_path = str(source_file)
|
|
463
|
+
|
|
464
|
+
results.append(
|
|
465
|
+
{
|
|
466
|
+
"file": rel_path,
|
|
467
|
+
"line": node.lineno,
|
|
468
|
+
"signature": signature,
|
|
469
|
+
"docstring": docstring or "(no docstring)",
|
|
470
|
+
"decorators": decorators,
|
|
471
|
+
}
|
|
472
|
+
)
|
|
473
|
+
except SyntaxError:
|
|
474
|
+
continue
|
|
475
|
+
except Exception:
|
|
476
|
+
continue
|
|
477
|
+
|
|
478
|
+
# Use tree-sitter for other languages
|
|
479
|
+
elif self._tree_sitter_available:
|
|
480
|
+
try:
|
|
481
|
+
rel_path = str(source_file.relative_to(base_path))
|
|
482
|
+
except ValueError:
|
|
483
|
+
rel_path = str(source_file)
|
|
484
|
+
|
|
485
|
+
ts_results = await self._find_function_with_tree_sitter(name, source_file, lang)
|
|
486
|
+
for r in ts_results:
|
|
487
|
+
r["file"] = rel_path
|
|
488
|
+
results.append(r)
|
|
489
|
+
|
|
490
|
+
if not results:
|
|
491
|
+
return f"No function named '{name}' found in {base_path}"
|
|
492
|
+
|
|
493
|
+
# Format results
|
|
494
|
+
output_parts = [f"Found {len(results)} function(s) named '{name}':\n"]
|
|
495
|
+
for r in results:
|
|
496
|
+
lang_tag = f" [{r.get('language', 'python')}]" if r.get("language") else ""
|
|
497
|
+
output_parts.append(f"📍 {r['file']}:{r['line']}{lang_tag}")
|
|
498
|
+
if r.get("decorators"):
|
|
499
|
+
output_parts.append(f" Decorators: {', '.join(r['decorators'])}")
|
|
500
|
+
output_parts.append(f" {r['signature']}")
|
|
501
|
+
doc = r["docstring"]
|
|
502
|
+
if len(doc) > 100:
|
|
503
|
+
doc = doc[:100] + "..."
|
|
504
|
+
output_parts.append(f' "{doc}"\n')
|
|
505
|
+
|
|
506
|
+
return "\n".join(output_parts)
|
|
507
|
+
|
|
508
|
+
async def _find_class(self, name: str, base_path: Path, language: Optional[str] = None) -> str:
|
|
509
|
+
"""Find all class definitions matching the name."""
|
|
510
|
+
results: List[Dict] = []
|
|
511
|
+
|
|
512
|
+
for source_file in await self._iter_source_files(base_path, language):
|
|
513
|
+
lang = detect_language(source_file)
|
|
514
|
+
if not lang:
|
|
515
|
+
continue
|
|
516
|
+
|
|
517
|
+
# Use Python AST for Python files (more detailed output)
|
|
518
|
+
if lang == "python":
|
|
519
|
+
try:
|
|
520
|
+
async with aiofiles.open(source_file, encoding="utf-8") as f:
|
|
521
|
+
content = await f.read()
|
|
522
|
+
tree = ast.parse(content, filename=str(source_file))
|
|
523
|
+
|
|
524
|
+
for node in ast.walk(tree):
|
|
525
|
+
if isinstance(node, ast.ClassDef) and node.name == name:
|
|
526
|
+
bases = [self._format_base_class(b) for b in node.bases]
|
|
527
|
+
methods = [
|
|
528
|
+
item.name for item in node.body if isinstance(item, ast.FunctionDef)
|
|
529
|
+
]
|
|
530
|
+
|
|
531
|
+
docstring = ast.get_docstring(node)
|
|
532
|
+
decorators = [self._format_decorator(d) for d in node.decorator_list]
|
|
533
|
+
|
|
534
|
+
try:
|
|
535
|
+
rel_path = str(source_file.relative_to(base_path))
|
|
536
|
+
except ValueError:
|
|
537
|
+
rel_path = str(source_file)
|
|
538
|
+
|
|
539
|
+
results.append(
|
|
540
|
+
{
|
|
541
|
+
"file": rel_path,
|
|
542
|
+
"line": node.lineno,
|
|
543
|
+
"bases": bases,
|
|
544
|
+
"methods": methods,
|
|
545
|
+
"docstring": docstring or "(no docstring)",
|
|
546
|
+
"decorators": decorators,
|
|
547
|
+
}
|
|
548
|
+
)
|
|
549
|
+
except SyntaxError:
|
|
550
|
+
continue
|
|
551
|
+
except Exception:
|
|
552
|
+
continue
|
|
553
|
+
|
|
554
|
+
# Use tree-sitter for other languages
|
|
555
|
+
elif self._tree_sitter_available:
|
|
556
|
+
try:
|
|
557
|
+
rel_path = str(source_file.relative_to(base_path))
|
|
558
|
+
except ValueError:
|
|
559
|
+
rel_path = str(source_file)
|
|
560
|
+
|
|
561
|
+
ts_results = await self._find_class_with_tree_sitter(name, source_file, lang)
|
|
562
|
+
for r in ts_results:
|
|
563
|
+
r["file"] = rel_path
|
|
564
|
+
results.append(r)
|
|
565
|
+
|
|
566
|
+
if not results:
|
|
567
|
+
return f"No class named '{name}' found in {base_path}"
|
|
568
|
+
|
|
569
|
+
# Format results
|
|
570
|
+
output_parts = [f"Found {len(results)} class(es) named '{name}':\n"]
|
|
571
|
+
for r in results:
|
|
572
|
+
lang_tag = f" [{r.get('language', 'python')}]" if r.get("language") else ""
|
|
573
|
+
output_parts.append(f"📍 {r['file']}:{r['line']}{lang_tag}")
|
|
574
|
+
output_parts.append(
|
|
575
|
+
f" class {name}({', '.join(r['bases']) if r['bases'] else 'object'})"
|
|
576
|
+
)
|
|
577
|
+
if r.get("decorators"):
|
|
578
|
+
output_parts.append(f" Decorators: {', '.join(r['decorators'])}")
|
|
579
|
+
|
|
580
|
+
doc = r["docstring"]
|
|
581
|
+
if len(doc) > 100:
|
|
582
|
+
doc = doc[:100] + "..."
|
|
583
|
+
output_parts.append(f' "{doc}"')
|
|
584
|
+
|
|
585
|
+
if r.get("methods"):
|
|
586
|
+
output_parts.append(
|
|
587
|
+
f" Methods ({len(r['methods'])}): {', '.join(r['methods'][:10])}"
|
|
588
|
+
)
|
|
589
|
+
if len(r["methods"]) > 10:
|
|
590
|
+
output_parts.append(f" ... and {len(r['methods']) - 10} more")
|
|
591
|
+
output_parts.append("")
|
|
592
|
+
|
|
593
|
+
return "\n".join(output_parts)
|
|
594
|
+
|
|
595
|
+
async def _show_structure(self, file_path: str) -> str:
|
|
596
|
+
"""Show the structure of a specific file."""
|
|
597
|
+
path = Path(file_path)
|
|
598
|
+
if not await aiofiles.os.path.exists(str(path)):
|
|
599
|
+
return f"Error: File does not exist: {file_path}"
|
|
600
|
+
|
|
601
|
+
lang = detect_language(path)
|
|
602
|
+
|
|
603
|
+
# Use Python AST for Python files
|
|
604
|
+
if lang == "python":
|
|
605
|
+
return await self._show_structure_python(path)
|
|
606
|
+
elif self._tree_sitter_available and lang:
|
|
607
|
+
return await self._show_structure_tree_sitter(path, lang)
|
|
608
|
+
else:
|
|
609
|
+
return f"Error: Unsupported file type or tree-sitter not available for: {file_path}"
|
|
610
|
+
|
|
611
|
+
async def _show_structure_python(self, path: Path) -> str:
|
|
612
|
+
"""Show structure of Python file using AST."""
|
|
613
|
+
try:
|
|
614
|
+
async with aiofiles.open(path, encoding="utf-8") as f:
|
|
615
|
+
content = await f.read()
|
|
616
|
+
tree = ast.parse(content, filename=str(path))
|
|
617
|
+
|
|
618
|
+
structure = {
|
|
619
|
+
"imports": [],
|
|
620
|
+
"classes": [],
|
|
621
|
+
"functions": [],
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
for node in ast.iter_child_nodes(tree):
|
|
625
|
+
if isinstance(node, ast.Import):
|
|
626
|
+
for alias in node.names:
|
|
627
|
+
structure["imports"].append(
|
|
628
|
+
{
|
|
629
|
+
"line": node.lineno,
|
|
630
|
+
"type": "import",
|
|
631
|
+
"name": alias.name,
|
|
632
|
+
"as": alias.asname,
|
|
633
|
+
}
|
|
634
|
+
)
|
|
635
|
+
elif isinstance(node, ast.ImportFrom):
|
|
636
|
+
for alias in node.names:
|
|
637
|
+
structure["imports"].append(
|
|
638
|
+
{
|
|
639
|
+
"line": node.lineno,
|
|
640
|
+
"type": "from",
|
|
641
|
+
"module": node.module or "",
|
|
642
|
+
"name": alias.name,
|
|
643
|
+
"as": alias.asname,
|
|
644
|
+
}
|
|
645
|
+
)
|
|
646
|
+
elif isinstance(node, ast.ClassDef):
|
|
647
|
+
methods = [
|
|
648
|
+
{"name": item.name, "line": item.lineno}
|
|
649
|
+
for item in node.body
|
|
650
|
+
if isinstance(item, ast.FunctionDef)
|
|
651
|
+
]
|
|
652
|
+
structure["classes"].append(
|
|
653
|
+
{
|
|
654
|
+
"line": node.lineno,
|
|
655
|
+
"name": node.name,
|
|
656
|
+
"bases": [self._format_base_class(b) for b in node.bases],
|
|
657
|
+
"methods": methods,
|
|
658
|
+
"docstring": ast.get_docstring(node),
|
|
659
|
+
}
|
|
660
|
+
)
|
|
661
|
+
elif isinstance(node, ast.FunctionDef):
|
|
662
|
+
args = self._format_function_args(node.args)
|
|
663
|
+
structure["functions"].append(
|
|
664
|
+
{
|
|
665
|
+
"line": node.lineno,
|
|
666
|
+
"name": node.name,
|
|
667
|
+
"args": args,
|
|
668
|
+
"docstring": ast.get_docstring(node),
|
|
669
|
+
}
|
|
670
|
+
)
|
|
671
|
+
|
|
672
|
+
return self._format_structure_output(str(path), structure, "python")
|
|
673
|
+
|
|
674
|
+
except SyntaxError as e:
|
|
675
|
+
return f"Error: Syntax error in {path} at line {e.lineno}: {e.msg}"
|
|
676
|
+
except Exception as e:
|
|
677
|
+
return f"Error parsing {path}: {str(e)}"
|
|
678
|
+
|
|
679
|
+
async def _show_structure_tree_sitter(self, path: Path, lang: str) -> str:
|
|
680
|
+
"""Show structure of file using tree-sitter."""
|
|
681
|
+
try:
|
|
682
|
+
parser, language = self._get_tree_sitter_parser_and_language(lang)
|
|
683
|
+
|
|
684
|
+
async with aiofiles.open(path, "rb") as f:
|
|
685
|
+
code = await f.read()
|
|
686
|
+
tree = parser.parse(code)
|
|
687
|
+
lines = code.decode("utf-8", errors="replace").splitlines()
|
|
688
|
+
|
|
689
|
+
structure = {
|
|
690
|
+
"imports": [],
|
|
691
|
+
"classes": [],
|
|
692
|
+
"functions": [],
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
# Find classes
|
|
696
|
+
if lang in CLASS_QUERIES:
|
|
697
|
+
try:
|
|
698
|
+
query = language.query(CLASS_QUERIES[lang])
|
|
699
|
+
captures = query.captures(tree.root_node)
|
|
700
|
+
for node, capture_name in captures:
|
|
701
|
+
if capture_name == "name":
|
|
702
|
+
class_name = node.text.decode("utf-8")
|
|
703
|
+
structure["classes"].append(
|
|
704
|
+
{
|
|
705
|
+
"line": node.start_point[0] + 1,
|
|
706
|
+
"name": class_name,
|
|
707
|
+
"bases": [],
|
|
708
|
+
"methods": [],
|
|
709
|
+
"docstring": None,
|
|
710
|
+
}
|
|
711
|
+
)
|
|
712
|
+
except Exception:
|
|
713
|
+
pass
|
|
714
|
+
|
|
715
|
+
# Find functions
|
|
716
|
+
if lang in FUNCTION_QUERIES:
|
|
717
|
+
try:
|
|
718
|
+
query = language.query(FUNCTION_QUERIES[lang])
|
|
719
|
+
captures = query.captures(tree.root_node)
|
|
720
|
+
for node, capture_name in captures:
|
|
721
|
+
if capture_name == "name":
|
|
722
|
+
func_name = node.text.decode("utf-8")
|
|
723
|
+
line_num = node.start_point[0] + 1
|
|
724
|
+
line_content = (
|
|
725
|
+
lines[line_num - 1].strip() if line_num <= len(lines) else ""
|
|
726
|
+
)
|
|
727
|
+
structure["functions"].append(
|
|
728
|
+
{
|
|
729
|
+
"line": line_num,
|
|
730
|
+
"name": func_name,
|
|
731
|
+
"args": line_content,
|
|
732
|
+
"docstring": None,
|
|
733
|
+
}
|
|
734
|
+
)
|
|
735
|
+
except Exception:
|
|
736
|
+
pass
|
|
737
|
+
|
|
738
|
+
return self._format_structure_output(str(path), structure, lang)
|
|
739
|
+
|
|
740
|
+
except Exception as e:
|
|
741
|
+
return f"Error parsing {path}: {str(e)}"
|
|
742
|
+
|
|
743
|
+
def _format_structure_output(self, file_path: str, structure: Dict, lang: str) -> str:
|
|
744
|
+
"""Format structure output."""
|
|
745
|
+
output_parts = [f"Structure of {file_path} [{lang}]:\n"]
|
|
746
|
+
|
|
747
|
+
# Imports
|
|
748
|
+
if structure["imports"]:
|
|
749
|
+
output_parts.append("📦 IMPORTS:")
|
|
750
|
+
for imp in structure["imports"][:20]:
|
|
751
|
+
if imp.get("type") == "import":
|
|
752
|
+
line = f" Line {imp['line']}: import {imp['name']}"
|
|
753
|
+
if imp.get("as"):
|
|
754
|
+
line += f" as {imp['as']}"
|
|
755
|
+
else:
|
|
756
|
+
line = (
|
|
757
|
+
f" Line {imp['line']}: from {imp.get('module', '')} import {imp['name']}"
|
|
758
|
+
)
|
|
759
|
+
if imp.get("as"):
|
|
760
|
+
line += f" as {imp['as']}"
|
|
761
|
+
output_parts.append(line)
|
|
762
|
+
if len(structure["imports"]) > 20:
|
|
763
|
+
output_parts.append(f" ... and {len(structure['imports']) - 20} more imports")
|
|
764
|
+
output_parts.append("")
|
|
765
|
+
|
|
766
|
+
# Classes
|
|
767
|
+
if structure["classes"]:
|
|
768
|
+
output_parts.append("📘 CLASSES:")
|
|
769
|
+
for cls in structure["classes"]:
|
|
770
|
+
bases_str = f"({', '.join(cls['bases'])})" if cls.get("bases") else ""
|
|
771
|
+
output_parts.append(f" Line {cls['line']}: class {cls['name']}{bases_str}")
|
|
772
|
+
if cls.get("docstring"):
|
|
773
|
+
doc = cls["docstring"]
|
|
774
|
+
if len(doc) > 60:
|
|
775
|
+
doc = doc[:60] + "..."
|
|
776
|
+
output_parts.append(f' "{doc}"')
|
|
777
|
+
if cls.get("methods"):
|
|
778
|
+
methods_str = ", ".join(m["name"] for m in cls["methods"][:5])
|
|
779
|
+
if len(cls["methods"]) > 5:
|
|
780
|
+
methods_str += f", ... (+{len(cls['methods']) - 5} more)"
|
|
781
|
+
output_parts.append(f" Methods: {methods_str}")
|
|
782
|
+
output_parts.append("")
|
|
783
|
+
|
|
784
|
+
# Functions
|
|
785
|
+
if structure["functions"]:
|
|
786
|
+
output_parts.append("🔧 FUNCTIONS:")
|
|
787
|
+
for func in structure["functions"]:
|
|
788
|
+
if lang == "python":
|
|
789
|
+
output_parts.append(
|
|
790
|
+
f" Line {func['line']}: def {func['name']}({func['args']})"
|
|
791
|
+
)
|
|
792
|
+
else:
|
|
793
|
+
output_parts.append(f" Line {func['line']}: {func['name']}")
|
|
794
|
+
if func.get("docstring"):
|
|
795
|
+
doc = func["docstring"]
|
|
796
|
+
if len(doc) > 60:
|
|
797
|
+
doc = doc[:60] + "..."
|
|
798
|
+
output_parts.append(f' "{doc}"')
|
|
799
|
+
output_parts.append("")
|
|
800
|
+
|
|
801
|
+
if not structure["imports"] and not structure["classes"] and not structure["functions"]:
|
|
802
|
+
output_parts.append("(File appears to be empty or contains only statements)")
|
|
803
|
+
|
|
804
|
+
return "\n".join(output_parts)
|
|
805
|
+
|
|
806
|
+
async def _find_usages(self, name: str, base_path: Path, language: Optional[str] = None) -> str:
|
|
807
|
+
"""Find where a function or class is used (called)."""
|
|
808
|
+
results = []
|
|
809
|
+
|
|
810
|
+
for source_file in await self._iter_source_files(base_path, language):
|
|
811
|
+
lang = detect_language(source_file)
|
|
812
|
+
if not lang:
|
|
813
|
+
continue
|
|
814
|
+
|
|
815
|
+
# Use Python AST for Python files
|
|
816
|
+
if lang == "python":
|
|
817
|
+
try:
|
|
818
|
+
async with aiofiles.open(source_file, encoding="utf-8") as f:
|
|
819
|
+
content = await f.read()
|
|
820
|
+
lines = content.splitlines()
|
|
821
|
+
tree = ast.parse(content, filename=str(source_file))
|
|
822
|
+
|
|
823
|
+
for node in ast.walk(tree):
|
|
824
|
+
if isinstance(node, ast.Call):
|
|
825
|
+
called_name = self._get_call_name(node.func)
|
|
826
|
+
if called_name == name:
|
|
827
|
+
line_num = node.lineno
|
|
828
|
+
context = (
|
|
829
|
+
lines[line_num - 1].strip() if line_num <= len(lines) else ""
|
|
830
|
+
)
|
|
831
|
+
|
|
832
|
+
try:
|
|
833
|
+
rel_path = str(source_file.relative_to(Path.cwd()))
|
|
834
|
+
except ValueError:
|
|
835
|
+
rel_path = str(source_file)
|
|
836
|
+
|
|
837
|
+
results.append(
|
|
838
|
+
{
|
|
839
|
+
"file": rel_path,
|
|
840
|
+
"line": line_num,
|
|
841
|
+
"type": "function_call",
|
|
842
|
+
"context": context,
|
|
843
|
+
}
|
|
844
|
+
)
|
|
845
|
+
|
|
846
|
+
elif isinstance(node, ast.Name) and node.id == name:
|
|
847
|
+
line_num = node.lineno
|
|
848
|
+
context = lines[line_num - 1].strip() if line_num <= len(lines) else ""
|
|
849
|
+
|
|
850
|
+
try:
|
|
851
|
+
rel_path = str(source_file.relative_to(base_path))
|
|
852
|
+
except ValueError:
|
|
853
|
+
rel_path = str(source_file)
|
|
854
|
+
|
|
855
|
+
results.append(
|
|
856
|
+
{
|
|
857
|
+
"file": rel_path,
|
|
858
|
+
"line": line_num,
|
|
859
|
+
"type": "name_reference",
|
|
860
|
+
"context": context,
|
|
861
|
+
}
|
|
862
|
+
)
|
|
863
|
+
except SyntaxError:
|
|
864
|
+
continue
|
|
865
|
+
except Exception:
|
|
866
|
+
continue
|
|
867
|
+
|
|
868
|
+
# For other languages, use simple text search for now
|
|
869
|
+
elif self._tree_sitter_available:
|
|
870
|
+
try:
|
|
871
|
+
async with aiofiles.open(source_file, encoding="utf-8") as f:
|
|
872
|
+
content = await f.read()
|
|
873
|
+
lines = content.splitlines()
|
|
874
|
+
|
|
875
|
+
for i, line in enumerate(lines):
|
|
876
|
+
if name in line:
|
|
877
|
+
try:
|
|
878
|
+
rel_path = str(source_file.relative_to(base_path))
|
|
879
|
+
except ValueError:
|
|
880
|
+
rel_path = str(source_file)
|
|
881
|
+
|
|
882
|
+
results.append(
|
|
883
|
+
{
|
|
884
|
+
"file": rel_path,
|
|
885
|
+
"line": i + 1,
|
|
886
|
+
"type": "text_match",
|
|
887
|
+
"context": line.strip(),
|
|
888
|
+
"language": lang,
|
|
889
|
+
}
|
|
890
|
+
)
|
|
891
|
+
except Exception:
|
|
892
|
+
continue
|
|
893
|
+
|
|
894
|
+
if not results:
|
|
895
|
+
return f"No usages of '{name}' found in {base_path}"
|
|
896
|
+
|
|
897
|
+
# Deduplicate results
|
|
898
|
+
seen = set()
|
|
899
|
+
unique_results = []
|
|
900
|
+
for r in results:
|
|
901
|
+
key = (r["file"], r["line"])
|
|
902
|
+
if key not in seen:
|
|
903
|
+
seen.add(key)
|
|
904
|
+
unique_results.append(r)
|
|
905
|
+
|
|
906
|
+
# Limit results
|
|
907
|
+
if len(unique_results) > 50:
|
|
908
|
+
unique_results = unique_results[:50]
|
|
909
|
+
truncated = True
|
|
910
|
+
else:
|
|
911
|
+
truncated = False
|
|
912
|
+
|
|
913
|
+
# Format results
|
|
914
|
+
output_parts = [f"Found {len(seen)} usage(s) of '{name}':\n"]
|
|
915
|
+
|
|
916
|
+
for r in unique_results:
|
|
917
|
+
lang_tag = f" [{r.get('language', 'python')}]" if r.get("language") else ""
|
|
918
|
+
output_parts.append(f"📍 {r['file']}:{r['line']}{lang_tag}")
|
|
919
|
+
context = str(r["context"])
|
|
920
|
+
if len(context) > 80:
|
|
921
|
+
context = context[:80] + "..."
|
|
922
|
+
output_parts.append(f" {context}\n")
|
|
923
|
+
|
|
924
|
+
if truncated:
|
|
925
|
+
output_parts.append(f"... (showing first 50 of {len(seen)} usages)")
|
|
926
|
+
|
|
927
|
+
return "\n".join(output_parts)
|
|
928
|
+
|
|
929
|
+
# Helper methods
|
|
930
|
+
|
|
931
|
+
def _format_function_args(self, args: ast.arguments) -> str:
|
|
932
|
+
"""Format function arguments as string."""
|
|
933
|
+
arg_strs = []
|
|
934
|
+
|
|
935
|
+
for arg in args.args:
|
|
936
|
+
arg_str = arg.arg
|
|
937
|
+
if arg.annotation:
|
|
938
|
+
arg_str += f": {ast.unparse(arg.annotation)}"
|
|
939
|
+
arg_strs.append(arg_str)
|
|
940
|
+
|
|
941
|
+
if args.vararg:
|
|
942
|
+
arg_str = f"*{args.vararg.arg}"
|
|
943
|
+
if args.vararg.annotation:
|
|
944
|
+
arg_str += f": {ast.unparse(args.vararg.annotation)}"
|
|
945
|
+
arg_strs.append(arg_str)
|
|
946
|
+
|
|
947
|
+
if args.kwarg:
|
|
948
|
+
arg_str = f"**{args.kwarg.arg}"
|
|
949
|
+
if args.kwarg.annotation:
|
|
950
|
+
arg_str += f": {ast.unparse(args.kwarg.annotation)}"
|
|
951
|
+
arg_strs.append(arg_str)
|
|
952
|
+
|
|
953
|
+
return ", ".join(arg_strs)
|
|
954
|
+
|
|
955
|
+
def _format_base_class(self, node: ast.expr) -> str:
|
|
956
|
+
"""Format a base class node as string."""
|
|
957
|
+
try:
|
|
958
|
+
return ast.unparse(node)
|
|
959
|
+
except Exception:
|
|
960
|
+
return "?"
|
|
961
|
+
|
|
962
|
+
def _format_decorator(self, node: ast.expr) -> str:
|
|
963
|
+
"""Format a decorator node as string."""
|
|
964
|
+
try:
|
|
965
|
+
return "@" + ast.unparse(node)
|
|
966
|
+
except Exception:
|
|
967
|
+
return "@?"
|
|
968
|
+
|
|
969
|
+
def _get_call_name(self, node: ast.expr) -> Optional[str]:
|
|
970
|
+
"""Extract the name from a function call node."""
|
|
971
|
+
if isinstance(node, ast.Name):
|
|
972
|
+
return node.id
|
|
973
|
+
elif isinstance(node, ast.Attribute):
|
|
974
|
+
return node.attr
|
|
975
|
+
return None
|