Primera version con auto-descubrimiento de tipos

This commit is contained in:
Miguel 2025-06-02 21:39:43 +02:00
parent 03964d2ff5
commit f16c878a58
9 changed files with 1039 additions and 283 deletions

View File

@ -1,5 +1,5 @@
""" """
Clase híbrida para números binarios Clase híbrida para números binarios - ADAPTADA AL NUEVO SISTEMA
""" """
from sympy_Base import SympyClassBase from sympy_Base import SympyClassBase
import re import re
@ -53,3 +53,18 @@ class Class_Bin(SympyClassBase):
def toDecimal(self): def toDecimal(self):
"""Convierte a decimal""" """Convierte a decimal"""
return self._value return self._value
# ========== FUNCIÓN DE REGISTRO - NUEVA ==========
def register_classes_in_module():
"""
Devuelve una lista de clases definidas en este módulo para ser registradas.
"""
return [
("Bin", Class_Bin, "SympyClassBase", {
"add_lowercase": True,
"supports_brackets": True,
"description": "Números binarios"
}),
]

View File

@ -1,5 +1,6 @@
""" """
Clase híbrida para caracteres Clase híbrida para caracteres - ADAPTADA AL NUEVO SISTEMA
Archivo: custom_types/chr_type.py
""" """
from sympy_Base import SympyClassBase from sympy_Base import SympyClassBase
import re import re
@ -64,3 +65,22 @@ class Class_Chr(SympyClassBase):
def toBin(self): def toBin(self):
"""Convierte a binario""" """Convierte a binario"""
return f"0b{self._value:08b}" return f"0b{self._value:08b}"
def register_classes_in_module():
"""
Devuelve una lista de clases definidas en este módulo para ser registradas.
Returns:
List[Tuple]: Lista de tuplas (nombre_publico, clase_objeto, categoria, opciones)
"""
return [
# (nombre_publico, clase_objeto, categoria, opciones)
("Chr", Class_Chr, "SympyClassBase", {
"add_lowercase": True, # Añadir 'chr' al contexto
"supports_brackets": True, # Soporta sintaxis Chr[...]
"description": "Caracteres y códigos ASCII"
}),
]

View File

@ -1,5 +1,5 @@
""" """
Clase híbrida para números decimales Clase híbrida para números decimales - ADAPTADA AL NUEVO SISTEMA
""" """
from sympy_Base import SympyClassBase from sympy_Base import SympyClassBase
import re import re
@ -54,3 +54,17 @@ class Class_Dec(SympyClassBase):
def toBin(self): def toBin(self):
"""Convierte a binario""" """Convierte a binario"""
return f"0b{self._value:08b}" return f"0b{self._value:08b}"
def register_classes_in_module():
"""
Devuelve una lista de clases definidas en este módulo para ser registradas.
"""
return [
("Dec", Class_Dec, "SympyClassBase", {
"add_lowercase": True,
"supports_brackets": True,
"description": "Números decimales"
}),
]

View File

@ -1,5 +1,5 @@
""" """
Clase híbrida para números hexadecimales Clase híbrida para números hexadecimales - ADAPTADA AL NUEVO SISTEMA
""" """
from sympy_Base import SympyClassBase from sympy_Base import SympyClassBase
import re import re
@ -53,3 +53,17 @@ class Class_Hex(SympyClassBase):
def toDecimal(self): def toDecimal(self):
"""Convierte a decimal""" """Convierte a decimal"""
return self._value return self._value
def register_classes_in_module():
"""
Devuelve una lista de clases definidas en este módulo para ser registradas.
"""
return [
("Hex", Class_Hex, "SympyClassBase", {
"add_lowercase": True,
"supports_brackets": True,
"description": "Números hexadecimales"
}),
]

View File

@ -876,3 +876,41 @@ class Class_IP4(SympyClassBase):
"public_count": len(ip_list) - private_count, "public_count": len(ip_list) - private_count,
"common_network": Class_IP4.find_common_network(ip_list) "common_network": Class_IP4.find_common_network(ip_list)
} }
def register_classes_in_module():
"""
Devuelve una lista de clases definidas en este módulo para ser registradas.
"""
return [
("IP4", Class_IP4, "SympyClassBase", {
"add_lowercase": True,
"supports_brackets": True,
"description": "Direcciones IPv4 con máscara"
}),
("IP4Mask", IP4Mask, "ClassBase", {
"add_lowercase": True,
"supports_brackets": True,
"description": "Máscaras de red IPv4"
}),
]
# ========== INFORMACIÓN DEL MÓDULO ==========
def get_module_info():
"""Información adicional sobre este módulo"""
return {
"name": "IPv4 Network Types",
"description": "Clases para manejo de direcciones IP y máscaras de red",
"version": "1.0",
"depends_on": ["class_base", "sympy_Base"],
"examples": [
"IP4[192.168.1.1/24]",
"IP4[10.0.0.1;255.255.255.0]",
"IP4[192.168.1.1/24].NetworkAddress()",
"IP4Mask[24]",
"IP4Mask[255.255.255.0]"
]
}

View File

