diff --git a/bin_type.py b/custom_types/bin_type.py similarity index 75% rename from bin_type.py rename to custom_types/bin_type.py index c887f8f..d005665 100644 --- a/bin_type.py +++ b/custom_types/bin_type.py @@ -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 import re @@ -52,4 +52,19 @@ class Class_Bin(SympyClassBase): def toDecimal(self): """Convierte a decimal""" - return self._value \ No newline at end of file + 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" + }), + ] \ No newline at end of file diff --git a/chr_type.py b/custom_types/chr_type.py similarity index 72% rename from chr_type.py rename to custom_types/chr_type.py index 78bbe8e..a5b7020 100644 --- a/chr_type.py +++ b/custom_types/chr_type.py @@ -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 import re @@ -63,4 +64,23 @@ class Class_Chr(SympyClassBase): def toBin(self): """Convierte a binario""" - return f"0b{self._value:08b}" \ No newline at end of file + 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" + }), + ] + diff --git a/dec_type.py b/custom_types/dec_type.py similarity index 77% rename from dec_type.py rename to custom_types/dec_type.py index d231999..c8bfd57 100644 --- a/dec_type.py +++ b/custom_types/dec_type.py @@ -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 import re @@ -53,4 +53,18 @@ class Class_Dec(SympyClassBase): def toBin(self): """Convierte a binario""" - return f"0b{self._value:08b}" \ No newline at end of file + 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" + }), + ] \ No newline at end of file diff --git a/hex_type.py b/custom_types/hex_type.py similarity index 77% rename from hex_type.py rename to custom_types/hex_type.py index 2bd3883..4edbff4 100644 --- a/hex_type.py +++ b/custom_types/hex_type.py @@ -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 import re @@ -52,4 +52,18 @@ class Class_Hex(SympyClassBase): def toDecimal(self): """Convierte a decimal""" - return self._value \ No newline at end of file + 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" + }), + ] \ No newline at end of file diff --git a/ip4_type.py b/custom_types/ip4_type.py similarity index 96% rename from ip4_type.py rename to custom_types/ip4_type.py index 6ada040..c5a33dc 100644 --- a/ip4_type.py +++ b/custom_types/ip4_type.py @@ -875,4 +875,42 @@ class Class_IP4(SympyClassBase): "private_count": private_count, "public_count": len(ip_list) - private_count, "common_network": Class_IP4.find_common_network(ip_list) - } \ No newline at end of file + } + + + +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]" + ] + } \ No newline at end of file diff --git a/main_calc_app.py b/main_calc_app.py index 803e96d..c4e0d6b 100644 --- a/main_calc_app.py +++ b/main_calc_app.py @@ -1,44 +1,32 @@ """ Calculadora MAV CAS Híbrida - Aplicación principal +VERSIÓN ADAPTADA AL NUEVO SISTEMA DE TIPOS """ import tkinter as tk from tkinter import scrolledtext, messagebox, Menu, filedialog import tkinter.font as tkFont import json import os -from pathlib import Path # Added for robust path handling +from pathlib import Path import threading from typing import List, Dict, Any, Optional 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 sympy_Base import SympyClassBase -from tl_popup import InteractiveResultManager, PlotResult # <--- Asegurar que PlotResult se importa -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 +from tl_popup import InteractiveResultManager, PlotResult +from type_registry import get_registered_helper_functions, get_registered_base_context import sympy from sympy_helper import SympyTools as SympyHelper 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" 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): self.root = root self.root.title("Calculadora MAV - CAS Híbrido") @@ -51,10 +39,13 @@ class HybridCalculatorApp: # Configurar ícono self._setup_icon() - # Componentes principales - self.engine = HybridEvaluationEngine() + # ========== COMPONENTES PRINCIPALES CON NUEVO SISTEMA ========== + self.engine = HybridEvaluationEngine(auto_discover_types=True) 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 self._debounce_job = None self._syncing_yview = False @@ -69,25 +60,85 @@ class HybridCalculatorApp: # Configurar eventos de cierre 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): """Configura el ícono de la aplicación""" try: - # Construct path relative to this script file (main_calc_app.py) script_dir = Path(__file__).resolve().parent icon_path = script_dir / "icon.png" if not icon_path.is_file(): print(f"Advertencia: Archivo de ícono no encontrado en '{icon_path}'.") - # Optionally, set a default Tk icon or simply return return self.app_icon = tk.PhotoImage(file=str(icon_path)) self.root.iconphoto(True, self.app_icon) 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}") 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}") def _load_settings(self) -> Dict[str, Any]: @@ -222,7 +273,15 @@ class HybridCalculatorApp: cas_menu.add_separator() 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) menubar.add_cascade(label="Ayuda", menu=help_menu) 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("info", foreground="#ffcb6b") 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") - # 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("bin", foreground="#4fc3f7") self.output_text.tag_configure("ip", foreground="#fff176") self.output_text.tag_configure("date", foreground="#ff8a80") 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")) def on_key_release(self, event=None): @@ -285,7 +344,7 @@ class HybridCalculatorApp: if 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: self._handle_dot_autocomplete() @@ -293,7 +352,7 @@ class HybridCalculatorApp: self._debounce_job = self.root.after(300, self._evaluate_and_update) 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() cursor_index_str = self.input_text.index(tk.INSERT) 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() - # 1. Determinar si es un popup GLOBAL + # 1. Determinar si es un popup GLOBAL (usando contexto dinámico) if not stripped_text_before_dot: print("DEBUG: Dot on empty line or after spaces. Offering global suggestions.") suggestions = [] - # MODIFIED: Get suggestions from HybridEvaluationEngine's base_context - if hasattr(self.engine, 'base_context') and isinstance(self.engine.base_context, dict): - for name, class_or_func in self.engine.base_context.items(): - # 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). - if name[0].isupper(): # Prioritize capitalized names for classes/main functions + # ========== USAR CONTEXTO DINÁMICO DEL REGISTRO ========== + try: + dynamic_context = get_registered_base_context() + + for name, class_or_func in dynamic_context.items(): + if name[0].isupper(): # Prioritizar nombres capitalizados hint = f"Tipo o función: {name}" if hasattr(class_or_func, '__doc__') and class_or_func.__doc__: first_line_doc = class_or_func.__doc__.strip().split('\n')[0] hint = f"{name} - {first_line_doc}" - elif hasattr(class_or_func, 'Helper'): # Usar Helper si está disponible - # Para obtener un hint del Helper, necesitamos llamarlo. - # Algunas clases Helper esperan el nombre de la clase. + elif hasattr(class_or_func, 'Helper'): try: - helper_text = class_or_func.Helper(name) # Pasar el nombre de la clase + helper_text = class_or_func.Helper(name) 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: print(f"DEBUG: Error calling Helper for {name}: {e_helper}") - pass # Mantener el hint genérico + pass suggestions.append((name, hint)) + + except Exception as e: + 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 (si no están ya en base_context de forma similar) - # Considerar si SympyHelper.PopupFunctionList() devuelve cosas ya cubiertas. + # Añadir funciones de SympyHelper try: sympy_functions = SympyHelper.PopupFunctionList() if sympy_functions: - # Evitar duplicados si los nombres ya están de base_context current_suggestion_names = {s[0] for s in suggestions} for fname, fhint in sympy_functions: if fname not in current_suggestion_names: @@ -350,25 +410,19 @@ class HybridCalculatorApp: print(f"DEBUG: Error calling SympyHelper.PopupFunctionList() for global: {e}") if suggestions: - # Ordenar alfabéticamente para consistencia suggestions.sort(key=lambda x: x[0]) self._show_autocomplete_popup(suggestions, is_global_popup=True) return - # 2. Es un popup de OBJETO. Extraer la expresión del objeto. + # 2. Es un popup de OBJETO 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_]*)(?:\[[^\]]*\])?)*)$" match = re.search(obj_expr_regex, stripped_text_before_dot) 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: - # 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 - # Validación simple para evitar evaluar cosas que claramente no son objetos if not obj_expr_str_candidate or \ not re.match(r"^[a-zA-Z_0-9\(\)\[\]\.\"\'\+\-\*\/ ]*$", obj_expr_str_candidate) or \ obj_expr_str_candidate.endswith(("+", "-", "*", "/", "(", ",")): @@ -378,7 +432,7 @@ class HybridCalculatorApp: obj_expr_str = obj_expr_str_candidate 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.") return @@ -395,20 +449,18 @@ class HybridCalculatorApp: print(f"DEBUG: Error calling SympyHelper.PopupFunctionList(): {e}") return - # 4. Preprocesar con BracketParser para sintaxis Clase[arg] y metodo[] - # Es importante transformar obj_expr_str ANTES de pasarlo a eval(). - if '[' in obj_expr_str: # Optimización: solo llamar si hay corchetes + # 4. Preprocesar con BracketParser + if '[' in 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) if obj_expr_str != original_for_debug: print(f"DEBUG: Preprocessed by BracketParser: '{original_for_debug}' -> '{obj_expr_str}'") - # 5. Evaluar la expresión del objeto - eval_context = self.engine._get_full_context() if hasattr(self.engine, '_get_full_context') else {} + # 5. Evaluar la expresión del objeto (usando contexto dinámico) + eval_context = self.engine._get_full_context() obj = None 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.") return print(f"DEBUG: Attempting to eval: '{obj_expr_str}'") @@ -423,11 +475,9 @@ class HybridCalculatorApp: methods = obj.PopupFunctionList() if methods: 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): - # suggestions: lista de tuplas (nombre, hint) + """Muestra popup de autocompletado (sin cambios)""" cursor_bbox = self.input_text.bbox(tk.INSERT) if not cursor_bbox: return @@ -449,35 +499,29 @@ class HybridCalculatorApp: if suggestions: self._autocomplete_listbox.select_set(0) self._autocomplete_listbox.pack(expand=True, fill=tk.BOTH) - self._autocomplete_listbox.bind("", self._on_autocomplete_select) - self._autocomplete_listbox.bind("", self._on_autocomplete_select) + self._autocomplete_listbox.bind("", lambda e, g=is_global_popup: self._on_autocomplete_select(e, is_global=g)) + self._autocomplete_listbox.bind("", lambda e, g=is_global_popup: self._on_autocomplete_select(e, is_global=g)) self._autocomplete_listbox.bind("", lambda e: self._close_autocomplete_popup()) self._autocomplete_listbox.bind("", lambda e, g=is_global_popup: self._on_autocomplete_select(e, is_global=g)) self._autocomplete_listbox.focus_set() self._autocomplete_listbox.bind("", lambda e: self._navigate_autocomplete(e, -1)) self._autocomplete_listbox.bind("", lambda e: self._navigate_autocomplete(e, 1)) - # self.input_text.bind("", lambda e: self._close_autocomplete_popup(), add=True) # Removed: Caused popup to close immediately self.input_text.bind("", lambda e: self._close_autocomplete_popup(), add=True) self.root.bind("", lambda e: self._close_autocomplete_popup(), add=True) - # self.input_text.bind("", 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("", lambda e, g=is_global_popup: self._on_autocomplete_select(e, is_global=g)) - self._autocomplete_listbox.bind("", 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 width = max(15, min(max_len + 10, 50)) 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] 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) else: self._close_autocomplete_popup() def _navigate_autocomplete(self, event, direction): + """Navegación en autocomplete (sin cambios)""" if not hasattr(self, '_autocomplete_listbox') or not self._autocomplete_listbox: return "break" current_selection = self._autocomplete_listbox.curselection() @@ -495,6 +539,7 @@ class HybridCalculatorApp: return "break" 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: return "break" selection = self._autocomplete_listbox.curselection() @@ -503,49 +548,31 @@ class HybridCalculatorApp: return "break" 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() if is_global: - # Eliminar el punto que activó el popup y luego insertar el nombre - cursor_pos_str = self.input_text.index(tk.INSERT) # Posición actual (después del punto) + cursor_pos_str = self.input_text.index(tk.INSERT) 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_index_str = f"{line_num}.{dot_pos_on_line}" - self.input_text.delete(dot_index_str) - - # Insertar el nombre de la función/clase seguido de "()" insert_text = item_name + "()" 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") else: - # Comportamiento existente para métodos de objeto 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.input_text.focus_set() - self.on_key_release() # Trigger re-evaluation + self.on_key_release() return "break" def _close_autocomplete_popup(self): + """Cierra popup de autocomplete (sin cambios)""" if hasattr(self, '_autocomplete_popup') and self._autocomplete_popup: self._autocomplete_popup.destroy() 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("", self._input_text_b1_bind_id) - # del self._input_text_b1_bind_id - # if hasattr(self, '_root_b1_bind_id'): - # self.root.unbind("", self._root_b1_bind_id) - # del self._root_b1_bind_id - if hasattr(self, '_autocomplete_listbox') and self._autocomplete_listbox: self._autocomplete_listbox = None @@ -592,7 +619,6 @@ class HybridCalculatorApp: if result.is_error: ayuda = self.obtener_ayuda(result.original_line) if ayuda: - # Mostrar ayuda en un solo renglón, truncando si es necesario ayuda_linea = ayuda.replace("\n", " ").replace("\r", " ") if len(ayuda_linea) > 120: ayuda_linea = ayuda_linea[:117] + "..." @@ -608,8 +634,8 @@ class HybridCalculatorApp: else: # Resultado normal if result.result is not None: - # Determinar tag basado en tipo - tag = self._get_result_tag(result.result) + # Determinar tag basado en tipo (DINÁMICO) + tag = self._get_result_tag_dynamic(result.result) # Verificar si es resultado interactivo if self.interactive_manager and result.is_interactive: @@ -623,90 +649,122 @@ class HybridCalculatorApp: # Añadir pista de clase para el resultado principal primary_result_object = result.result - if not isinstance(primary_result_object, PlotResult): # PlotResult ya tiene su propio formato - class_display_name = "" - 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 not isinstance(primary_result_object, PlotResult): + class_display_name = self._get_class_display_name_dynamic(primary_result_object) if class_display_name: output_parts.append(("class_hint", f"[{class_display_name}]")) # Mostrar evaluación numérica si existe 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 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 - def _get_result_tag(self, result: Any) -> str: - """Determina el tag de color para un resultado""" - if isinstance(result, Class_Hex): - return "hex" - elif isinstance(result, Class_Bin): - return "bin" - elif isinstance(result, Class_IP4): - return "ip" - elif isinstance(result, Class_Chr): - return "chr_type" + def _get_result_tag_dynamic(self, result: Any) -> str: + """Determina el tag de color para un resultado - VERSIÓN DINÁMICA""" + # Obtener clases registradas dinámicamente + try: + registered_classes = self.engine.get_available_types().get('registered_classes', {}) + + # Verificar si es una instancia de alguna clase registrada + for name, cls in registered_classes.items(): + if isinstance(result, cls): + # 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): return "symbolic" else: 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]]): - """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.delete("1.0", tk.END) 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] == ""): pass else: - # Mostrar partes de la línea for part_idx, (tag, content) in enumerate(line_parts): - if not content: # Omitir contenido vacío + if not content: continue - # Determinar si se necesita un separador antes de esta parte if part_idx > 0: 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: 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: 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) - # Añadir nueva línea excepto para la última línea if line_idx < len(output_data) - 1: self.output_text.insert(tk.END, "\n") @@ -753,7 +811,8 @@ class HybridCalculatorApp: finally: 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): """Inicia nueva sesión""" self.clear_input() @@ -838,16 +897,42 @@ class HybridCalculatorApp: self.root.clipboard_append(content) def insert_example(self): - """Inserta código de ejemplo""" - example = """# Calculadora MAV - CAS Híbrido + """Inserta código de ejemplo - ACTUALIZADO CON TIPOS DINÁMICOS""" + # 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 -# 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 diff(x**2 + sin(x), x) integrate(x**2, x) @@ -870,6 +955,7 @@ plot(sin(x), (x, -2*pi, 2*pi)) # Matrices Matrix([[1, 2], [3, 4]]) """ + self.input_text.delete("1.0", tk.END) self.input_text.insert("1.0", example) self._evaluate_and_update() @@ -963,13 +1049,64 @@ Matrix([[1, 2], [3, 4]]) except Exception as 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): - """Muestra guía rápida""" + """Muestra guía rápida - ACTUALIZADA""" 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 -- IP4[192.168.1.1/24] en lugar de IP4("192.168.1.1/24") -- Hex[FF], Bin[1010], Dec[10.5], Chr[A] +- Sintaxis: Tipo[valor] en lugar de Tipo("valor") +- Ejemplos: Hex[FF], Bin[1010], Dec[10.5], Chr[A] +- Use menú Tipos → Información de tipos para ver tipos disponibles ## Ecuaciones Automáticas - x**2 + 2*x = 8 (detectado automáticamente) @@ -990,26 +1127,29 @@ Matrix([[1, 2], [3, 4]]) - Todas las variables son símbolos SymPy - x = 5 crea Symbol('x') con valor 5 - 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) def show_syntax_help(self): - """Muestra ayuda de sintaxis""" + """Muestra ayuda de sintaxis - ACTUALIZADA""" syntax = """# Sintaxis del CAS Híbrido -## Clases Especializadas (solo corchetes) -IP4[dirección/prefijo] # IP4[192.168.1.1/24] -Hex[valor] # Hex[FF], Hex[255] -Bin[valor] # Bin[1010], Bin[10] -Dec[valor] # Dec[10.5], Dec[10] -Chr[carácter] # Chr[A], Chr[Hello] +## Sistema de Tipos Dinámico +Los tipos se detectan automáticamente desde custom_types/ +Use menú Tipos → Información de tipos para ver tipos disponibles -## Métodos Disponibles -IP4[...].NetworkAddress[] -IP4[...].BroadcastAddress[] -IP4[...].Nodes() -Hex[...].toDecimal() +## Sintaxis con Corchetes (Dinámica) +Tipo[valor] # Sintaxis general +Tipo[arg1; arg2] # Múltiples argumentos + +## 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) 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) def show_sympy_functions(self): - """Muestra funciones SymPy disponibles""" + """Muestra funciones SymPy disponibles (sin cambios)""" functions = """# Funciones SymPy Disponibles ## Matemáticas Básicas @@ -1066,20 +1206,28 @@ pi, E, I (imaginario), oo (infinito) self._show_help_window("Funciones SymPy", functions) 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 -Versión: 2.0 -Motor: SymPy + Clases Especializadas +Versión: 2.1 (Sistema de Tipos Dinámico) +Motor: SymPy + Auto-descubrimiento de Tipos Características: • Motor algebraico completo (SymPy) +• Sistema de tipos dinámico y extensible • Sintaxis simplificada con corchetes • Detección automática de ecuaciones • Resultados interactivos clickeables -• Tipos especializados (IP4, Hex, Bin, etc.) +• Auto-descubrimiento de tipos en custom_types/ • Variables SymPy puras • 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 con soporte especializado para redes, @@ -1147,10 +1295,15 @@ programación y análisis numérico. self.root.destroy() def obtener_ayuda(self, input_str): + """Obtiene ayuda usando helpers dinámicos""" for helper in self.HELPERS: - ayuda = helper(input_str) - if ayuda: - return ayuda + try: + ayuda = helper(input_str) + if ayuda: + return ayuda + except Exception as e: + print(f"DEBUG: Error en helper: {e}") + continue return None @@ -1168,4 +1321,4 @@ def main(): if __name__ == "__main__": - main() + main() \ No newline at end of file diff --git a/main_evaluation.py b/main_evaluation.py index 696a8fb..95372bc 100644 --- a/main_evaluation.py +++ b/main_evaluation.py @@ -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 from sympy import symbols, Symbol, sympify, solve, Eq, simplify @@ -8,36 +8,64 @@ import ast import re 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_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: """ 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.symbol_table: Dict[str, Any] = {} self.equations: List[sympy.Eq] = [] self.last_result = None - # Contexto base con funciones y clases - self._setup_base_context() + # Configuración del sistema de tipos + 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 self.debug = False + # Configurar contexto base + self._setup_base_context() + + def _setup_base_context(self): """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 = { 'pi': sympy.pi, 'e': sympy.E, @@ -83,24 +111,10 @@ class HybridEvaluationEngine: 'plot3d': self._create_plot3d_placeholder, } - # Clases especializadas - specialized_classes = { - '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, - } + # 3. CLASES ESPECIALIZADAS (DESDE AUTO-DESCUBRIMIENTO) + specialized_classes = self.registered_types_info.get('base_context', {}) - # Funciones de utilidad + # 4. FUNCIONES DE UTILIDAD utility_functions = { '_add_equation': self._add_equation, '_assign_variable': self._assign_variable, @@ -108,11 +122,64 @@ class HybridEvaluationEngine: '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 = { **math_functions, **specialized_classes, **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): """Crear placeholder para plots que será manejado por resultados interactivos""" @@ -127,16 +194,30 @@ class HybridEvaluationEngine: return PlotResult('plot3d', args, kwargs) 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: return "Ayuda disponible. Use help(función) para ayuda específica." + # Primero intentar con el objeto directamente if hasattr(obj, '__doc__') and obj.__doc__: return obj.__doc__ elif hasattr(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': """ @@ -316,7 +397,7 @@ class HybridEvaluationEngine: result = eval(expression, {"__builtins__": {}}, context) # 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 elif isinstance(result, PlotResult): if self.debug: @@ -488,55 +569,59 @@ class EvaluationResult: # Funciones de testing -def test_evaluation_engine(): - """Test del motor de evaluación""" - engine = HybridEvaluationEngine() +def test_hybrid_engine_with_types(): + """Test del motor de evaluación con sistema de tipos""" + print("🧪 Test HybridEvaluationEngine con Type Registry") + print("=" * 60) + + # Crear motor con auto-descubrimiento + engine = HybridEvaluationEngine(auto_discover_types=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 = [ # Expresiones básicas "2 + 3", "x + 2", "sin(pi/2)", - # Sintaxis con corchetes - "Hex[FF]", - "IP4[192.168.1.1/24]", + # Tipo Chr si está disponible + "Chr[A]" if "Chr" in types_info['registered_classes'] else "# Chr no disponible", - # 🧪 PLOTS - Casos específicos para testing - "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 + # Variables y ecuaciones "a = 10", - "b = a + 5", + "x + 2 = 5", + "solve(x + 2 - 5, x)", # Funciones avanzadas "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: + if test.startswith('#'): + print(f"⏭️ {test}") + continue + result = engine.evaluate_line(test) - print(f"'{test}' → {result} (type: {result.result_type})") - - # 🔍 Información adicional para plots - if 'plot' in test: - print(f" 🎯 Es interactivo: {result.is_interactive}") - if isinstance(result.result, PlotResult): - print(f" 📊 PlotResult confirmado: {result.result.plot_type}") + print(f"✅ '{test}' → {result} (type: {result.result_type})") 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__": - test_evaluation_engine() + test_hybrid_engine_with_types() diff --git a/tl_bracket_parser.py b/tl_bracket_parser.py index 51502fb..a7ff37a 100644 --- a/tl_bracket_parser.py +++ b/tl_bracket_parser.py @@ -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 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: """Parser que convierte sintaxis con corchetes y detecta ecuaciones contextualmente""" - # Clases que soportan sintaxis con corchetes - BRACKET_CLASSES = {'IP4', 'Hex', 'Bin', 'Date', 'Dec', 'Chr', 'IP4Mask'} + # Clases base que soportan sintaxis con corchetes (fallback) + DEFAULT_BRACKET_CLASSES = {'IP4', 'Hex', 'Bin', 'Date', 'Dec', 'Chr', 'IP4Mask'} # Operadores de comparación que pueden formar ecuaciones EQUATION_OPERATORS = {'==', '!=', '<', '<=', '>', '>=', '='} - def __init__(self): + def __init__(self, use_type_registry: bool = True): 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]: """ @@ -144,9 +197,19 @@ class BracketParser: def _transform_brackets(self, line: str) -> str: """ Transforma sintaxis Class[args] → Class("args") y maneja métodos + VERSIÓN DINÁMICA que usa las clases registradas """ - # Pattern principal: ClassName[contenido] - pattern = r'(\b(?:' + '|'.join(self.BRACKET_CLASSES) + r')\b)\[([^\]]*)\]' + # Crear pattern dinámicamente basado en clases registradas + 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): class_name = match.group(1) @@ -169,6 +232,7 @@ class BracketParser: processed_args.append(f'"{escaped_arg}"') return f'{class_name}({", ".join(processed_args)})' + # Aplicar transformación repetidamente hasta que no haya más cambios transformed = line while True: @@ -182,6 +246,25 @@ class BracketParser: transformed = re.sub(method_pattern, r'.\1()', 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: @@ -227,20 +310,20 @@ class EquationDetector: # Funciones de utilidad para testing -def test_bracket_parser(): - """Función de testing para el bracket parser""" - parser = BracketParser() +def test_bracket_parser_with_types(): + """Función de testing para el bracket parser con tipos""" + print("🧪 Test BracketParser con Type Registry") + print("=" * 50) + + # Crear parser con registro de tipos + parser = BracketParser(use_type_registry=True) parser.debug = True - test_cases = [ - # Sintaxis con corchetes - ("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"), - + print(f"🔧 Clases de corchetes disponibles: {parser.get_bracket_classes()}") + print() + + # Test casos básicos (que funcionen sin importar qué tipos estén disponibles) + basic_test_cases = [ # Atajos solve ("x=?", "solve(x)", "solve_shortcut"), ("variable_name=?", "solve(variable_name)", "solve_shortcut"), @@ -248,32 +331,67 @@ def test_bracket_parser(): # Asignaciones ("z = 5", '_assign_variable("z", 5)', "assignment"), ("w = z**2 + 3", '_assign_variable("w", z**2 + 3)', "assignment"), - ("result = Hex[FF]", '_assign_variable("result", Hex("FF"))', "assignment"), # Ecuaciones standalone ("x + 2 = 5", '_add_equation("x + 2 = 5")', "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 ("x + 2*y", "x + 2*y", "expression"), ("diff(x**2, x)", "diff(x**2, x)", "expression"), ] - print("=== Test Bracket Parser ===") - for test_input, expected_result, expected_info in test_cases: + # Test casos de sintaxis con corchetes (dinámico) + 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) status = "✅" if result == expected_result and info == expected_info else "❌" print(f"{status} '{test_input}' → '{result}' ({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__": - test_bracket_parser() + test_bracket_parser_with_types() \ No newline at end of file diff --git a/type_registry.py b/type_registry.py new file mode 100644 index 0000000..e44456c --- /dev/null +++ b/type_registry.py @@ -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()