Implementación de un sistema de logging para mejorar la gestión de advertencias y errores en la aplicación. Se establece un límite para la cantidad de archivos de log generados y se optimiza la configuración de la ventana. Se ajusta el manejo de la evaluación y el autocompletado, mejorando la experiencia del usuario.

This commit is contained in:
Miguel 2025-06-04 21:38:40 +02:00
parent 589bab03b2
commit 0c7ed33d0d
6 changed files with 1448 additions and 54 deletions

1262
.doc/OLD_calc.py Normal file

File diff suppressed because it is too large Load Diff

15
calc.py
View File

@ -19,6 +19,7 @@ import platform
def setup_logging():
"""Configura el sistema de logging completo"""
MAX_LOG_FILES = 10 # Límite de archivos de log
log_dir = Path("logs")
log_dir.mkdir(exist_ok=True)
@ -26,6 +27,20 @@ def setup_logging():
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
log_file = log_dir / f"mav_calc_{timestamp}.log"
# Eliminar logs antiguos si se supera el límite
try:
existing_logs = sorted(
[f for f in log_dir.glob("mav_calc_*.log") if f.is_file()],
key=os.path.getmtime
)
if len(existing_logs) >= MAX_LOG_FILES:
logs_to_delete = existing_logs[:len(existing_logs) - MAX_LOG_FILES + 1]
for old_log in logs_to_delete:
old_log.unlink()
logging.info(f"Eliminado log antiguo: {old_log}")
except Exception as e:
logging.warning(f"No se pudieron eliminar logs antiguos: {e}")
# Configurar logging
logging.basicConfig(
level=logging.DEBUG,

View File

@ -1,10 +1,5 @@
a = 10 + b
a=?
x=12
a=x
x=2
b=x
a
b

View File

@ -1,6 +1,6 @@
{
"window_geometry": "1020x700+144+161",
"sash_pos_x": 355,
"window_geometry": "1020x700+467+199",
"sash_pos_x": 363,
"symbolic_mode": true,
"show_numeric_approximation": true,
"keep_symbolic_fractions": true,

View File

@ -6,6 +6,7 @@ import tkinter as tk
from tkinter import scrolledtext, messagebox, Menu, filedialog
import tkinter.font as tkFont
import json
import logging # <--- AÑADIDO
import os
from pathlib import Path
import threading
@ -35,10 +36,12 @@ except ImportError:
except ImportError:
HTML_VIEWER_TYPE = None
# Usar logging para estas advertencias iniciales
module_logger = logging.getLogger(__name__)
if not MARKDOWN_AVAILABLE:
print("Advertencia: La librería 'markdown' no está instalada. La ayuda se mostrará en texto plano.")
module_logger.warning("La librería 'markdown' no está instalada. La ayuda se mostrará en texto plano.")
if MARKDOWN_AVAILABLE and HTML_VIEWER_TYPE is None:
print("Advertencia: 'markdown' está disponible, pero no se encontró un visor HTML (tkinterweb/tkhtmlview). La ayuda se mostrará en texto plano.")
module_logger.warning("'markdown' está disponible, pero no se encontró un visor HTML (tkinterweb/tkhtmlview). La ayuda se mostrará en texto plano.")
# ========== IMPORTS ADAPTADOS AL NUEVO SISTEMA ==========
# Importar componentes del CAS híbrido con nuevo sistema de tipos
@ -58,6 +61,7 @@ class HybridCalculatorApp:
def __init__(self, root: tk.Tk):
self.root = root
self.logger = logging.getLogger(__name__) # <--- AÑADIDO: Logger para la instancia
self.root.title("Calculadora MAV - CAS Híbrido")
# Configuración y estado
@ -121,34 +125,35 @@ class HybridCalculatorApp:
# Obtener helpers registrados dinámicamente
self.HELPERS = get_registered_helper_functions()
# Añadir SympyHelper al final
# Añadir SympyHelper.Helper al final
self.HELPERS.append(SympyHelper.Helper)
print(f"🆘 Helpers dinámicos cargados: {len(self.HELPERS)}")
# Usar logger en lugar de print, y sin emoji para la consola
self.logger.info(f"Helpers dinámicos cargados: {len(self.HELPERS)}") # Original: 🆘
except Exception as e:
print(f"⚠️ Error cargando helpers dinámicos: {e}")
# Usar logger en lugar de print, y sin emoji para la consola
self.logger.error(f"Error cargando helpers dinámicos: {e}", exc_info=True) # Original: ⚠️
# 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...")
self.logger.info("Recargando sistema de tipos...") # Original: 🔄
# 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")
self.logger.info("Sistema de tipos recargado.") # Original: ✅
except Exception as e:
print(f"Error recargando tipos: {e}")
self.logger.error(f"Error recargando tipos: {e}", exc_info=True) # Original: ❌
messagebox.showerror("Error", f"Error recargando tipos:\n{e}")
def show_types_info(self):
@ -186,15 +191,15 @@ CLASES DISPONIBLES:
icon_path = script_dir / "icon.png"
if not icon_path.is_file():
print(f"Advertencia: Archivo de ícono no encontrado en '{icon_path}'.")
self.logger.warning(f"Archivo de ícono no encontrado en '{icon_path}'.")
return
self.app_icon = tk.PhotoImage(file=str(icon_path))
self.root.iconphoto(True, self.app_icon)
except tk.TclError as e:
print(f"Advertencia: No se pudo cargar el ícono desde '{icon_path}'. Error de Tkinter: {e}")
self.logger.warning(f"No se pudo cargar el ícono desde '{icon_path}'. Error de Tkinter: {e}")
except Exception as e:
print(f"Advertencia: Ocurrió un error inesperado al cargar el ícono desde '{icon_path}': {e}")
self.logger.warning(f"Ocurrió un error inesperado al cargar el ícono desde '{icon_path}': {e}", exc_info=True)
def _load_settings(self) -> Dict[str, Any]:
"""Carga configuración de la aplicación"""
@ -221,7 +226,7 @@ CLASES DISPONIBLES:
json.dump(self.settings, f, indent=4, ensure_ascii=False)
except Exception as e:
if self.debug:
print(f"Error guardando configuración: {e}")
self.logger.error(f"Error guardando configuración: {e}", exc_info=True)
def update_symbolic_settings(self, symbolic_mode=None, show_numeric=None,
keep_fractions=None, auto_simplify=None):
@ -444,8 +449,8 @@ CLASES DISPONIBLES:
if event and event.char == '.' and self.input_text.focus_get() == self.input_text:
self._handle_dot_autocomplete()
# Evaluación con debounce
self._debounce_job = self.root.after(300, self._evaluate_and_update)
# Evaluación con debounce y auto-dimensionado
self._debounce_job = self.root.after(300, self._process_input_and_adjust_layout)
def _handle_dot_autocomplete(self):
"""Maneja el autocompletado cuando se escribe un punto - VERSIÓN DINÁMICA"""
@ -456,7 +461,7 @@ CLASES DISPONIBLES:
char_idx_after_dot = int(char_num_str)
if char_idx_after_dot == 0:
print("DEBUG: Autocomplete: Cursor at beginning of line after dot. No action.")
self.logger.debug("Autocomplete: Cursor at beginning of line after dot. No action.")
return
dot_char_index_in_line = char_idx_after_dot - 1
@ -466,7 +471,7 @@ CLASES DISPONIBLES:
# 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.")
self.logger.debug("Dot on empty line or after spaces. Offering global suggestions.")
suggestions = []
# ========== USAR CONTEXTO DINÁMICO DEL REGISTRO ==========
@ -485,12 +490,12 @@ CLASES DISPONIBLES:
if helper_text:
hint = helper_text.split('\n')[0]
except Exception as e_helper:
print(f"DEBUG: Error calling Helper for {name}: {e_helper}")
self.logger.debug(f"Error calling Helper for {name}: {e_helper}")
pass
suggestions.append((name, hint))
except Exception as e:
print(f"DEBUG: Error obteniendo contexto dinámico: {e}")
self.logger.debug(f"Error obteniendo contexto dinámico: {e}")
# Fallback básico
suggestions = [("sin", "Función seno"), ("cos", "Función coseno")]
@ -503,7 +508,7 @@ CLASES DISPONIBLES:
if fname not in current_suggestion_names:
suggestions.append((fname, fhint))
except Exception as e:
print(f"DEBUG: Error calling SympyHelper.PopupFunctionList() for global: {e}")
self.logger.debug(f"Error calling SympyHelper.PopupFunctionList() for global: {e}")
if suggestions:
suggestions.sort(key=lambda x: x[0])
@ -522,48 +527,48 @@ CLASES DISPONIBLES:
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(("+", "-", "*", "/", "(", ",")):
print(f"DEBUG: Extracted expr '{obj_expr_str_candidate}' from '{stripped_text_before_dot}' not a valid object for dot autocomplete.")
self.logger.debug(f"Extracted expr '{obj_expr_str_candidate}' from '{stripped_text_before_dot}' not a valid object for dot autocomplete.")
return
obj_expr_str = obj_expr_str_candidate
print(f"DEBUG: Autocomplete for object. Extracted: '{obj_expr_str}' from: '{text_on_line_up_to_dot}'")
self.logger.debug(f"Autocomplete for object. Extracted: '{obj_expr_str}' from: '{text_on_line_up_to_dot}'")
if not obj_expr_str:
print("DEBUG: Object expression is empty after extraction. No autocomplete.")
self.logger.debug("Object expression is empty after extraction. No autocomplete.")
return
# 3. Caso especial para el módulo sympy
if obj_expr_str == "sympy":
print(f"DEBUG: Detected 'sympy.', using SympyHelper for suggestions.")
self.logger.debug(f"Detected 'sympy.', using SympyHelper for suggestions.")
try:
methods = SympyHelper.PopupFunctionList()
if methods:
self._show_autocomplete_popup(methods, is_global_popup=False)
else:
print(f"DEBUG: SympyHelper.PopupFunctionList returned no methods.")
self.logger.debug(f"SympyHelper.PopupFunctionList returned no methods.")
except Exception as e:
print(f"DEBUG: Error calling SympyHelper.PopupFunctionList(): {e}")
self.logger.debug(f"Error calling SympyHelper.PopupFunctionList(): {e}")
return
# 4. Preprocesar con BracketParser
if '[' in obj_expr_str:
original_for_debug = obj_expr_str
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}'")
if obj_expr_str != original_for_debug and self.debug: # Solo loguear si self.debug es True
self.logger.debug(f"Preprocessed by BracketParser: '{original_for_debug}' -> '{obj_expr_str}'")
# 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():
print("DEBUG: Object expression became empty before eval. No action.")
self.logger.debug("Object expression became empty before eval. No action.")
return
print(f"DEBUG: Attempting to eval: '{obj_expr_str}'")
self.logger.debug(f"Attempting to eval: '{obj_expr_str}'")
obj = eval(obj_expr_str, eval_context)
print(f"DEBUG: Eval successful. Object: {type(obj)}, Value: {obj}")
self.logger.debug(f"Eval successful. Object: {type(obj)}, Value: {obj}")
except Exception as e:
print(f"DEBUG: Error evaluating object expression '{obj_expr_str}': {e}")
self.logger.debug(f"Error evaluating object expression '{obj_expr_str}': {e}")
return
# 6. Mostrar popup de autocompletado para el objeto
@ -793,7 +798,7 @@ CLASES DISPONIBLES:
except Exception as e:
if self.debug:
print(f"DEBUG: Error en get_result_tag_dynamic: {e}")
self.logger.debug(f"Error en get_result_tag_dynamic: {e}")
# Fallback a tags existentes para tipos no registrados
if isinstance(result, sympy.Basic):
@ -813,7 +818,7 @@ CLASES DISPONIBLES:
except Exception as e:
if self.debug:
print(f"DEBUG: Error en get_class_display_name_dynamic: {e}")
self.logger.debug(f"Error en get_class_display_name_dynamic: {e}")
# Fallback a lógica existente para tipos nativos
if isinstance(obj, sympy.logic.boolalg.BooleanAtom):
@ -933,7 +938,7 @@ CLASES DISPONIBLES:
self.input_text.delete("1.0", tk.END)
self.input_text.insert("1.0", content)
self._evaluate_and_update()
self._process_input_and_adjust_layout()
except Exception as e:
messagebox.showerror("Error", f"No se pudo cargar el archivo:\n{e}")
@ -1203,9 +1208,9 @@ programación y análisis numérico.
self.input_text.insert("1.0", content)
# Hacer evaluación inicial para mostrar resultados del historial
# Esto mantiene el comportamiento de contexto limpio pero muestra resultados
self.root.after_idle(self._evaluate_and_update)
self.root.after_idle(self._process_input_and_adjust_layout)
except Exception as e:
print(f"Error cargando historial: {e}")
self.logger.error(f"Error cargando historial: {e}", exc_info=True)
def save_history(self):
"""Guarda historial de entrada"""
@ -1217,7 +1222,7 @@ programación y análisis numérico.
elif os.path.exists(self.HISTORY_FILE):
os.remove(self.HISTORY_FILE)
except Exception as e:
print(f"Error guardando historial: {e}")
self.logger.error(f"Error guardando historial: {e}", exc_info=True)
def on_close(self):
"""Maneja cierre de la aplicación"""
@ -1336,7 +1341,7 @@ programación y análisis numérico.
html_viewer.pack(padx=0, pady=0, fill=tk.BOTH, expand=True)
except Exception as e:
print(f"Error al renderizar Markdown a HTML: {e}")
self.logger.error(f"Error al renderizar Markdown a HTML: {e}", exc_info=True)
# Fallback to text if HTML fails
self._show_text_help(help_win, readme_content)
else:
@ -1419,7 +1424,7 @@ Para crear un archivo de ayuda personalizado, cree un archivo `readme.md` en el
if ayuda:
return ayuda
except Exception as e:
print(f"DEBUG: Error en helper: {e}")
self.logger.debug(f"Error en helper: {e}")
continue
return None
@ -1460,6 +1465,123 @@ Para crear un archivo de ayuda personalizado, cree un archivo `readme.md` en el
return f"{mode}{numeric_indicator}{fractions_indicator}"
def _get_input_font(self):
"""Obtiene o crea y cachea el objeto tk.Font para el panel de entrada."""
if not self._cached_input_font:
# Asume la fuente configurada en create_widgets: ("Consolas", 11)
self._cached_input_font = tkFont.Font(family="Consolas", size=11)
return self._cached_input_font
def _adjust_input_pane_width(self):
"""Ajusta el ancho del panel de entrada según su contenido."""
if not hasattr(self, 'paned_window') or not self.paned_window.winfo_exists():
return
# Esperar a que la ventana tenga un tamaño válido
if self.paned_window.winfo_width() <= 1:
return # Se reintentará en la siguiente llamada (ej. por KeyRelease)
# Obtener contenido excluyendo el último newline automático del widget Text
input_content = self.input_text.get("1.0", f"{tk.END}-1c")
lines = input_content.splitlines()
input_font = self._get_input_font()
max_pixel_width = 0
if not input_content.strip(): # Si está vacío o solo espacios en blanco
max_pixel_width = 5 # Ancho mínimo para el cursor o como placeholder
else:
for line in lines:
measured_width = input_font.measure(line) if line.strip() else input_font.measure(" ")
if measured_width > max_pixel_width:
max_pixel_width = measured_width
padding = 40 # Relleno para barra de desplazamiento, márgenes, etc.
width_needed_by_text = max_pixel_width + padding
# Debugging opcional (descomenta si necesitas depurar)
if self.debug:
self.logger.debug(f"--- Adjusting Input Pane ---")
self.logger.debug(f"Input content: '{input_content[:50]}...'")
self.logger.debug(f"Max pixel width of text: {max_pixel_width}")
self.logger.debug(f"Width needed by text (max_pixel_width + padding): {width_needed_by_text}")
min_input_pane_width = 200 # Definido en create_widgets
min_output_pane_width = 200 # Definido en create_widgets
total_width = self.paned_window.winfo_width()
current_sash_pos = 0
try:
sash_coords = self.paned_window.sash_coord(0)
if sash_coords:
current_sash_pos = sash_coords[0]
else:
if self.debug:
self.logger.debug("Could not get sash_coord.")
return
except tk.TclError:
if self.debug:
self.logger.debug("TclError getting sash_coord.")
return
if self.debug:
self.logger.debug(f"Current sash position (input pane width): {current_sash_pos}")
if width_needed_by_text > current_sash_pos:
if self.debug:
self.logger.debug(f"Condition MET: Text needs more space ({width_needed_by_text} > {current_sash_pos})")
new_input_width = width_needed_by_text # Punto de partida
# Asegurar que el nuevo ancho no sea menor que el mínimo del panel de entrada
new_input_width = max(new_input_width, min_input_pane_width)
# Asegurar que el panel de salida conserve su espacio mínimo
if total_width - new_input_width < min_output_pane_width:
new_input_width = total_width - min_output_pane_width
new_input_width = max(new_input_width, min_input_pane_width) # Re-verificar mínimo del input
# Aplicar un ratio máximo para el panel de entrada
max_input_ratio = 0.75 # Podría ser una constante de clase
max_width_by_ratio = int(total_width * max_input_ratio)
if new_input_width > max_width_by_ratio:
if max_width_by_ratio >= min_input_pane_width and \
(total_width - max_width_by_ratio) >= min_output_pane_width:
new_input_width = max_width_by_ratio
final_new_input_width = max(0, int(new_input_width)) # No debe ser negativo
if self.debug:
self.logger.debug(f"Calculated final new input width: {final_new_input_width}")
# Mover el sash solo si el nuevo ancho es significativamente mayor que el actual (umbral ajustado)
sash_adjustment_threshold = 3 # Píxeles
if final_new_input_width > current_sash_pos and \
(final_new_input_width - current_sash_pos) >= sash_adjustment_threshold:
if self.debug:
self.logger.debug(f"Condition MET for sash_place: New width {final_new_input_width} is significantly larger (diff >= {sash_adjustment_threshold}).")
try:
if self.paned_window.winfo_exists() and total_width >= (min_input_pane_width + min_output_pane_width):
self.paned_window.sash_place(0, final_new_input_width, 0) # Añadido el argumento y=0
if self.debug:
self.logger.debug(f"Sash placed at: {final_new_input_width}")
elif self.debug:
self.logger.debug("Paned window not ready or total width too small for sash_place.")
except tk.TclError as e_sash:
if self.debug:
self.logger.debug(f"TclError during sash_place: {e_sash}")
pass
elif self.debug:
self.logger.debug(f"Condition NOT MET for sash_place: final_new_input_width ({final_new_input_width}) vs current_sash_pos ({current_sash_pos}) or threshold ({sash_adjustment_threshold}).")
elif self.debug:
self.logger.debug(f"Condition NOT MET: Text does not need more space ({width_needed_by_text} <= {current_sash_pos})")
if self.debug:
self.logger.debug(f"--- End Adjusting Input Pane ---")
def _process_input_and_adjust_layout(self):
"""Evalúa todas las líneas y luego ajusta el ancho del panel de entrada."""
self._evaluate_and_update()
self._adjust_input_pane_width()
def main():
"""Función principal"""

View File

@ -343,10 +343,10 @@ class HybridEvaluationEngine:
try:
numeric_eval = result.evalf()
# Solo mostrar evaluación numérica si es diferente del resultado simbólico
if numeric_eval != result and not (
hasattr(result, 'is_number') and result.is_number and
abs(float(numeric_eval) - float(result)) < 1e-15
):
if (str(numeric_eval) != str(result) and numeric_eval != result and
not (isinstance(result, (int, float)) or
(hasattr(result, 'is_number') and result.is_number and
hasattr(result, 'is_Integer') and result.is_Integer))):
numeric_result = numeric_eval
except:
pass