@ -1,44 +1,32 @@
""" """
Calculadora MAV CAS Híbrida - Aplicación principal Calculadora MAV CAS Híbrida - Aplicación principal
VERSIÓN ADAPTADA AL NUEVO SISTEMA DE TIPOS
""" """
import tkinter as tk import tkinter as tk
from tkinter import scrolledtext, messagebox, Menu, filedialog from tkinter import scrolledtext, messagebox, Menu, filedialog
import tkinter.font as tkFont import tkinter.font as tkFont
import json import json
import os import os
from pathlib import Path # Added for robust path handling from pathlib import Path
import threading import threading
from typing import List, Dict, Any, Optional from typing import List, Dict, Any, Optional
import re import re
# Importar componentes del CAS híbrido # ========== IMPORTS ADAPTADOS AL NUEVO SISTEMA ==========
# Importar componentes del CAS híbrido con nuevo sistema de tipos
from main_evaluation import HybridEvaluationEngine, EvaluationResult from main_evaluation import HybridEvaluationEngine, EvaluationResult
from sympy_Base import SympyClassBase from tl_popup import InteractiveResultManager, PlotResult
from tl_popup import InteractiveResultManager, PlotResult # <--- Asegurar que PlotResult se importa from type_registry import get_registered_helper_functions, get_registered_base_context
from ip4_type import Class_IP4
from hex_type import Class_Hex
from bin_type import Class_Bin
from dec_type import Class_Dec
from chr_type import Class_Chr
import sympy import sympy
from sympy_helper import SympyTools as SympyHelper from sympy_helper import SympyTools as SympyHelper
class HybridCalculatorApp: class HybridCalculatorApp:
"""Aplicación principal del CAS híbrido""" """Aplicación principal del CAS híbrido - ADAPTADA AL NUEVO SISTEMA"""
SETTINGS_FILE = "hybrid_calc_settings.json" SETTINGS_FILE = "hybrid_calc_settings.json"
HISTORY_FILE = "hybrid_calc_history.txt" HISTORY_FILE = "hybrid_calc_history.txt"
HELPERS = [
Class_IP4.Helper,
Class_Hex.Helper,
Class_Bin.Helper,
Class_Dec.Helper,
Class_Chr.Helper,
SympyHelper.Helper,
]
def __init__(self, root: tk.Tk): def __init__(self, root: tk.Tk):
self.root = root self.root = root
self.root.title("Calculadora MAV - CAS Híbrido") self.root.title("Calculadora MAV - CAS Híbrido")
@ -51,10 +39,13 @@ class HybridCalculatorApp:
# Configurar ícono # Configurar ícono
self._setup_icon() self._setup_icon()
# Componentes principales # ========== COMPONENTES PRINCIPALES CON NUEVO SISTEMA ==========
self.engine = HybridEvaluationEngine() self.engine = HybridEvaluationEngine(auto_discover_types=True)
self.interactive_manager = None # Se inicializa después de crear widgets self.interactive_manager = None # Se inicializa después de crear widgets
# ========== HELPERS DINÁMICOS DEL REGISTRO ==========
self._setup_dynamic_helpers()
# Estado de la aplicación # Estado de la aplicación
self._debounce_job = None self._debounce_job = None
self._syncing_yview = False self._syncing_yview = False
@ -69,25 +60,85 @@ class HybridCalculatorApp:
# Configurar eventos de cierre # Configurar eventos de cierre
self.root.protocol("WM_DELETE_WINDOW", self.on_close) self.root.protocol("WM_DELETE_WINDOW", self.on_close)
def _setup_dynamic_helpers(self):
"""Configura helpers dinámicamente desde el registro de tipos"""
try:
# Obtener helpers registrados dinámicamente
self.HELPERS = get_registered_helper_functions()
# Añadir SympyHelper al final
self.HELPERS.append(SympyHelper.Helper)
print(f"🆘 Helpers dinámicos cargados: {len(self.HELPERS)}")
except Exception as e:
print(f"⚠️ Error cargando helpers dinámicos: {e}")
# Fallback a helpers básicos
self.HELPERS = [SympyHelper.Helper]
def reload_types(self):
"""Recarga el sistema de tipos (útil para desarrollo)"""
try:
print("🔄 Recargando sistema de tipos...")
# Recargar engine
self.engine.reload_types()
# Recargar helpers
self._setup_dynamic_helpers()
# Re-evaluar contenido actual
self._evaluate_and_update()
print("✅ Sistema de tipos recargado")
except Exception as e:
print(f"❌ Error recargando tipos: {e}")
messagebox.showerror("Error", f"Error recargando tipos:\n{e}")
def show_types_info(self):
"""Muestra información sobre tipos disponibles"""
try:
types_info = self.engine.get_available_types()
info_text = f"""INFORMACIÓN DEL SISTEMA DE TIPOS
Clases registradas: {len(types_info.get('registered_classes', {}))}
Clases con sintaxis de corchetes: {len(types_info.get('bracket_classes', []))}
Entradas en contexto: {types_info.get('total_context_entries', 0)}
Helper functions: {types_info.get('helper_functions_count', 0)}
CLASES DISPONIBLES:
"""
for name, cls in types_info.get('registered_classes', {}).items():
info_text += f"{name}: {cls.__name__}\n"
info_text += f"\nCLASES CON SINTAXIS DE CORCHETES:\n"
for name in types_info.get('bracket_classes', []):
info_text += f"{name}[...]\n"
# Mostrar en ventana
self._show_help_window("Información de Tipos", info_text)
except Exception as e:
messagebox.showerror("Error", f"Error obteniendo información de tipos:\n{e}")
def _setup_icon(self): def _setup_icon(self):
"""Configura el ícono de la aplicación""" """Configura el ícono de la aplicación"""
try: try:
# Construct path relative to this script file (main_calc_app.py)
script_dir = Path(__file__).resolve().parent script_dir = Path(__file__).resolve().parent
icon_path = script_dir / "icon.png" icon_path = script_dir / "icon.png"
if not icon_path.is_file(): if not icon_path.is_file():
print(f"Advertencia: Archivo de ícono no encontrado en '{icon_path}'.") print(f"Advertencia: Archivo de ícono no encontrado en '{icon_path}'.")
# Optionally, set a default Tk icon or simply return
return return
self.app_icon = tk.PhotoImage(file=str(icon_path)) self.app_icon = tk.PhotoImage(file=str(icon_path))
self.root.iconphoto(True, self.app_icon) self.root.iconphoto(True, self.app_icon)
except tk.TclError as e: except tk.TclError as e:
# Provide more specific error, including the path and Tkinter's error message
print(f"Advertencia: No se pudo cargar el ícono desde '{icon_path}'. Error de Tkinter: {e}") print(f"Advertencia: No se pudo cargar el ícono desde '{icon_path}'. Error de Tkinter: {e}")
except Exception as e: except Exception as e:
# Catch other potential errors during icon loading
print(f"Advertencia: Ocurrió un error inesperado al cargar el ícono desde '{icon_path}': {e}") print(f"Advertencia: Ocurrió un error inesperado al cargar el ícono desde '{icon_path}': {e}")
def _load_settings(self) -> Dict[str, Any]: def _load_settings(self) -> Dict[str, Any]:
@ -222,7 +273,15 @@ class HybridCalculatorApp:
cas_menu.add_separator() cas_menu.add_separator()
cas_menu.add_command(label="Resolver sistema", command=self.solve_system) cas_menu.add_command(label="Resolver sistema", command=self.solve_system)
# Menú Ayuda # ========== MENÚ TIPOS (NUEVO) ==========
types_menu = Menu(menubar, tearoff=0)
menubar.add_cascade(label="Tipos", menu=types_menu)
types_menu.add_command(label="Información de tipos", command=self.show_types_info)
types_menu.add_command(label="Recargar tipos", command=self.reload_types)
types_menu.add_separator()
types_menu.add_command(label="Sintaxis de tipos", command=self.show_types_syntax)
# Menú Ayuda (actualizado)
help_menu = Menu(menubar, tearoff=0) help_menu = Menu(menubar, tearoff=0)
menubar.add_cascade(label="Ayuda", menu=help_menu) menubar.add_cascade(label="Ayuda", menu=help_menu)
help_menu.add_command(label="Guía rápida", command=self.show_quick_guide) help_menu.add_command(label="Guía rápida", command=self.show_quick_guide)
@ -268,16 +327,16 @@ class HybridCalculatorApp:
self.output_text.tag_configure("equation", foreground="#c792ea") self.output_text.tag_configure("equation", foreground="#c792ea")
self.output_text.tag_configure("info", foreground="#ffcb6b") self.output_text.tag_configure("info", foreground="#ffcb6b")
self.output_text.tag_configure("comment", foreground="#546e7a") self.output_text.tag_configure("comment", foreground="#546e7a")
self.output_text.tag_configure("class_hint", foreground="#888888") # Gris para la pista de clase self.output_text.tag_configure("class_hint", foreground="#888888")
self.output_text.tag_configure("type_hint", foreground="#6a6a6a") self.output_text.tag_configure("type_hint", foreground="#6a6a6a")
# Tags para tipos especializados # Tags para tipos especializados (genéricos para cualquier tipo)
self.output_text.tag_configure("custom_type", foreground="#f9a825")
self.output_text.tag_configure("hex", foreground="#f9a825") self.output_text.tag_configure("hex", foreground="#f9a825")
self.output_text.tag_configure("bin", foreground="#4fc3f7") self.output_text.tag_configure("bin", foreground="#4fc3f7")
self.output_text.tag_configure("ip", foreground="#fff176") self.output_text.tag_configure("ip", foreground="#fff176")
self.output_text.tag_configure("date", foreground="#ff8a80") self.output_text.tag_configure("date", foreground="#ff8a80")
self.output_text.tag_configure("chr_type", foreground="#80cbc4") self.output_text.tag_configure("chr_type", foreground="#80cbc4")
# Agregar tag para ayuda contextual
self.output_text.tag_configure("helper", foreground="#ffd700", font=("Consolas", 11, "italic")) self.output_text.tag_configure("helper", foreground="#ffd700", font=("Consolas", 11, "italic"))
def on_key_release(self, event=None): def on_key_release(self, event=None):
@ -285,7 +344,7 @@ class HybridCalculatorApp:
if self._debounce_job: if self._debounce_job:
self.root.after_cancel(self._debounce_job) self.root.after_cancel(self._debounce_job)
# Autocompletado con punto # Autocompletado con punto (usando contexto dinámico)
if event and event.char == '.' and self.input_text.focus_get() == self.input_text: if event and event.char == '.' and self.input_text.focus_get() == self.input_text:
self._handle_dot_autocomplete() self._handle_dot_autocomplete()
@ -293,7 +352,7 @@ class HybridCalculatorApp:
self._debounce_job = self.root.after(300, self._evaluate_and_update) self._debounce_job = self.root.after(300, self._evaluate_and_update)
def _handle_dot_autocomplete(self): def _handle_dot_autocomplete(self):
"""Maneja el autocompletado cuando se escribe un punto.""" """Maneja el autocompletado cuando se escribe un punto - VERSIÓN DINÁMICA"""
self._close_autocomplete_popup() self._close_autocomplete_popup()
cursor_index_str = self.input_text.index(tk.INSERT) cursor_index_str = self.input_text.index(tk.INSERT)
line_num_str, char_num_str = cursor_index_str.split('.') line_num_str, char_num_str = cursor_index_str.split('.')
@ -309,39 +368,40 @@ class HybridCalculatorApp:
stripped_text_before_dot = text_on_line_up_to_dot.strip() stripped_text_before_dot = text_on_line_up_to_dot.strip()
# 1. Determinar si es un popup GLOBAL # 1. Determinar si es un popup GLOBAL (usando contexto dinámico)
if not stripped_text_before_dot: if not stripped_text_before_dot:
print("DEBUG: Dot on empty line or after spaces. Offering global suggestions.") print("DEBUG: Dot on empty line or after spaces. Offering global suggestions.")
suggestions = [] suggestions = []
# MODIFIED: Get suggestions from HybridEvaluationEngine's base_context # ========== USAR CONTEXTO DINÁMICO DEL REGISTRO ==========
if hasattr(self.engine, 'base_context') and isinstance(self.engine.base_context, dict): try:
for name, class_or_func in self.engine.base_context.items(): dynamic_context = get_registered_base_context()
# Solo queremos clases (tipos) y funciones para el autocompletado global principal.
# Evitamos alias en minúscula si la versión capitalizada ya está (heurística simple). for name, class_or_func in dynamic_context.items():
if name[0].isupper(): # Prioritize capitalized names for classes/main functions if name[0].isupper(): # Prioritizar nombres capitalizados
hint = f"Tipo o función: {name}" hint = f"Tipo o función: {name}"
if hasattr(class_or_func, '__doc__') and class_or_func.__doc__: if hasattr(class_or_func, '__doc__') and class_or_func.__doc__:
first_line_doc = class_or_func.__doc__.strip().split('\n')[0] first_line_doc = class_or_func.__doc__.strip().split('\n')[0]
hint = f"{name} - {first_line_doc}" hint = f"{name} - {first_line_doc}"
elif hasattr(class_or_func, 'Helper'): # Usar Helper si está disponible elif hasattr(class_or_func, 'Helper'):
# Para obtener un hint del Helper, necesitamos llamarlo.
# Algunas clases Helper esperan el nombre de la clase.
try: try:
helper_text = class_or_func.Helper(name) # Pasar el nombre de la clase helper_text = class_or_func.Helper(name)
if helper_text: if helper_text:
hint = helper_text.split('\n')[0] # Primera línea del helper hint = helper_text.split('\n')[0]
except Exception as e_helper: except Exception as e_helper:
print(f"DEBUG: Error calling Helper for {name}: {e_helper}") print(f"DEBUG: Error calling Helper for {name}: {e_helper}")
pass # Mantener el hint genérico pass
suggestions.append((name, hint)) suggestions.append((name, hint))
# Añadir funciones de SympyHelper (si no están ya en base_context de forma similar) except Exception as e:
# Considerar si SympyHelper.PopupFunctionList() devuelve cosas ya cubiertas. print(f"DEBUG: Error obteniendo contexto dinámico: {e}")
# Fallback básico
suggestions = [("sin", "Función seno"), ("cos", "Función coseno")]
# Añadir funciones de SympyHelper
try: try:
sympy_functions = SympyHelper.PopupFunctionList() sympy_functions = SympyHelper.PopupFunctionList()
if sympy_functions: if sympy_functions:
# Evitar duplicados si los nombres ya están de base_context
current_suggestion_names = {s[0] for s in suggestions} current_suggestion_names = {s[0] for s in suggestions}
for fname, fhint in sympy_functions: for fname, fhint in sympy_functions:
if fname not in current_suggestion_names: if fname not in current_suggestion_names:
@ -350,25 +410,19 @@ class HybridCalculatorApp:
print(f"DEBUG: Error calling SympyHelper.PopupFunctionList() for global: {e}") print(f"DEBUG: Error calling SympyHelper.PopupFunctionList() for global: {e}")
if suggestions: if suggestions:
# Ordenar alfabéticamente para consistencia
suggestions.sort(key=lambda x: x[0]) suggestions.sort(key=lambda x: x[0])
self._show_autocomplete_popup(suggestions, is_global_popup=True) self._show_autocomplete_popup(suggestions, is_global_popup=True)
return return
# 2. Es un popup de OBJETO. Extraer la expresión del objeto. # 2. Es un popup de OBJETO
obj_expr_str_candidate = "" obj_expr_str_candidate = ""
# Regex para `identificador_o_ClaseConCorchetes(.identificador_o_ClaseConCorchetes)*`
# Anclado al final de stripped_text_before_dot
obj_expr_regex = r"([a-zA-Z_][a-zA-Z0-9_]*(?:\[[^\]]*\])?(?:(?:\s*\.\s*[a-zA-Z_][a-zA-Z0-9_]*)(?:\[[^\]]*\])?)*)$" obj_expr_regex = r"([a-zA-Z_][a-zA-Z0-9_]*(?:\[[^\]]*\])?(?:(?:\s*\.\s*[a-zA-Z_][a-zA-Z0-9_]*)(?:\[[^\]]*\])?)*)$"
match = re.search(obj_expr_regex, stripped_text_before_dot) match = re.search(obj_expr_regex, stripped_text_before_dot)
if match: if match:
obj_expr_str_candidate = match.group(1).replace(" ", "") # Quitar espacios como en "obj . method" obj_expr_str_candidate = match.group(1).replace(" ", "")
else: else:
# Heurística: si el regex no coincide, tomar todo stripped_text_before_dot.
# Esto podría capturar (a+b) o mi_func()
obj_expr_str_candidate = stripped_text_before_dot obj_expr_str_candidate = stripped_text_before_dot
# Validación simple para evitar evaluar cosas que claramente no son objetos
if not obj_expr_str_candidate or \ if not obj_expr_str_candidate or \
not re.match(r"^[a-zA-Z_0-9\(\)\[\]\.\"\'\+\-\*\/ ]*$", obj_expr_str_candidate) or \ not re.match(r"^[a-zA-Z_0-9\(\)\[\]\.\"\'\+\-\*\/ ]*$", obj_expr_str_candidate) or \
obj_expr_str_candidate.endswith(("+", "-", "*", "/", "(", ",")): obj_expr_str_candidate.endswith(("+", "-", "*", "/", "(", ",")):
@ -378,7 +432,7 @@ class HybridCalculatorApp:
obj_expr_str = obj_expr_str_candidate obj_expr_str = obj_expr_str_candidate
print(f"DEBUG: Autocomplete for object. Extracted: '{obj_expr_str}' from: '{text_on_line_up_to_dot}'") print(f"DEBUG: Autocomplete for object. Extracted: '{obj_expr_str}' from: '{text_on_line_up_to_dot}'")
if not obj_expr_str: # Debería estar cubierto por el popup global, pero por si acaso. if not obj_expr_str:
print("DEBUG: Object expression is empty after extraction. No autocomplete.") print("DEBUG: Object expression is empty after extraction. No autocomplete.")
return return
@ -395,20 +449,18 @@ class HybridCalculatorApp:
print(f"DEBUG: Error calling SympyHelper.PopupFunctionList(): {e}") print(f"DEBUG: Error calling SympyHelper.PopupFunctionList(): {e}")
return return
# 4. Preprocesar con BracketParser para sintaxis Clase[arg] y metodo[] # 4. Preprocesar con BracketParser
# Es importante transformar obj_expr_str ANTES de pasarlo a eval(). if '[' in obj_expr_str:
if '[' in obj_expr_str: # Optimización: solo llamar si hay corchetes
original_for_debug = obj_expr_str original_for_debug = obj_expr_str
# self.engine.parser es una instancia de BracketParser
obj_expr_str = self.engine.parser._transform_brackets(obj_expr_str) obj_expr_str = self.engine.parser._transform_brackets(obj_expr_str)
if obj_expr_str != original_for_debug: if obj_expr_str != original_for_debug:
print(f"DEBUG: Preprocessed by BracketParser: '{original_for_debug}' -> '{obj_expr_str}'") print(f"DEBUG: Preprocessed by BracketParser: '{original_for_debug}' -> '{obj_expr_str}'")
# 5. Evaluar la expresión del objeto # 5. Evaluar la expresión del objeto (usando contexto dinámico)
eval_context = self.engine._get_full_context() if hasattr(self.engine, '_get_full_context') else {} eval_context = self.engine._get_full_context()
obj = None obj = None
try: try:
if not obj_expr_str.strip(): # Seguridad adicional if not obj_expr_str.strip():
print("DEBUG: Object expression became empty before eval. No action.") print("DEBUG: Object expression became empty before eval. No action.")
return return
print(f"DEBUG: Attempting to eval: '{obj_expr_str}'") print(f"DEBUG: Attempting to eval: '{obj_expr_str}'")
@ -423,11 +475,9 @@ class HybridCalculatorApp:
methods = obj.PopupFunctionList() methods = obj.PopupFunctionList()
if methods: if methods:
self._show_autocomplete_popup(methods, is_global_popup=False) self._show_autocomplete_popup(methods, is_global_popup=False)
# else: Podríamos añadir un fallback a dir(obj) aquí si se desea para objetos genéricos
# print(f"DEBUG: Object {type(obj)} has no PopupFunctionList. dir(obj) could be used.")
def _show_autocomplete_popup(self, suggestions, is_global_popup=False): def _show_autocomplete_popup(self, suggestions, is_global_popup=False):
# suggestions: lista de tuplas (nombre, hint) """Muestra popup de autocompletado (sin cambios)"""
cursor_bbox = self.input_text.bbox(tk.INSERT) cursor_bbox = self.input_text.bbox(tk.INSERT)
if not cursor_bbox: if not cursor_bbox:
return return
@ -449,35 +499,29 @@ class HybridCalculatorApp:
if suggestions: if suggestions:
self._autocomplete_listbox.select_set(0) self._autocomplete_listbox.select_set(0)
self._autocomplete_listbox.pack(expand=True, fill=tk.BOTH) self._autocomplete_listbox.pack(expand=True, fill=tk.BOTH)
self._autocomplete_listbox.bind("<Return>", self._on_autocomplete_select) self._autocomplete_listbox.bind("<Return>", lambda e, g=is_global_popup: self._on_autocomplete_select(e, is_global=g))
self._autocomplete_listbox.bind("<Tab>", self._on_autocomplete_select) self._autocomplete_listbox.bind("<Tab>", lambda e, g=is_global_popup: self._on_autocomplete_select(e, is_global=g))
self._autocomplete_listbox.bind("<Escape>", lambda e: self._close_autocomplete_popup()) self._autocomplete_listbox.bind("<Escape>", lambda e: self._close_autocomplete_popup())
self._autocomplete_listbox.bind("<Double-Button-1>", lambda e, g=is_global_popup: self._on_autocomplete_select(e, is_global=g)) self._autocomplete_listbox.bind("<Double-Button-1>", lambda e, g=is_global_popup: self._on_autocomplete_select(e, is_global=g))
self._autocomplete_listbox.focus_set() self._autocomplete_listbox.focus_set()
self._autocomplete_listbox.bind("<Up>", lambda e: self._navigate_autocomplete(e, -1)) self._autocomplete_listbox.bind("<Up>", lambda e: self._navigate_autocomplete(e, -1))
self._autocomplete_listbox.bind("<Down>", lambda e: self._navigate_autocomplete(e, 1)) self._autocomplete_listbox.bind("<Down>", lambda e: self._navigate_autocomplete(e, 1))
# self.input_text.bind("<FocusOut>", lambda e: self._close_autocomplete_popup(), add=True) # Removed: Caused popup to close immediately
self.input_text.bind("<Button-1>", lambda e: self._close_autocomplete_popup(), add=True) self.input_text.bind("<Button-1>", lambda e: self._close_autocomplete_popup(), add=True)
self.root.bind("<Button-1>", lambda e: self._close_autocomplete_popup(), add=True) self.root.bind("<Button-1>", lambda e: self._close_autocomplete_popup(), add=True)
# self.input_text.bind("<Key>", lambda e: self._close_autocomplete_popup(), add=True) # Removed: Too aggressive
# Pasar el flag is_global_popup a los bindings que llaman a _on_autocomplete_select
self._autocomplete_listbox.bind("<Return>", lambda e, g=is_global_popup: self._on_autocomplete_select(e, is_global=g))
self._autocomplete_listbox.bind("<Tab>", lambda e, g=is_global_popup: self._on_autocomplete_select(e, is_global=g))
max_len = max(len(name) for name, _ in suggestions) if suggestions else 10 max_len = max(len(name) for name, _ in suggestions) if suggestions else 10
width = max(15, min(max_len + 10, 50)) width = max(15, min(max_len + 10, 50))
height = min(len(suggestions), 10) height = min(len(suggestions), 10)
# Calcular el ancho basado en el texto completo que se muestra en el listbox
full_text_suggestions = [f"{name}{hint}" for name, hint in suggestions] full_text_suggestions = [f"{name}{hint}" for name, hint in suggestions]
max_full_len = max(len(text) for text in full_text_suggestions) if full_text_suggestions else 20 max_full_len = max(len(text) for text in full_text_suggestions) if full_text_suggestions else 20
width = max(20, min(max_full_len + 5, 80)) # Ajustar el +5 y el límite 80 según sea necesario width = max(20, min(max_full_len + 5, 80))
self._autocomplete_listbox.config(width=width, height=height) self._autocomplete_listbox.config(width=width, height=height)
else: else:
self._close_autocomplete_popup() self._close_autocomplete_popup()
def _navigate_autocomplete(self, event, direction): def _navigate_autocomplete(self, event, direction):
"""Navegación en autocomplete (sin cambios)"""
if not hasattr(self, '_autocomplete_listbox') or not self._autocomplete_listbox: if not hasattr(self, '_autocomplete_listbox') or not self._autocomplete_listbox:
return "break" return "break"
current_selection = self._autocomplete_listbox.curselection() current_selection = self._autocomplete_listbox.curselection()
@ -495,6 +539,7 @@ class HybridCalculatorApp:
return "break" return "break"
def _on_autocomplete_select(self, event, is_global=False): def _on_autocomplete_select(self, event, is_global=False):
"""Selección de autocomplete (sin cambios)"""
if not hasattr(self, '_autocomplete_listbox') or not self._autocomplete_listbox: if not hasattr(self, '_autocomplete_listbox') or not self._autocomplete_listbox:
return "break" return "break"
selection = self._autocomplete_listbox.curselection() selection = self._autocomplete_listbox.curselection()
@ -503,49 +548,31 @@ class HybridCalculatorApp:
return "break" return "break"
selected_text_with_hint = self._autocomplete_listbox.get(selection[0]) selected_text_with_hint = self._autocomplete_listbox.get(selection[0])
# Extraer solo el nombre del ítem, antes de " —"
item_name = selected_text_with_hint.split("")[0].strip() item_name = selected_text_with_hint.split("")[0].strip()
if is_global: if is_global:
# Eliminar el punto que activó el popup y luego insertar el nombre cursor_pos_str = self.input_text.index(tk.INSERT)
cursor_pos_str = self.input_text.index(tk.INSERT) # Posición actual (después del punto)
line_num, char_num = map(int, cursor_pos_str.split('.')) line_num, char_num = map(int, cursor_pos_str.split('.'))
# El punto está en char_num - 1 en la línea actual
dot_pos_on_line = char_num - 1 dot_pos_on_line = char_num - 1
dot_index_str = f"{line_num}.{dot_pos_on_line}" dot_index_str = f"{line_num}.{dot_pos_on_line}"
self.input_text.delete(dot_index_str) self.input_text.delete(dot_index_str)
# Insertar el nombre de la función/clase seguido de "()"
insert_text = item_name + "()" insert_text = item_name + "()"
self.input_text.insert(dot_index_str, insert_text) self.input_text.insert(dot_index_str, insert_text)
# Colocar cursor dentro de los paréntesis: después del nombre y el '('
self.input_text.mark_set(tk.INSERT, f"{dot_index_str}+{len(item_name)+1}c") self.input_text.mark_set(tk.INSERT, f"{dot_index_str}+{len(item_name)+1}c")
else: else:
# Comportamiento existente para métodos de objeto
self.input_text.insert(tk.INSERT, item_name + "()") self.input_text.insert(tk.INSERT, item_name + "()")
self.input_text.mark_set(tk.INSERT, f"{tk.INSERT}-1c") # Cursor dentro de los paréntesis self.input_text.mark_set(tk.INSERT, f"{tk.INSERT}-1c")
self._close_autocomplete_popup() self._close_autocomplete_popup()
self.input_text.focus_set() self.input_text.focus_set()
self.on_key_release() # Trigger re-evaluation self.on_key_release()
return "break" return "break"
def _close_autocomplete_popup(self): def _close_autocomplete_popup(self):
"""Cierra popup de autocomplete (sin cambios)"""
if hasattr(self, '_autocomplete_popup') and self._autocomplete_popup: if hasattr(self, '_autocomplete_popup') and self._autocomplete_popup:
self._autocomplete_popup.destroy() self._autocomplete_popup.destroy()
self._autocomplete_popup = None self._autocomplete_popup = None
# Consider unbinding the Button-1 events if they were stored with IDs,
# for now, their guard condition `if self._autocomplete_popup:` handles multiple calls.
# Example of how to unbind if IDs were stored:
# if hasattr(self, '_input_text_b1_bind_id'):
# self.input_text.unbind("<Button-1>", self._input_text_b1_bind_id)
# del self._input_text_b1_bind_id
# if hasattr(self, '_root_b1_bind_id'):
# self.root.unbind("<Button-1>", self._root_b1_bind_id)
# del self._root_b1_bind_id
if hasattr(self, '_autocomplete_listbox') and self._autocomplete_listbox: if hasattr(self, '_autocomplete_listbox') and self._autocomplete_listbox:
self._autocomplete_listbox = None self._autocomplete_listbox = None
@ -592,7 +619,6 @@ class HybridCalculatorApp:
if result.is_error: if result.is_error:
ayuda = self.obtener_ayuda(result.original_line) ayuda = self.obtener_ayuda(result.original_line)
if ayuda: if ayuda:
# Mostrar ayuda en un solo renglón, truncando si es necesario
ayuda_linea = ayuda.replace("\n", " ").replace("\r", " ") ayuda_linea = ayuda.replace("\n", " ").replace("\r", " ")
if len(ayuda_linea) > 120: if len(ayuda_linea) > 120:
ayuda_linea = ayuda_linea[:117] + "..." ayuda_linea = ayuda_linea[:117] + "..."
@ -608,8 +634,8 @@ class HybridCalculatorApp:
else: else:
# Resultado normal # Resultado normal
if result.result is not None: if result.result is not None:
# Determinar tag basado en tipo # Determinar tag basado en tipo (DINÁMICO)
tag = self._get_result_tag(result.result) tag = self._get_result_tag_dynamic(result.result)
# Verificar si es resultado interactivo # Verificar si es resultado interactivo
if self.interactive_manager and result.is_interactive: if self.interactive_manager and result.is_interactive:
@ -623,90 +649,122 @@ class HybridCalculatorApp:
# Añadir pista de clase para el resultado principal # Añadir pista de clase para el resultado principal
primary_result_object = result.result primary_result_object = result.result
if not isinstance(primary_result_object, PlotResult): # PlotResult ya tiene su propio formato if not isinstance(primary_result_object, PlotResult):
class_display_name = "" class_display_name = self._get_class_display_name_dynamic(primary_result_object)
if isinstance(primary_result_object, SympyClassBase):
class_display_name = type(primary_result_object).__name__.replace("Class_", "")
elif isinstance(primary_result_object, sympy.logic.boolalg.BooleanAtom): # sympy.true/false
class_display_name = "Boolean"
elif isinstance(primary_result_object, sympy.Basic): # Objetos SymPy generales
if hasattr(primary_result_object, 'is_number') and primary_result_object.is_number:
if hasattr(primary_result_object, 'is_Integer') and primary_result_object.is_Integer:
class_display_name = "Integer"
elif hasattr(primary_result_object, 'is_Rational') and primary_result_object.is_Rational and not primary_result_object.is_Integer :
class_display_name = "Rational"
elif hasattr(primary_result_object, 'is_Float') and primary_result_object.is_Float:
class_display_name = "Float"
else:
class_display_name = "SympyNumber" # Otros números de SymPy
else: # Expresiones SymPy, símbolos, etc.
class_display_name = "Sympy"
elif isinstance(primary_result_object, bool): # bool de Python
class_display_name = "Boolean"
elif isinstance(primary_result_object, (int, float, str, list, dict, tuple, type(None))):
class_display_name = type(primary_result_object).__name__.capitalize()
if class_display_name == "Nonetype": class_display_name = "None"
# Nombres como 'Int', 'Float', 'Str', 'List', 'Dict', 'Tuple' están bien.
if class_display_name: if class_display_name:
output_parts.append(("class_hint", f"[{class_display_name}]")) output_parts.append(("class_hint", f"[{class_display_name}]"))
# Mostrar evaluación numérica si existe # Mostrar evaluación numérica si existe
if result.numeric_result is not None and result.numeric_result != result.result: if result.numeric_result is not None and result.numeric_result != result.result:
output_parts.append(("numeric", f"{result.numeric_result}")) # El espacio se controlará en _display_output output_parts.append(("numeric", f"{result.numeric_result}"))
# Mostrar información adicional # Mostrar información adicional
if result.info: if result.info:
output_parts.append(("info", f"({result.info})")) # El espacio se controlará en _display_output output_parts.append(("info", f"({result.info})"))
return output_parts return output_parts
def _get_result_tag(self, result: Any) -> str: def _get_result_tag_dynamic(self, result: Any) -> str:
"""Determina el tag de color para un resultado""" """Determina el tag de color para un resultado - VERSIÓN DINÁMICA"""
if isinstance(result, Class_Hex): # Obtener clases registradas dinámicamente
return "hex" try:
elif isinstance(result, Class_Bin): registered_classes = self.engine.get_available_types().get('registered_classes', {})
return "bin"
elif isinstance(result, Class_IP4): # Verificar si es una instancia de alguna clase registrada
return "ip" for name, cls in registered_classes.items():
elif isinstance(result, Class_Chr): if isinstance(result, cls):
return "chr_type" # Usar tags específicos si existen, sino usar genérico
if name.lower() == "hex":
return "hex"
elif name.lower() == "bin":
return "bin"
elif name.lower() in ["ip4", "ip"]:
return "ip"
elif name.lower() == "chr":
return "chr_type"
else:
return "custom_type" # Tag genérico para tipos personalizados
except Exception as e:
print(f"DEBUG: Error en get_result_tag_dynamic: {e}")
# Fallback a tags existentes
if hasattr(result, '__class__') and 'Class_' in result.__class__.__name__:
return "custom_type"
elif isinstance(result, sympy.Basic): elif isinstance(result, sympy.Basic):
return "symbolic" return "symbolic"
else: else:
return "result" return "result"
def _get_class_display_name_dynamic(self, obj: Any) -> str:
"""Obtiene nombre de clase para display - VERSIÓN DINÁMICA"""
try:
# Verificar si es una clase registrada
registered_classes = self.engine.get_available_types().get('registered_classes', {})
for name, cls in registered_classes.items():
if isinstance(obj, cls):
return name
except Exception as e:
print(f"DEBUG: Error en get_class_display_name_dynamic: {e}")
# Fallback a lógica existente
if hasattr(obj, '__class__'):
class_name = obj.__class__.__name__
if class_name.startswith('Class_'):
return class_name.replace("Class_", "")
elif class_name.endswith('Mask'):
return class_name
if isinstance(obj, sympy.logic.boolalg.BooleanAtom):
return "Boolean"
elif isinstance(obj, sympy.Basic):
if hasattr(obj, 'is_number') and obj.is_number:
if hasattr(obj, 'is_Integer') and obj.is_Integer:
return "Integer"
elif hasattr(obj, 'is_Rational') and obj.is_Rational and not obj.is_Integer:
return "Rational"
elif hasattr(obj, 'is_Float') and obj.is_Float:
return "Float"
else:
return "SympyNumber"
else:
return "Sympy"
elif isinstance(obj, bool):
return "Boolean"
elif isinstance(obj, (int, float, str, list, dict, tuple, type(None))):
class_display_name = type(obj).__name__.capitalize()
if class_display_name == "Nonetype":
class_display_name = "None"
return class_display_name
return ""
def _display_output(self, output_data: List[List[tuple]]): def _display_output(self, output_data: List[List[tuple]]):
"""Muestra los datos de salida en el widget""" """Muestra los datos de salida en el widget (sin cambios)"""
self.output_text.config(state="normal") self.output_text.config(state="normal")
self.output_text.delete("1.0", tk.END) self.output_text.delete("1.0", tk.END)
for line_idx, line_parts in enumerate(output_data): for line_idx, line_parts in enumerate(output_data):
# Línea vacía
if not line_parts or (len(line_parts) == 1 and line_parts[0][0] == "" and line_parts[0][1] == ""): if not line_parts or (len(line_parts) == 1 and line_parts[0][0] == "" and line_parts[0][1] == ""):
pass pass
else: else:
# Mostrar partes de la línea
for part_idx, (tag, content) in enumerate(line_parts): for part_idx, (tag, content) in enumerate(line_parts):
if not content: # Omitir contenido vacío if not content:
continue continue
# Determinar si se necesita un separador antes de esta parte
if part_idx > 0: if part_idx > 0:
prev_tag, prev_content = line_parts[part_idx-1] if part_idx > 0 else (None, None) prev_tag, prev_content = line_parts[part_idx-1] if part_idx > 0 else (None, None)
# No añadir separador si la parte actual es una "anotación" o si la parte anterior estaba vacía.
if tag not in ["class_hint", "numeric", "info"] and prev_content: if tag not in ["class_hint", "numeric", "info"] and prev_content:
self.output_text.insert(tk.END, " ; ") self.output_text.insert(tk.END, " ; ")
# 'numeric' e 'info' necesitan un espacio precedente si siguen a contenido.
elif tag in ["numeric", "info"] and prev_content: elif tag in ["numeric", "info"] and prev_content:
self.output_text.insert(tk.END, " ") self.output_text.insert(tk.END, " ")
# 'class_hint' se une directamente.
if content: # Asegurarse de que hay contenido antes de insertar if content:
self.output_text.insert(tk.END, str(content), tag) self.output_text.insert(tk.END, str(content), tag)
# Añadir nueva línea excepto para la última línea
if line_idx < len(output_data) - 1: if line_idx < len(output_data) - 1:
self.output_text.insert(tk.END, "\n") self.output_text.insert(tk.END, "\n")
@ -753,7 +811,8 @@ class HybridCalculatorApp:
finally: finally:
context_menu.grab_release() context_menu.grab_release()
# Métodos de menú y comandos # ========== MÉTODOS DE MENÚ Y COMANDOS (la mayoría sin cambios) ==========
def new_session(self): def new_session(self):
"""Inicia nueva sesión""" """Inicia nueva sesión"""
self.clear_input() self.clear_input()
@ -838,16 +897,42 @@ class HybridCalculatorApp:
self.root.clipboard_append(content) self.root.clipboard_append(content)
def insert_example(self): def insert_example(self):
"""Inserta código de ejemplo""" """Inserta código de ejemplo - ACTUALIZADO CON TIPOS DINÁMICOS"""
example = """# Calculadora MAV - CAS Híbrido # Obtener tipos disponibles dinámicamente
try:
available_types = self.engine.get_available_types()
registered_classes = available_types.get('registered_classes', {})
except:
registered_classes = {}
# Crear ejemplo base
example = """# Calculadora MAV - CAS Híbrido con Sistema de Tipos Dinámico
# Sintaxis nueva con corchetes # Sintaxis nueva con corchetes
# Tipos especializados """
Hex[FF] + 1
IP4[192.168.1.100/24].NetworkAddress[]
Bin[1010] * 2
# Matemáticas simbólicas # Añadir ejemplos de tipos disponibles dinámicamente
if registered_classes:
example += "# Tipos especializados disponibles\n"
for name in sorted(registered_classes.keys()):
if name == "Hex":
example += "Hex[FF] + 1\n"
elif name == "Bin":
example += "Bin[1010] * 2\n"
elif name == "Chr":
example += "Chr[A].toHex()\n"
elif name == "Dec":
example += "Dec[42].toBin()\n"
elif name == "IP4":
example += "IP4[192.168.1.100/24].NetworkAddress()\n"
elif name == "IP4Mask":
example += "IP4Mask[24].hosts_count()\n"
example += "\n"
# Resto del ejemplo (sin cambios)
example += """# Matemáticas simbólicas
x + 2*y x + 2*y
diff(x**2 + sin(x), x) diff(x**2 + sin(x), x)
integrate(x**2, x) integrate(x**2, x)
@ -870,6 +955,7 @@ plot(sin(x), (x, -2*pi, 2*pi))
# Matrices # Matrices
Matrix([[1, 2], [3, 4]]) Matrix([[1, 2], [3, 4]])
""" """
self.input_text.delete("1.0", tk.END) self.input_text.delete("1.0", tk.END)
self.input_text.insert("1.0", example) self.input_text.insert("1.0", example)
self._evaluate_and_update() self._evaluate_and_update()
@ -963,13 +1049,64 @@ Matrix([[1, 2], [3, 4]])
except Exception as e: except Exception as e:
messagebox.showerror("Error", f"Error resolviendo sistema:\n{e}") messagebox.showerror("Error", f"Error resolviendo sistema:\n{e}")
def show_types_syntax(self):
"""Muestra sintaxis de tipos disponibles - NUEVA FUNCIÓN"""
try:
types_info = self.engine.get_available_types()
registered_classes = types_info.get('registered_classes', {})
syntax_text = "SINTAXIS DE TIPOS DISPONIBLES\n\n"
if not registered_classes:
syntax_text += "No hay tipos personalizados disponibles.\n"
else:
syntax_text += "Tipos personalizados detectados:\n\n"
for name, cls in sorted(registered_classes.items()):
syntax_text += f"=== {name} ===\n"
# Sintaxis básica
syntax_text += f"Sintaxis: {name}[valor]\n"
syntax_text += f"Alias: {name.lower()}[valor]\n"
# Obtener ayuda si está disponible
if hasattr(cls, 'Helper'):
try:
help_text = cls.Helper(name)
if help_text:
syntax_text += f"Ayuda: {help_text}\n"
except:
pass
# Obtener métodos si está disponible
if hasattr(cls, 'PopupFunctionList'):
try:
methods = cls.PopupFunctionList()
if methods:
syntax_text += "Métodos disponibles:\n"
for method_name, method_desc in methods:
syntax_text += f"{method_name}() - {method_desc}\n"
except:
pass
syntax_text += "\n"
self._show_help_window("Sintaxis de Tipos", syntax_text)
except Exception as e:
messagebox.showerror("Error", f"Error obteniendo sintaxis de tipos:\n{e}")
def show_quick_guide(self): def show_quick_guide(self):
"""Muestra guía rápida""" """Muestra guía rápida - ACTUALIZADA"""
guide = """# Calculadora MAV - CAS Híbrido guide = """# Calculadora MAV - CAS Híbrido
## Sistema de Tipos Dinámico
El sistema detecta automáticamente tipos disponibles en custom_types/
## Sintaxis Nueva con Corchetes ## Sintaxis Nueva con Corchetes
- IP4[192.168.1.1/24] en lugar de IP4("192.168.1.1/24") - Sintaxis: Tipo[valor] en lugar de Tipo("valor")
- Hex[FF], Bin[1010], Dec[10.5], Chr[A] - Ejemplos: Hex[FF], Bin[1010], Dec[10.5], Chr[A]
- Use menú Tipos Información de tipos para ver tipos disponibles
## Ecuaciones Automáticas ## Ecuaciones Automáticas
- x**2 + 2*x = 8 (detectado automáticamente) - x**2 + 2*x = 8 (detectado automáticamente)
@ -990,26 +1127,29 @@ Matrix([[1, 2], [3, 4]])
- Todas las variables son símbolos SymPy - Todas las variables son símbolos SymPy
- x = 5 crea Symbol('x') con valor 5 - x = 5 crea Symbol('x') con valor 5
- Evaluación simbólica + numérica automática - Evaluación simbólica + numérica automática
## Autocompletado Dinámico
- Escriba "." después de cualquier objeto para ver métodos
- El sistema usa los tipos registrados automáticamente
""" """
self._show_help_window("Guía Rápida", guide) self._show_help_window("Guía Rápida", guide)
def show_syntax_help(self): def show_syntax_help(self):
"""Muestra ayuda de sintaxis""" """Muestra ayuda de sintaxis - ACTUALIZADA"""
syntax = """# Sintaxis del CAS Híbrido syntax = """# Sintaxis del CAS Híbrido
## Clases Especializadas (solo corchetes) ## Sistema de Tipos Dinámico
IP4[dirección/prefijo] # IP4[192.168.1.1/24] Los tipos se detectan automáticamente desde custom_types/
Hex[valor] # Hex[FF], Hex[255] Use menú Tipos Información de tipos para ver tipos disponibles
Bin[valor] # Bin[1010], Bin[10]
Dec[valor] # Dec[10.5], Dec[10]
Chr[carácter] # Chr[A], Chr[Hello]
## Métodos Disponibles ## Sintaxis con Corchetes (Dinámica)
IP4[...].NetworkAddress[] Tipo[valor] # Sintaxis general
IP4[...].BroadcastAddress[] Tipo[arg1; arg2] # Múltiples argumentos
IP4[...].Nodes()
Hex[...].toDecimal() ## Métodos Disponibles (Dinámicos)
Tipo[...].método() # Métodos específicos del tipo
objeto.método[] # Método sin argumentos
## Ecuaciones (detección automática) ## Ecuaciones (detección automática)
expresión = expresión # Ecuación simple expresión = expresión # Ecuación simple
@ -1028,7 +1168,7 @@ expresión # Evaluación simbólica automática
self._show_help_window("Sintaxis", syntax) self._show_help_window("Sintaxis", syntax)
def show_sympy_functions(self): def show_sympy_functions(self):
"""Muestra funciones SymPy disponibles""" """Muestra funciones SymPy disponibles (sin cambios)"""
functions = """# Funciones SymPy Disponibles functions = """# Funciones SymPy Disponibles
## Matemáticas Básicas ## Matemáticas Básicas
@ -1066,20 +1206,28 @@ pi, E, I (imaginario), oo (infinito)
self._show_help_window("Funciones SymPy", functions) self._show_help_window("Funciones SymPy", functions)
def show_about(self): def show_about(self):
"""Muestra información sobre la aplicación""" """Muestra información sobre la aplicación - ACTUALIZADA"""
about = """Calculadora MAV - CAS Híbrido about = """Calculadora MAV - CAS Híbrido
Versión: 2.0 Versión: 2.1 (Sistema de Tipos Dinámico)
Motor: SymPy + Clases Especializadas Motor: SymPy + Auto-descubrimiento de Tipos
Características: Características:
Motor algebraico completo (SymPy) Motor algebraico completo (SymPy)
Sistema de tipos dinámico y extensible
Sintaxis simplificada con corchetes Sintaxis simplificada con corchetes
Detección automática de ecuaciones Detección automática de ecuaciones
Resultados interactivos clickeables Resultados interactivos clickeables
Tipos especializados (IP4, Hex, Bin, etc.) Auto-descubrimiento de tipos en custom_types/
Variables SymPy puras Variables SymPy puras
Plotting integrado Plotting integrado
Autocompletado dinámico
NUEVO: Sistema de Tipos Dinámico
Detección automática de nuevos tipos
Organización modular en custom_types/
Registro automático sin modificar código
Escalabilidad mejorada
Desarrollado para cálculo matemático avanzado Desarrollado para cálculo matemático avanzado
con soporte especializado para redes, con soporte especializado para redes,
@ -1147,10 +1295,15 @@ programación y análisis numérico.
self.root.destroy() self.root.destroy()
def obtener_ayuda(self, input_str): def obtener_ayuda(self, input_str):
"""Obtiene ayuda usando helpers dinámicos"""
for helper in self.HELPERS: for helper in self.HELPERS:
ayuda = helper(input_str) try:
if ayuda: ayuda = helper(input_str)
return ayuda if ayuda:
return ayuda
except Exception as e:
print(f"DEBUG: Error en helper: {e}")
continue
return None return None

