Calc/app/type_registry.py

221 lines
8.8 KiB
Python

"""
Sistema de auto-descubrimiento y registro de clases personalizadas
"""
import os
import importlib.util
import inspect
from pathlib import Path
from typing import List, Tuple, Dict, Any, Type, Optional
import logging
# Configurar logging para debugging
logger = logging.getLogger(__name__)
class TypeRegistry:
"""
Sistema centralizado para el registro automático de clases personalizadas
"""
def __init__(self, types_directory: str = "custom_types"):
self.types_directory = Path(types_directory)
self.registered_classes: Dict[str, Type] = {}
self.bracket_classes: set = set()
self.base_context: Dict[str, Any] = {}
self.helper_functions: List[callable] = []
self.tokenization_patterns: List[Dict] = [] # NUEVO: patrones de tokenización
def discover_and_register_all(self) -> Dict[str, Any]:
"""
Descubre y registra todas las clases en el directorio de tipos
Returns:
Dict con toda la información registrada
"""
if not self.types_directory.exists():
logger.warning(f"Directorio de tipos no encontrado: {self.types_directory}")
return self._get_registry_info()
# Limpiar registros previos
self._clear_registries()
# Escanear archivos *_type.py
type_files = list(self.types_directory.glob("*_type.py"))
for type_file in type_files:
try:
self._process_type_file(type_file)
except Exception as e:
logger.error(f"Error procesando {type_file}: {e}")
continue
logger.info(f"Registro completado: {len(self.registered_classes)} clases, {len(self.tokenization_patterns)} patrones")
return self._get_registry_info()
def _clear_registries(self):
"""Limpia todos los registros"""
self.registered_classes.clear()
self.bracket_classes.clear()
self.base_context.clear()
self.helper_functions.clear()
self.tokenization_patterns.clear() # NUEVO
def _process_type_file(self, type_file: Path):
"""Procesa un archivo de tipo individual"""
module_name = type_file.stem
# Importar dinámicamente el módulo
spec = importlib.util.spec_from_file_location(module_name, type_file)
if not spec or not spec.loader:
logger.error(f"No se pudo cargar spec para {type_file}")
return
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
# Buscar función de registro
if hasattr(module, 'register_classes_in_module'):
class_info_list = module.register_classes_in_module()
self._register_classes_from_info(class_info_list, module_name)
else:
# Fallback: buscar clases automáticamente
logger.warning(f"{type_file} no tiene register_classes_in_module(), usando auto-detección")
self._auto_detect_classes(module, module_name)
def _register_classes_from_info(self, class_info_list: List[Tuple], module_name: str):
"""
Registra clases basándose en la información proporcionada
class_info_list: Lista de tuplas (nombre_publico, clase_objeto, categoria, [opciones])
"""
for class_info in class_info_list:
try:
if len(class_info) >= 3:
name, class_obj, category = class_info[:3]
options = class_info[3] if len(class_info) > 3 else {}
else:
logger.error(f"Formato incorrecto en {module_name}: {class_info}")
continue
self._register_single_class(name, class_obj, category, options, module_name)
except Exception as e:
logger.error(f"Error registrando clase en {module_name}: {e}")
def _register_single_class(self, name: str, class_obj: Type, category: str,
options: Dict, module_name: str):
"""Registra una clase individual"""
# Registro básico
self.registered_classes[name] = class_obj
# Añadir al contexto base (siempre)
self.base_context[name] = class_obj
# Añadir versión en minúsculas si se especifica
if options.get('add_lowercase', True):
self.base_context[name.lower()] = class_obj
# Registrar en bracket_classes si es apropiado
if category in ['ClassBase', 'SympyClassBase'] or options.get('supports_brackets', False):
self.bracket_classes.add(name)
# NUEVA FUNCIONALIDAD: También agregar versión en minúscula a bracket_classes
if options.get('add_lowercase', True):
self.bracket_classes.add(name.lower())
# Registrar función Helper si existe
if hasattr(class_obj, 'Helper') and callable(class_obj.Helper):
self.helper_functions.append(class_obj.Helper)
# NUEVO: Registrar patrones de tokenización si existen
if hasattr(class_obj, 'get_tokenization_patterns') and callable(class_obj.get_tokenization_patterns):
try:
patterns = class_obj.get_tokenization_patterns()
if patterns:
self.tokenization_patterns.extend(patterns)
logger.debug(f"Patrones de tokenización añadidos desde {name}: {len(patterns)}")
except Exception as e:
logger.warning(f"Error obteniendo patrones de tokenización de {name}: {e}")
logger.debug(f"Registrada: {name} ({category}) desde {module_name}")
def _auto_detect_classes(self, module, module_name: str):
"""Auto-detección de clases cuando no hay función de registro"""
for name, obj in inspect.getmembers(module, inspect.isclass):
if name.startswith('Class_') or name.endswith('Mask'):
# Detectar categoría
category = "SympyClassBase" if hasattr(obj, '_sympystr') else "ClassBase"
# Usar nombre sin prefijo Class_
public_name = name.replace('Class_', '') if name.startswith('Class_') else name
self._register_single_class(public_name, obj, category, {}, module_name)
def _get_registry_info(self) -> Dict[str, Any]:
"""Retorna información completa del registro"""
return {
'base_context': self.base_context.copy(),
'bracket_classes': self.bracket_classes.copy(),
'helper_functions': self.helper_functions.copy(),
'registered_classes': self.registered_classes.copy(),
'tokenization_patterns': self.tokenization_patterns.copy(), # NUEVO
'class_count': len(self.registered_classes),
'bracket_count': len(self.bracket_classes),
'pattern_count': len(self.tokenization_patterns) # NUEVO
}
def get_base_context(self) -> Dict[str, Any]:
"""Retorna contexto base para el motor de evaluación"""
return self.base_context.copy()
def get_bracket_classes(self) -> set:
"""Retorna set de clases que soportan sintaxis con corchetes"""
return self.bracket_classes.copy()
def get_helper_functions(self) -> List[callable]:
"""Retorna lista de funciones Helper"""
return self.helper_functions.copy()
def get_tokenization_patterns(self) -> List[Dict]:
"""NUEVO: Retorna lista de patrones de tokenización"""
return self.tokenization_patterns.copy()
# Instancia global del registro
_global_registry = TypeRegistry()
def discover_and_register_types(types_directory: str = "custom_types") -> Dict[str, Any]:
"""
Función principal para descubrir y registrar todos los tipos
Args:
types_directory: Directorio donde están los archivos *_type.py
Returns:
Dict con información del registro
"""
global _global_registry
_global_registry = TypeRegistry(types_directory)
return _global_registry.discover_and_register_all()
def get_registered_base_context() -> Dict[str, Any]:
"""Obtiene el contexto base con todas las clases registradas"""
return _global_registry.get_base_context()
def get_registered_bracket_classes() -> set:
"""Obtiene las clases que soportan sintaxis con corchetes"""
return _global_registry.get_bracket_classes()
def get_registered_helper_functions() -> List[callable]:
"""Obtiene las funciones Helper registradas"""
return _global_registry.get_helper_functions()
def get_registered_tokenization_patterns() -> List[Dict]:
"""NUEVO: Obtiene los patrones de tokenización registrados"""
return _global_registry.get_tokenization_patterns()