#!/usr/bin/env python3 """ Script de validation des imports circulaires. Vérifie qu'aucun import circulaire n'existe dans le système. Auteur: Dom, Alice Kiro Date: 20 décembre 2024 """ import sys import ast import os from pathlib import Path from typing import Dict, Set, List, Tuple from collections import defaultdict, deque class ImportAnalyzer(ast.NodeVisitor): """Analyseur d'imports pour un fichier Python""" def __init__(self, module_path: str): self.module_path = module_path self.imports: Set[str] = set() self.from_imports: Set[str] = set() def visit_Import(self, node: ast.Import): """Visite les imports directs (import module)""" for alias in node.names: self.imports.add(alias.name) self.generic_visit(node) def visit_ImportFrom(self, node: ast.ImportFrom): """Visite les imports from (from module import ...)""" if node.module: self.from_imports.add(node.module) self.generic_visit(node) class CircularImportDetector: """Détecteur d'imports circulaires""" def __init__(self, root_path: Path): self.root_path = root_path self.module_graph: Dict[str, Set[str]] = defaultdict(set) self.module_paths: Dict[str, Path] = {} def _get_module_name(self, file_path: Path) -> str: """Convertit un chemin de fichier en nom de module""" relative_path = file_path.relative_to(self.root_path) # Enlever l'extension .py if relative_path.suffix == '.py': relative_path = relative_path.with_suffix('') # Convertir les séparateurs de chemin en points module_name = str(relative_path).replace(os.sep, '.') # Gérer __init__.py if module_name.endswith('.__init__'): module_name = module_name[:-9] # Enlever .__init__ return module_name def _normalize_import(self, import_name: str, current_module: str) -> str: """Normalise un nom d'import (gère les imports relatifs)""" if import_name.startswith('.'): # Import relatif parts = current_module.split('.') level = 0 for char in import_name: if char == '.': level += 1 else: break if level > len(parts): return import_name # Import invalide, on le garde tel quel base_parts = parts[:-level] if level > 0 else parts relative_part = import_name[level:] if relative_part: return '.'.join(base_parts + [relative_part]) else: return '.'.join(base_parts) return import_name def analyze_file(self, file_path: Path) -> None: """Analyse un fichier Python pour extraire ses imports""" try: with open(file_path, 'r', encoding='utf-8') as f: content = f.read() tree = ast.parse(content) analyzer = ImportAnalyzer(str(file_path)) analyzer.visit(tree) module_name = self._get_module_name(file_path) self.module_paths[module_name] = file_path # Ajouter tous les imports au graphe all_imports = analyzer.imports | analyzer.from_imports for import_name in all_imports: normalized_import = self._normalize_import(import_name, module_name) # Ne considérer que les imports internes (commençant par 'core') if normalized_import.startswith('core'): self.module_graph[module_name].add(normalized_import) except (SyntaxError, UnicodeDecodeError) as e: print(f"Erreur lors de l'analyse de {file_path}: {e}") def find_cycles(self) -> List[List[str]]: """Trouve tous les cycles dans le graphe d'imports""" cycles = [] visited = set() rec_stack = set() path = [] def dfs(node: str) -> bool: """DFS pour détecter les cycles""" if node in rec_stack: # Cycle détecté, extraire le cycle cycle_start = path.index(node) cycle = path[cycle_start:] + [node] cycles.append(cycle) return True if node in visited: return False visited.add(node) rec_stack.add(node) path.append(node) for neighbor in self.module_graph.get(node, set()): if dfs(neighbor): return True rec_stack.remove(node) path.pop() return False # Parcourir tous les nœuds for node in self.module_graph: if node not in visited: dfs(node) return cycles def analyze_directory(self, directory: Path) -> None: """Analyse tous les fichiers Python dans un répertoire""" for py_file in directory.rglob('*.py'): # Ignorer les fichiers de test et les répertoires spéciaux if any(part.startswith('.') for part in py_file.parts): continue if 'test' in str(py_file).lower(): continue if '__pycache__' in str(py_file): continue self.analyze_file(py_file) def main(): """Fonction principale""" print("🔍 Validation des imports circulaires...") # Chemin racine du projet root_path = Path(__file__).parent core_path = root_path / 'core' if not core_path.exists(): print("❌ Répertoire 'core' non trouvé") sys.exit(1) # Analyser tous les fichiers detector = CircularImportDetector(root_path) detector.analyze_directory(core_path) print(f"📊 Analysé {len(detector.module_paths)} modules") print(f"📊 Trouvé {sum(len(deps) for deps in detector.module_graph.values())} dépendances") # Détecter les cycles cycles = detector.find_cycles() if cycles: print(f"\n❌ {len(cycles)} import(s) circulaire(s) détecté(s):") for i, cycle in enumerate(cycles, 1): print(f"\n🔄 Cycle {i}:") for j, module in enumerate(cycle): if j < len(cycle) - 1: print(f" {module} → {cycle[j + 1]}") else: print(f" {module}") print("\n💡 Solutions suggérées:") print(" 1. Utiliser TYPE_CHECKING pour les imports de type") print(" 2. Déplacer les imports dans les fonctions (lazy loading)") print(" 3. Créer des interfaces abstraites") print(" 4. Refactorer pour réduire les dépendances") sys.exit(1) else: print("\n✅ Aucun import circulaire détecté!") print("🎉 Le système respecte les bonnes pratiques d'imports") # Statistiques additionnelles print(f"\n📈 Statistiques:") print(f" • Modules analysés: {len(detector.module_paths)}") print(f" • Dépendances totales: {sum(len(deps) for deps in detector.module_graph.values())}") # Modules les plus dépendants most_dependent = sorted( detector.module_graph.items(), key=lambda x: len(x[1]), reverse=True )[:5] if most_dependent: print(f"\n🔗 Modules avec le plus de dépendances:") for module, deps in most_dependent: print(f" • {module}: {len(deps)} dépendances") if __name__ == "__main__": main()