View File

@ -1,5 +1,5 @@
""" """
Motor de evaluación híbrida que usa SymPy como base con clases especializadas Motor de evaluación híbrida INTEGRADO con el sistema de auto-descubrimiento de tipos
""" """
import sympy import sympy
from sympy import symbols, Symbol, sympify, solve, Eq, simplify from sympy import symbols, Symbol, sympify, solve, Eq, simplify
@ -8,36 +8,64 @@ import ast
import re import re
from contextlib import contextmanager from contextlib import contextmanager
# Importaciones del sistema de tipos
from type_registry import (
discover_and_register_types,
get_registered_base_context,
get_registered_bracket_classes,
get_registered_helper_functions
)
# Importaciones existentes
from tl_bracket_parser import BracketParser from tl_bracket_parser import BracketParser
from tl_popup import PlotResult from tl_popup import PlotResult
from sympy_Base import SympyClassBase
from ip4_type import Class_IP4, IP4Mask
from hex_type import Class_Hex
from bin_type import Class_Bin
from dec_type import Class_Dec
from chr_type import Class_Chr
class HybridEvaluationEngine: class HybridEvaluationEngine:
""" """
Motor de evaluación híbrida que combina SymPy con clases especializadas Motor de evaluación híbrida que combina SymPy con clases especializadas
VERSIÓN INTEGRADA con auto-descubrimiento de tipos
""" """
def __init__(self): def __init__(self, auto_discover_types: bool = True, types_directory: str = "custom_types"):
self.parser = BracketParser() self.parser = BracketParser()
self.symbol_table: Dict[str, Any] = {} self.symbol_table: Dict[str, Any] = {}
self.equations: List[sympy.Eq] = [] self.equations: List[sympy.Eq] = []
self.last_result = None self.last_result = None
# Contexto base con funciones y clases # Configuración del sistema de tipos
self._setup_base_context() self.types_directory = types_directory
self.auto_discover_enabled = auto_discover_types
# Información de tipos registrados
self.registered_types_info = {}
self.helper_functions = []
# Debug mode # Debug mode
self.debug = False self.debug = False
# Configurar contexto base
self._setup_base_context()
def _setup_base_context(self): def _setup_base_context(self):
"""Configura el contexto base con funciones matemáticas y clases""" """Configura el contexto base con funciones matemáticas y clases"""
# Funciones matemáticas de SymPy
# 1. DESCOBRIR Y REGISTRAR TIPOS AUTOMÁTICAMENTE
if self.auto_discover_enabled:
try:
self.registered_types_info = discover_and_register_types(self.types_directory)
if self.debug:
print(f"🔍 Tipos descubiertos: {self.registered_types_info['class_count']} clases")
except Exception as e:
print(f"⚠️ Error en auto-descubrimiento: {e}")
self.registered_types_info = {
'base_context': {},
'bracket_classes': set(),
'helper_functions': []
}
# 2. FUNCIONES MATEMÁTICAS DE SYMPY (BASE)
math_functions = { math_functions = {
'pi': sympy.pi, 'pi': sympy.pi,
'e': sympy.E, 'e': sympy.E,
@ -83,24 +111,10 @@ class HybridEvaluationEngine:
'plot3d': self._create_plot3d_placeholder, 'plot3d': self._create_plot3d_placeholder,
} }
# Clases especializadas # 3. CLASES ESPECIALIZADAS (DESDE AUTO-DESCUBRIMIENTO)
specialized_classes = { specialized_classes = self.registered_types_info.get('base_context', {})
'Hex': Class_Hex,
'Bin': Class_Bin,
'Dec': Class_Dec,
'IP4': Class_IP4,
'Chr': Class_Chr,
'IP4Mask': IP4Mask,
# Alias en minúsculas
'hex': Class_Hex,
'bin': Class_Bin,
'dec': Class_Dec,
'ip4': Class_IP4,
'chr': Class_Chr,
'ip4mask': IP4Mask,
}
# Funciones de utilidad # 4. FUNCIONES DE UTILIDAD
utility_functions = { utility_functions = {
'_add_equation': self._add_equation, '_add_equation': self._add_equation,
'_assign_variable': self._assign_variable, '_assign_variable': self._assign_variable,
@ -108,12 +122,65 @@ class HybridEvaluationEngine:
'evalf': lambda expr, n=15: expr.evalf(n) if hasattr(expr, 'evalf') else float(expr), 'evalf': lambda expr, n=15: expr.evalf(n) if hasattr(expr, 'evalf') else float(expr),
} }
# 5. COMBINAR TODO EN EL CONTEXTO BASE
self.base_context = { self.base_context = {
**math_functions, **math_functions,
**specialized_classes, **specialized_classes,
**utility_functions **utility_functions
} }
# 6. ACTUALIZAR HELPER FUNCTIONS
self.helper_functions = get_registered_helper_functions()
# 7. ACTUALIZAR BRACKET PARSER CON CLASES DESCUBIERTAS
self._update_bracket_parser()
if self.debug:
print(f"📋 Contexto base configurado: {len(self.base_context)} entradas")
print(f"🆘 Helper functions: {len(self.helper_functions)}")
def _update_bracket_parser(self):
"""Actualiza el BracketParser con las clases descubiertas"""
try:
discovered_bracket_classes = get_registered_bracket_classes()
# Combinar con clases existentes del parser
self.parser.BRACKET_CLASSES = self.parser.BRACKET_CLASSES.union(discovered_bracket_classes)
if self.debug:
print(f"🔧 Bracket classes actualizadas: {self.parser.BRACKET_CLASSES}")
except Exception as e:
print(f"⚠️ Error actualizando bracket parser: {e}")
def reload_types(self):
"""Recarga todos los tipos del directorio (útil para desarrollo)"""
if self.debug:
print("🔄 Recargando tipos...")
self._setup_base_context()
if self.debug:
print("✅ Tipos recargados")
def get_available_types(self) -> Dict[str, Any]:
"""Retorna información sobre los tipos disponibles"""
return {
'registered_classes': self.registered_types_info.get('registered_classes', {}),
'bracket_classes': list(self.registered_types_info.get('bracket_classes', set())),
'total_context_entries': len(self.base_context),
'helper_functions_count': len(self.helper_functions)
}
def get_type_help(self, type_name: str) -> Optional[str]:
"""Obtiene ayuda para un tipo específico"""
# Buscar en clases registradas
registered_classes = self.registered_types_info.get('registered_classes', {})
if type_name in registered_classes:
cls = registered_classes[type_name]
if hasattr(cls, 'Helper'):
return cls.Helper(type_name)
return None
def _create_plot_placeholder(self, *args, **kwargs): def _create_plot_placeholder(self, *args, **kwargs):
"""Crear placeholder para plots que será manejado por resultados interactivos""" """Crear placeholder para plots que será manejado por resultados interactivos"""
if self.debug: if self.debug:
@ -127,16 +194,30 @@ class HybridEvaluationEngine:
return PlotResult('plot3d', args, kwargs) return PlotResult('plot3d', args, kwargs)
def _help_function(self, obj=None): def _help_function(self, obj=None):
"""Función de ayuda integrada""" """Función de ayuda integrada que usa el sistema de helpers"""
if obj is None: if obj is None:
return "Ayuda disponible. Use help(función) para ayuda específica." return "Ayuda disponible. Use help(función) para ayuda específica."
# Primero intentar con el objeto directamente
if hasattr(obj, '__doc__') and obj.__doc__: if hasattr(obj, '__doc__') and obj.__doc__:
return obj.__doc__ return obj.__doc__
elif hasattr(obj, 'Helper'): elif hasattr(obj, 'Helper'):
return obj.Helper("") return obj.Helper("")
else:
return f"No hay ayuda disponible para {obj}" # Luego buscar en helpers registrados
obj_name = getattr(obj, '__name__', str(obj))
for helper_func in self.helper_functions:
try:
help_result = helper_func(obj_name)
if help_result:
return help_result
except:
continue
return f"No hay ayuda disponible para {obj}"
# ========== RESTO DE MÉTODOS EXISTENTES ==========
# (Los métodos de evaluación permanecen igual)
def evaluate_line(self, line: str) -> 'EvaluationResult': def evaluate_line(self, line: str) -> 'EvaluationResult':
""" """
@ -316,7 +397,7 @@ class HybridEvaluationEngine:
result = eval(expression, {"__builtins__": {}}, context) result = eval(expression, {"__builtins__": {}}, context)
# Si el resultado es un objeto híbrido, integrarlo con SymPy si es necesario # Si el resultado es un objeto híbrido, integrarlo con SymPy si es necesario
if isinstance(result, SympyClassBase): if hasattr(result, '_sympystr'): # SympyClassBase
return result return result
elif isinstance(result, PlotResult): elif isinstance(result, PlotResult):
if self.debug: if self.debug:
@ -488,55 +569,59 @@ class EvaluationResult:
# Funciones de testing # Funciones de testing
def test_evaluation_engine(): def test_hybrid_engine_with_types():
"""Test del motor de evaluación""" """Test del motor de evaluación con sistema de tipos"""
engine = HybridEvaluationEngine() print("🧪 Test HybridEvaluationEngine con Type Registry")
print("=" * 60)
# Crear motor con auto-descubrimiento
engine = HybridEvaluationEngine(auto_discover_types=True)
engine.debug = True engine.debug = True
# Mostrar información de tipos
types_info = engine.get_available_types()
print(f"📊 Tipos disponibles: {types_info}")
print()
# Test casos básicos
test_cases = [ test_cases = [
# Expresiones básicas # Expresiones básicas
"2 + 3", "2 + 3",
"x + 2", "x + 2",
"sin(pi/2)", "sin(pi/2)",
# Sintaxis con corchetes # Tipo Chr si está disponible
"Hex[FF]", "Chr[A]" if "Chr" in types_info['registered_classes'] else "# Chr no disponible",
"IP4[192.168.1.1/24]",
# 🧪 PLOTS - Casos específicos para testing # Variables y ecuaciones
"plot(sin(x), (x, -pi, pi))",
"plot(x**2, (x, -5, 5))",
# Ecuaciones
"x + 2 = 5",
"y**2 = 16",
# Solve
"solve(x + 2 - 5, x)",
# Variables
"a = 10", "a = 10",
"b = a + 5", "x + 2 = 5",
"solve(x + 2 - 5, x)",
# Funciones avanzadas # Funciones avanzadas
"diff(x**2, x)", "diff(x**2, x)",
"integrate(x**2, x)", "plot(sin(x), (x, -pi, pi))",
] ]
print("=== Test Motor de Evaluación ===") print("🔍 Ejecutando casos de prueba:")
for test in test_cases: for test in test_cases:
result = engine.evaluate_line(test) if test.startswith('#'):
print(f"'{test}'{result} (type: {result.result_type})") print(f"⏭️ {test}")
continue
# 🔍 Información adicional para plots result = engine.evaluate_line(test)
if 'plot' in test: print(f"'{test}'{result} (type: {result.result_type})")
print(f" 🎯 Es interactivo: {result.is_interactive}")
if isinstance(result.result, PlotResult):
print(f" 📊 PlotResult confirmado: {result.result.plot_type}")
if result.info: if result.info:
print(f" Info: {result.info}") print(f" Info: {result.info}")
if result.is_error:
print(f" ❌ Error: {result.error}")
print("\n🔄 Test recarga de tipos:")
engine.reload_types()
print("✅ Recarga completada")
if __name__ == "__main__": if __name__ == "__main__":
test_evaluation_engine() test_hybrid_engine_with_types()

