Skip to main content
Glama
python_analyzer.py10.7 kB
#!/usr/bin/env python3 """ Python AST Analyzer Helper Parses Python files and extracts code structure information """ import ast import json import sys from typing import List, Dict, Any, Optional class PythonAnalyzer(ast.NodeVisitor): def __init__(self, source_code: str, file_path: str): self.source_code = source_code self.file_path = file_path self.lines = source_code.split('\n') self.classes = [] self.functions = [] self.imports = [] self.current_class = None def get_docstring(self, node) -> Optional[str]: """Extract docstring from a node""" return ast.get_docstring(node) def get_location(self, node) -> Dict[str, int]: """Get location information for a node""" return { 'startLine': node.lineno, 'startColumn': node.col_offset + 1, 'endLine': node.end_lineno or node.lineno, 'endColumn': (node.end_col_offset + 1) if node.end_col_offset is not None else node.col_offset + 1 } def get_type_annotation(self, annotation) -> Optional[str]: """Convert type annotation to string""" if annotation is None: return None return ast.unparse(annotation) def extract_decorators(self, node) -> List[str]: """Extract decorator names""" decorators = [] for decorator in node.decorator_list: if isinstance(decorator, ast.Name): decorators.append(decorator.id) elif isinstance(decorator, ast.Call) and isinstance(decorator.func, ast.Name): decorators.append(decorator.func.id) else: decorators.append(ast.unparse(decorator)) return decorators def extract_parameters(self, args: ast.arguments) -> List[Dict[str, Any]]: """Extract function parameters""" params = [] # Regular args for i, arg in enumerate(args.args): param = { 'name': arg.arg, 'type': self.get_type_annotation(arg.annotation), 'isOptional': False, 'defaultValue': None } # Check if has default value defaults_offset = len(args.args) - len(args.defaults) if i >= defaults_offset: default_idx = i - defaults_offset param['isOptional'] = True param['defaultValue'] = ast.unparse(args.defaults[default_idx]) params.append(param) # *args if args.vararg: params.append({ 'name': f"*{args.vararg.arg}", 'type': self.get_type_annotation(args.vararg.annotation), 'isOptional': True, 'defaultValue': None }) # **kwargs if args.kwarg: params.append({ 'name': f"**{args.kwarg.arg}", 'type': self.get_type_annotation(args.kwarg.annotation), 'isOptional': True, 'defaultValue': None }) return params def visit_ClassDef(self, node: ast.ClassDef): """Visit class definition""" class_info = { 'name': node.name, 'documentation': self.get_docstring(node), 'isExported': True, # Python doesn't have export keyword, assume public 'location': self.get_location(node), 'decorators': self.extract_decorators(node), 'baseClasses': [ast.unparse(base) for base in node.bases], 'methods': [], 'properties': [], 'constructors': [] } # Save current class context prev_class = self.current_class self.current_class = class_info # Visit class body for item in node.body: if isinstance(item, ast.FunctionDef): if item.name == '__init__': # Constructor constructor = { 'documentation': self.get_docstring(item), 'location': self.get_location(item), 'parameters': self.extract_parameters(item.args)[1:] # Skip 'self' } class_info['constructors'].append(constructor) else: # Method method = { 'name': item.name, 'documentation': self.get_docstring(item), 'location': self.get_location(item), 'isStatic': any(d.id == 'staticmethod' if isinstance(d, ast.Name) else False for d in item.decorator_list), 'isAsync': isinstance(item, ast.AsyncFunctionDef), 'decorators': self.extract_decorators(item), 'parameters': self.extract_parameters(item.args)[1:], # Skip 'self' 'returnType': self.get_type_annotation(item.returns) } class_info['methods'].append(method) elif isinstance(item, ast.AnnAssign) and isinstance(item.target, ast.Name): # Class variable with type annotation prop = { 'name': item.target.id, 'type': self.get_type_annotation(item.annotation), 'location': self.get_location(item) } class_info['properties'].append(prop) self.classes.append(class_info) self.current_class = prev_class self.generic_visit(node) def visit_FunctionDef(self, node: ast.FunctionDef): """Visit function definition (top-level only)""" # Only process top-level functions (not methods) if self.current_class is None: func_info = { 'name': node.name, 'documentation': self.get_docstring(node), 'isExported': True, # Assume exported 'isAsync': isinstance(node, ast.AsyncFunctionDef), 'decorators': self.extract_decorators(node), 'location': self.get_location(node), 'parameters': self.extract_parameters(node.args), 'returnType': self.get_type_annotation(node.returns) } self.functions.append(func_info) self.generic_visit(node) def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef): """Visit async function definition""" self.visit_FunctionDef(node) def visit_Import(self, node: ast.Import): """Visit import statement""" for alias in node.names: import_info = { 'importedNames': [alias.asname or alias.name], 'moduleSpecifier': alias.name, 'defaultImport': None, 'namespaceImport': alias.asname if alias.asname else None } self.imports.append(import_info) self.generic_visit(node) def visit_ImportFrom(self, node: ast.ImportFrom): """Visit from...import statement""" module = node.module or '' import_info = { 'importedNames': [alias.asname or alias.name for alias in node.names], 'moduleSpecifier': module, 'defaultImport': None, 'namespaceImport': None } self.imports.append(import_info) self.generic_visit(node) def analyze(self) -> Dict[str, Any]: """Perform analysis and return results""" try: tree = ast.parse(self.source_code, filename=self.file_path) self.visit(tree) # Calculate documentation coverage total_symbols = len(self.classes) + len(self.functions) documented_symbols = 0 for cls in self.classes: if cls['documentation']: documented_symbols += 1 for method in cls['methods']: total_symbols += 1 if method['documentation']: documented_symbols += 1 for constructor in cls['constructors']: total_symbols += 1 if constructor['documentation']: documented_symbols += 1 for func in self.functions: if func['documentation']: documented_symbols += 1 coverage = (documented_symbols / total_symbols * 100) if total_symbols > 0 else 0 return { 'path': self.file_path, 'classes': self.classes, 'functions': self.functions, 'interfaces': [], # Python doesn't have interfaces (use ABC) 'enums': [], # Would need separate handling 'typeAliases': [], # Would need separate handling 'imports': self.imports, 'exports': [], # Python doesn't have explicit exports 'documentation': { 'hasDocumentation': documented_symbols > 0, 'documentedSymbols': documented_symbols, 'totalSymbols': total_symbols, 'coverage': coverage } } except SyntaxError as e: return { 'error': f'Syntax error: {str(e)}', 'path': self.file_path } except Exception as e: return { 'error': f'Analysis error: {str(e)}', 'path': self.file_path } def analyze_file(file_path: str) -> Dict[str, Any]: """Analyze a single Python file""" try: with open(file_path, 'r', encoding='utf-8') as f: source_code = f.read() analyzer = PythonAnalyzer(source_code, file_path) return analyzer.analyze() except FileNotFoundError: return {'error': f'File not found: {file_path}', 'path': file_path} except Exception as e: return {'error': f'Error reading file: {str(e)}', 'path': file_path} if __name__ == '__main__': if len(sys.argv) < 2: print(json.dumps({'error': 'No file path provided'})) sys.exit(1) file_path = sys.argv[1] result = analyze_file(file_path) print(json.dumps(result, indent=2))

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/LiL-Loco/documentation-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server