dependency_analyzer.py•7.49 kB
"""
Dependency Analyzer for building code relationship graphs
Tracks imports, function calls, and class inheritance
"""
import ast
import json
from pathlib import Path
from typing import Dict, List, Set, Tuple, Optional, Any
from collections import defaultdict
class DependencyAnalyzer:
"""Analyzes code dependencies and builds relationship graphs"""
def __init__(self, project_root: Path):
self.project_root = Path(project_root)
self.import_graph = defaultdict(set) # file -> imported modules
self.call_graph = defaultdict(set) # function -> called functions
self.inheritance_tree = defaultdict(set) # class -> parent classes
self.reverse_call_graph = defaultdict(set) # function -> callers
self.reverse_inheritance = defaultdict(set) # class -> child classes
def analyze_file(self, file_path: Path) -> Dict[str, Any]:
"""Analyze a single Python file for dependencies"""
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = f.read()
tree = ast.parse(content)
relative_path = file_path.relative_to(self.project_root) if file_path.is_absolute() else file_path
# Extract imports
imports = self._extract_imports(tree)
# Analyze each symbol
symbols_info = {}
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
symbol_name = node.name
# Get function calls within this symbol
calls = self._extract_calls(node)
# Get inheritance for classes
inherits_from = None
if isinstance(node, ast.ClassDef):
inherits_from = self._extract_inheritance(node)
symbols_info[symbol_name] = {
'imports': imports,
'calls': list(calls),
'inherits_from': inherits_from
}
return symbols_info
except Exception as e:
return {}
def _extract_imports(self, tree: ast.AST) -> List[str]:
"""Extract all import statements from AST"""
imports = []
for node in ast.walk(tree):
if isinstance(node, ast.Import):
for alias in node.names:
imports.append(alias.name)
elif isinstance(node, ast.ImportFrom):
module = node.module or ''
for alias in node.names:
if alias.name == '*':
imports.append(f"{module}.*")
else:
imports.append(f"{module}.{alias.name}")
return imports
def _extract_calls(self, node: ast.AST) -> Set[str]:
"""Extract function calls from a node"""
calls = set()
for child in ast.walk(node):
if isinstance(child, ast.Call):
call_name = self._get_call_name(child)
if call_name:
calls.add(call_name)
return calls
def _get_call_name(self, call_node: ast.Call) -> Optional[str]:
"""Get the name of a function being called"""
if isinstance(call_node.func, ast.Name):
return call_node.func.id
elif isinstance(call_node.func, ast.Attribute):
# Handle method calls like obj.method()
parts = []
node = call_node.func
while isinstance(node, ast.Attribute):
parts.append(node.attr)
node = node.value
if isinstance(node, ast.Name):
parts.append(node.id)
return '.'.join(reversed(parts)) if parts else None
return None
def _extract_inheritance(self, class_node: ast.ClassDef) -> Optional[str]:
"""Extract parent classes for a class definition"""
parents = []
for base in class_node.bases:
if isinstance(base, ast.Name):
parents.append(base.id)
elif isinstance(base, ast.Attribute):
# Handle module.Class inheritance
parent_name = []
node = base
while isinstance(node, ast.Attribute):
parent_name.append(node.attr)
node = node.value
if isinstance(node, ast.Name):
parent_name.append(node.id)
parents.append('.'.join(reversed(parent_name)))
return ', '.join(parents) if parents else None
def build_project_graph(self) -> Dict[str, Any]:
"""Build complete dependency graph for the project"""
project_graph = {
'imports': {},
'call_graph': {},
'inheritance': {},
'statistics': {}
}
# Find all Python files
python_files = list(self.project_root.rglob('*.py'))
for file_path in python_files:
# Skip common directories to ignore
if any(part in file_path.parts for part in ['.git', '__pycache__', 'venv', '.venv']):
continue
file_info = self.analyze_file(file_path)
relative_path = str(file_path.relative_to(self.project_root))
for symbol_name, info in file_info.items():
key = f"{relative_path}::{symbol_name}"
# Track imports
if info['imports']:
project_graph['imports'][key] = info['imports']
# Track calls
if info['calls']:
project_graph['call_graph'][key] = info['calls']
# Update reverse call graph
for called_func in info['calls']:
if called_func not in project_graph['call_graph']:
project_graph['call_graph'][called_func] = []
# Track inheritance
if info.get('inherits_from'):
project_graph['inheritance'][key] = info['inherits_from']
# Calculate statistics
project_graph['statistics'] = {
'total_files': len(python_files),
'total_imports': len(project_graph['imports']),
'total_calls': sum(len(calls) for calls in project_graph['call_graph'].values()),
'total_inheritance': len(project_graph['inheritance'])
}
return project_graph
def find_dependencies(self, symbol_name: str) -> Dict[str, List[str]]:
"""Find all dependencies for a given symbol"""
dependencies = {
'imports': [],
'calls': [],
'called_by': [],
'inherits_from': None,
'inherited_by': []
}
# This would be populated from the built graph
# For now, returning structure
return dependencies
def to_json(self, obj: Any) -> str:
"""Convert dependency information to JSON"""
if isinstance(obj, set):
obj = list(obj)
return json.dumps(obj, default=str)