View File

@ -1,22 +1,75 @@
""" """
Bracket Parser - Transformador de sintaxis con corchetes y detección contextual de ecuaciones Bracket Parser - INTEGRADO con el sistema de auto-descubrimiento de tipos
""" """
import ast import ast
import re import re
from typing import Tuple, Optional from typing import Tuple, Optional, Set, Dict
# Importar sistema de tipos
try:
from type_registry import get_registered_bracket_classes
TYPE_REGISTRY_AVAILABLE = True
except ImportError:
TYPE_REGISTRY_AVAILABLE = False
print("⚠️ Sistema de tipos no disponible, usando clases hardcodeadas")
class BracketParser: class BracketParser:
"""Parser que convierte sintaxis con corchetes y detecta ecuaciones contextualmente""" """Parser que convierte sintaxis con corchetes y detecta ecuaciones contextualmente"""
# Clases que soportan sintaxis con corchetes # Clases base que soportan sintaxis con corchetes (fallback)
BRACKET_CLASSES = {'IP4', 'Hex', 'Bin', 'Date', 'Dec', 'Chr', 'IP4Mask'} DEFAULT_BRACKET_CLASSES = {'IP4', 'Hex', 'Bin', 'Date', 'Dec', 'Chr', 'IP4Mask'}
# Operadores de comparación que pueden formar ecuaciones # Operadores de comparación que pueden formar ecuaciones
EQUATION_OPERATORS = {'==', '!=', '<', '<=', '>', '>=', '='} EQUATION_OPERATORS = {'==', '!=', '<', '<=', '>', '>=', '='}
def __init__(self): def __init__(self, use_type_registry: bool = True):
self.debug = False self.debug = False
self.use_type_registry = use_type_registry and TYPE_REGISTRY_AVAILABLE
# Inicializar clases de corchetes
self._update_bracket_classes()
def _update_bracket_classes(self):
"""Actualiza las clases que soportan sintaxis con corchetes"""
if self.use_type_registry:
try:
# Obtener clases del registro de tipos
registered_classes = get_registered_bracket_classes()
self.BRACKET_CLASSES = registered_classes.union(self.DEFAULT_BRACKET_CLASSES)
if self.debug:
print(f"🔧 Bracket classes desde registro: {registered_classes}")
print(f"🔧 Total bracket classes: {self.BRACKET_CLASSES}")
except Exception as e:
if self.debug:
print(f"⚠️ Error obteniendo clases del registro: {e}")
self.BRACKET_CLASSES = self.DEFAULT_BRACKET_CLASSES.copy()
else:
self.BRACKET_CLASSES = self.DEFAULT_BRACKET_CLASSES.copy()
def reload_bracket_classes(self):
"""Recarga las clases de corchetes desde el registro"""
if self.debug:
print("🔄 Recargando bracket classes...")
self._update_bracket_classes()
def add_bracket_class(self, class_name: str):
"""Añade una clase que soporta sintaxis con corchetes"""
self.BRACKET_CLASSES.add(class_name)
if self.debug:
print(f" Añadida bracket class: {class_name}")
def remove_bracket_class(self, class_name: str):
"""Remueve una clase de la sintaxis con corchetes"""
self.BRACKET_CLASSES.discard(class_name)
if self.debug:
print(f" Removida bracket class: {class_name}")
def get_bracket_classes(self) -> Set[str]:
"""Retorna el set actual de clases con sintaxis de corchetes"""
return self.BRACKET_CLASSES.copy()
def parse_line(self, code_line: str) -> Tuple[str, str]: def parse_line(self, code_line: str) -> Tuple[str, str]:
""" """
@ -144,9 +197,19 @@ class BracketParser:
def _transform_brackets(self, line: str) -> str: def _transform_brackets(self, line: str) -> str:
""" """
Transforma sintaxis Class[args] Class("args") y maneja métodos Transforma sintaxis Class[args] Class("args") y maneja métodos
VERSIÓN DINÁMICA que usa las clases registradas
""" """
# Pattern principal: ClassName[contenido] # Crear pattern dinámicamente basado en clases registradas
pattern = r'(\b(?:' + '|'.join(self.BRACKET_CLASSES) + r')\b)\[([^\]]*)\]' if not self.BRACKET_CLASSES:
return line # No hay clases registradas
# Pattern principal: ClassName[contenido] usando clases dinámicas
bracket_classes_pattern = '|'.join(re.escape(cls) for cls in self.BRACKET_CLASSES)
pattern = rf'(\b(?:{bracket_classes_pattern})\b)\[([^\]]*)\]'
if self.debug:
print(f"🔍 Usando pattern: {pattern}")
print(f"🔧 Clases registradas: {self.BRACKET_CLASSES}")
def replace_match(match): def replace_match(match):
class_name = match.group(1) class_name = match.group(1)
@ -169,6 +232,7 @@ class BracketParser:
processed_args.append(f'"{escaped_arg}"') processed_args.append(f'"{escaped_arg}"')
return f'{class_name}({", ".join(processed_args)})' return f'{class_name}({", ".join(processed_args)})'
# Aplicar transformación repetidamente hasta que no haya más cambios # Aplicar transformación repetidamente hasta que no haya más cambios
transformed = line transformed = line
while True: while True:
@ -183,6 +247,25 @@ class BracketParser:
return transformed return transformed
def test_bracket_transformation(self, test_line: str) -> Dict[str, str]:
"""Test de transformación de una línea específica"""
result = {
'input': test_line,
'bracket_classes': list(self.BRACKET_CLASSES),
'output': None,
'parse_info': None,
'error': None
}
try:
output, parse_info = self.parse_line(test_line)
result['output'] = output
result['parse_info'] = parse_info
except Exception as e:
result['error'] = str(e)
return result
class EquationDetector: class EquationDetector:
"""Detector específico para ecuaciones con análisis AST avanzado""" """Detector específico para ecuaciones con análisis AST avanzado"""
@ -227,20 +310,20 @@ class EquationDetector:
# Funciones de utilidad para testing # Funciones de utilidad para testing
def test_bracket_parser(): def test_bracket_parser_with_types():
"""Función de testing para el bracket parser""" """Función de testing para el bracket parser con tipos"""
parser = BracketParser() print("🧪 Test BracketParser con Type Registry")
print("=" * 50)
# Crear parser con registro de tipos
parser = BracketParser(use_type_registry=True)
parser.debug = True parser.debug = True
test_cases = [ print(f"🔧 Clases de corchetes disponibles: {parser.get_bracket_classes()}")
# Sintaxis con corchetes print()
("Hex[FF]", 'Hex("FF")', "bracket_transform"),
("IP4[192.168.1.1/24]", 'IP4("192.168.1.1/24")', "bracket_transform"),
("IP4[192.168.1.1;24]", 'IP4("192.168.1.1"; "24")', "bracket_transform"),
("IP4[10.0.0.5;255.255.0.0]", 'IP4("10.0.0.5", "255.255.0.0")', "bracket_transform"),
("IP4[192.168.1.1/24].NetworkAddress[]", 'IP4("192.168.1.1/24").NetworkAddress()', "bracket_transform"),
("Bin[1010]", 'Bin("1010")', "bracket_transform"),
# Test casos básicos (que funcionen sin importar qué tipos estén disponibles)
basic_test_cases = [
# Atajos solve # Atajos solve
("x=?", "solve(x)", "solve_shortcut"), ("x=?", "solve(x)", "solve_shortcut"),
("variable_name=?", "solve(variable_name)", "solve_shortcut"), ("variable_name=?", "solve(variable_name)", "solve_shortcut"),
@ -248,32 +331,67 @@ def test_bracket_parser():
# Asignaciones # Asignaciones
("z = 5", '_assign_variable("z", 5)', "assignment"), ("z = 5", '_assign_variable("z", 5)', "assignment"),
("w = z**2 + 3", '_assign_variable("w", z**2 + 3)', "assignment"), ("w = z**2 + 3", '_assign_variable("w", z**2 + 3)', "assignment"),
("result = Hex[FF]", '_assign_variable("result", Hex("FF"))', "assignment"),
# Ecuaciones standalone # Ecuaciones standalone
("x + 2 = 5", '_add_equation("x + 2 = 5")', "equation"), ("x + 2 = 5", '_add_equation("x + 2 = 5")', "equation"),
("3*a + b = 10", '_add_equation("3*a + b = 10")', "equation"), ("3*a + b = 10", '_add_equation("3*a + b = 10")', "equation"),
("x > 5", "x > 5", "expression"), # Comparación válida de Python
("a == b", "a == b", "expression"), # Comparación válida de Python
# NO ecuaciones
("result = solve(x + 2, x)", '_assign_variable("result", solve(x + 2, x))', "assignment"), # Asignación Python
("2 + 3", "2 + 3", "expression"), # Expresión simple
("sin(pi/2)", "sin(pi/2)", "expression"), # Función
# Expresiones normales # Expresiones normales
("x + 2*y", "x + 2*y", "expression"), ("x + 2*y", "x + 2*y", "expression"),
("diff(x**2, x)", "diff(x**2, x)", "expression"), ("diff(x**2, x)", "diff(x**2, x)", "expression"),
] ]
print("=== Test Bracket Parser ===") # Test casos de sintaxis con corchetes (dinámico)
for test_input, expected_result, expected_info in test_cases: bracket_test_cases = []
available_classes = parser.get_bracket_classes()
if "Chr" in available_classes:
bracket_test_cases.extend([
("Chr[A]", 'Chr("A")', "bracket_transform"),
("Chr[65]", 'Chr("65")', "bracket_transform"),
])
if "Hex" in available_classes:
bracket_test_cases.extend([
("Hex[FF]", 'Hex("FF")', "bracket_transform"),
("Hex[255]", 'Hex("255")', "bracket_transform"),
])
if "IP4" in available_classes:
bracket_test_cases.extend([
("IP4[192.168.1.1/24]", 'IP4("192.168.1.1/24")', "bracket_transform"),
("IP4[192.168.1.1;24]", 'IP4("192.168.1.1", "24")', "bracket_transform"),
])
# Combinar casos de test
all_test_cases = basic_test_cases + bracket_test_cases
print("🔍 Ejecutando casos de prueba:")
for test_input, expected_result, expected_info in all_test_cases:
result, info = parser.parse_line(test_input) result, info = parser.parse_line(test_input)
status = "" if result == expected_result and info == expected_info else "" status = "" if result == expected_result and info == expected_info else ""
print(f"{status} '{test_input}''{result}' ({info})") print(f"{status} '{test_input}''{result}' ({info})")
if result != expected_result or info != expected_info: if result != expected_result or info != expected_info:
print(f" Esperado: '{expected_result}' ({expected_info})") print(f" 💭 Esperado: '{expected_result}' ({expected_info})")
# Test de añadir/remover clases dinámicamente
print(f"\n🔧 Test modificación dinámica de clases:")
print(f" Clases iniciales: {len(parser.get_bracket_classes())}")
parser.add_bracket_class("TestClass")
print(f" Después de añadir TestClass: {len(parser.get_bracket_classes())}")
test_result = parser.test_bracket_transformation("TestClass[example]")
print(f" Test TestClass[example]: {test_result['output']}")
parser.remove_bracket_class("TestClass")
print(f" Después de remover TestClass: {len(parser.get_bracket_classes())}")
# Test recarga
print(f"\n🔄 Test recarga:")
parser.reload_bracket_classes()
print(f" Clases después de recarga: {len(parser.get_bracket_classes())}")
if __name__ == "__main__": if __name__ == "__main__":
test_bracket_parser() test_bracket_parser_with_types()

