Files
rpa_vision_v3/validate_circular_imports.py
Dom a27b74cf22 v1.0 - Version stable: multi-PC, détection UI-DETR-1, 3 modes exécution
- Frontend v4 accessible sur réseau local (192.168.1.40)
- Ports ouverts: 3002 (frontend), 5001 (backend), 5004 (dashboard)
- Ollama GPU fonctionnel
- Self-healing interactif
- Dashboard confiance

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-29 11:23:51 +01:00

230 lines
7.8 KiB
Python
Executable File

#!/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()