- 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>
230 lines
7.8 KiB
Python
Executable File
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() |