299
type_registry.py Normal file
View File

@ -0,0 +1,299 @@
"""
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] = []
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 encontradas")
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()
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)
# Registrar función Helper si existe
if hasattr(class_obj, 'Helper') and callable(class_obj.Helper):
self.helper_functions.append(class_obj.Helper)
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(),
'class_count': len(self.registered_classes),
'bracket_count': len(self.bracket_classes)
}
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()
# 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 create_types_directory_structure():
"""
Crea la estructura básica del directorio de tipos
"""
types_dir = Path("custom_types")
types_dir.mkdir(exist_ok=True)
# Crear __init__.py
init_file = types_dir / "__init__.py"
if not init_file.exists():
init_file.write_text('''"""
Directorio de tipos personalizados para la Calculadora MAV
Cada archivo *_type.py en este directorio debe definir:
def register_classes_in_module():
"""Devuelve una lista de clases definidas en este módulo para ser registradas."""
return [
("NombrePublico", ClaseObjeto, "SympyClassBase", {"add_lowercase": True}),
# ... más clases
]
Categorías soportadas:
- "ClassBase": Clase base simple
- "SympyClassBase": Clase híbrida con SymPy
Opciones disponibles:
- "add_lowercase": bool - Añadir versión en minúsculas al contexto
- "supports_brackets": bool - Forzar soporte de sintaxis con corchetes
"""
''')
return types_dir
# Función de testing
def test_type_registry():
"""Test del sistema de registro de tipos"""
print("🧪 Test Type Registry System")
print("=" * 50)
# Crear estructura si no existe
types_dir = create_types_directory_structure()
print(f"📁 Directorio de tipos: {types_dir.absolute()}")
# Crear archivo de ejemplo si no existe
example_file = types_dir / "example_type.py"
if not example_file.exists():
example_content = '''"""
Ejemplo de archivo de tipo personalizado
"""
from class_base import ClassBase
class ExampleClass(ClassBase):
def __init__(self, value):
super().__init__(value, str(value))
@staticmethod
def Helper(input_str):
if "Example" in input_str:
return "Ej: Example[test]"
return None
@staticmethod
def PopupFunctionList():
return [("test_method", "Método de prueba")]
def register_classes_in_module():
"""Devuelve clases para registro"""
return [
("Example", ExampleClass, "ClassBase", {"add_lowercase": True}),
]
'''
example_file.write_text(example_content)
print(f"📝 Archivo de ejemplo creado: {example_file}")
# Probar descubrimiento
try:
registry_info = discover_and_register_types()
print(f"✅ Descubrimiento completado:")
print(f" 📦 Clases registradas: {registry_info['class_count']}")
print(f" 🔧 Clases con brackets: {registry_info['bracket_count']}")
print(f" 📋 Contexto base: {len(registry_info['base_context'])} entradas")
print("\n📋 Clases encontradas:")
for name, cls in registry_info['registered_classes'].items():
print(f"{name}: {cls.__name__}")
print("\n🔧 Clases con sintaxis de corchetes:")
for name in registry_info['bracket_classes']:
print(f"{name}")
return True
except Exception as e:
print(f"❌ Error en test: {e}")
import traceback
traceback.print_exc()
return False
if __name__ == "__main__":
test_type_registry()