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
import re
@ -52,4 +52,19 @@ class Class_Bin(SympyClassBase):
def toDecimal(self):
"""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
import re
@ -63,4 +64,23 @@ class Class_Chr(SympyClassBase):
def toBin(self):
"""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
import re
@ -53,4 +53,18 @@ class Class_Dec(SympyClassBase):
def toBin(self):
"""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
import re
@ -52,4 +52,18 @@ class Class_Hex(SympyClassBase):
def toDecimal(self):
"""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

@ -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)
}
}
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
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("<Return>", self._on_autocomplete_select)
self._autocomplete_listbox.bind("<Tab>", 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>", 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("<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.bind("<Up>", 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.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
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("<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:
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()

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
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()

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 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()

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()