462 lines
16 KiB
Python
462 lines
16 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
Simple Debug API - Calculadora MAV CAS
|
||
|
||
API CLI simple que usa el motor de evaluación existente para generar debug traces
|
||
sin modificar el código base. Permite debuggear "como si usaras la aplicación"
|
||
mediante archivos JSON.
|
||
|
||
Uso:
|
||
python simple_debug.py debug_input.json
|
||
python simple_debug.py debug_input.json --output debug_results.json
|
||
python simple_debug.py debug_input.json --verbose
|
||
"""
|
||
|
||
import json
|
||
import sys
|
||
import argparse
|
||
from datetime import datetime
|
||
from pathlib import Path
|
||
|
||
# Importar el motor de evaluación existente
|
||
from main_evaluation import HybridEvaluationEngine
|
||
import sympy
|
||
|
||
|
||
def _process_evaluation_result_for_debug(result, engine):
|
||
"""
|
||
Procesa el resultado de evaluación tal como lo haría la aplicación
|
||
Retorna información de formato, colores y contenido exacto
|
||
"""
|
||
output_parts = []
|
||
|
||
if result.is_error:
|
||
# Intentar obtener ayuda como lo hace la app
|
||
ayuda = _obtener_ayuda_simulada(result.original_line, engine)
|
||
if ayuda:
|
||
ayuda_linea = ayuda.replace("\n", " ").replace("\r", " ")
|
||
if len(ayuda_linea) > 120:
|
||
ayuda_linea = ayuda_linea[:117] + "..."
|
||
output_parts.append(("helper", ayuda_linea))
|
||
else:
|
||
output_parts.append(("error", f"Error: {result.error}"))
|
||
elif result.result_type == "comment":
|
||
output_parts.append(("comment", result.original_line))
|
||
elif result.result_type == "equation_added":
|
||
output_parts.append(("equation", result.symbolic_result))
|
||
elif result.result_type == "assignment":
|
||
output_parts.append(("info", result.symbolic_result))
|
||
# Mostrar evaluación numérica para asignaciones si existe
|
||
if result.numeric_result is not None and result.numeric_result != result.result:
|
||
output_parts.append(("numeric", f"≈ {result.numeric_result}"))
|
||
else:
|
||
# Resultado normal
|
||
if result.result is not None:
|
||
# Determinar tag basado en tipo (simulando lógica dinámica)
|
||
tag = _get_result_tag_dynamic_debug(result.result, engine)
|
||
|
||
# Verificar si es resultado interactivo (simulado)
|
||
if _is_interactive_result(result.result):
|
||
interactive_tag, display_text = _create_interactive_tag_debug(result.result)
|
||
if interactive_tag:
|
||
output_parts.append((interactive_tag, display_text))
|
||
else:
|
||
output_parts.append((tag, str(result.result)))
|
||
else:
|
||
output_parts.append((tag, str(result.result)))
|
||
|
||
# Añadir pista de clase para el resultado principal
|
||
class_display_name = _get_class_display_name_dynamic_debug(result.result, engine)
|
||
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}"))
|
||
|
||
# Mostrar información adicional
|
||
if result.info:
|
||
output_parts.append(("info", f"({result.info})"))
|
||
|
||
# Convertir partes a string formateado como la app
|
||
formatted_output = _format_output_parts_debug(output_parts)
|
||
|
||
return {
|
||
'parts': output_parts,
|
||
'formatted_text': formatted_output,
|
||
'tag_info': _get_tag_color_info()
|
||
}
|
||
|
||
|
||
def _obtener_ayuda_simulada(input_str, engine):
|
||
"""Simula la obtención de ayuda como lo hace la app"""
|
||
try:
|
||
# Intentar usar los helpers del engine si están disponibles
|
||
if hasattr(engine, 'HELPERS'):
|
||
for helper in engine.HELPERS:
|
||
try:
|
||
ayuda = helper(input_str)
|
||
if ayuda:
|
||
return ayuda
|
||
except:
|
||
continue
|
||
except:
|
||
pass
|
||
return None
|
||
|
||
|
||
def _get_result_tag_dynamic_debug(result, engine):
|
||
"""Simula _get_result_tag_dynamic de la app"""
|
||
try:
|
||
registered_classes = 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):
|
||
name_lower = name.lower()
|
||
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"
|
||
elif name_lower == "date":
|
||
return "date"
|
||
else:
|
||
return "custom_type"
|
||
|
||
except Exception:
|
||
pass
|
||
|
||
# Fallback a tags existentes para tipos no registrados
|
||
if isinstance(result, sympy.Basic):
|
||
return "symbolic"
|
||
else:
|
||
return "result"
|
||
|
||
|
||
def _get_class_display_name_dynamic_debug(obj, engine):
|
||
"""Simula _get_class_display_name_dynamic de la app"""
|
||
try:
|
||
# Verificar si es una clase registrada dinámicamente
|
||
registered_classes = engine.get_available_types().get('registered_classes', {})
|
||
|
||
for name, cls in registered_classes.items():
|
||
if isinstance(obj, cls):
|
||
return name
|
||
|
||
except Exception:
|
||
pass
|
||
|
||
# Fallback a lógica existente para tipos nativos
|
||
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 _is_interactive_result(result):
|
||
"""Simula la lógica is_interactive de EvaluationResult"""
|
||
try:
|
||
# Intentar importar PlotResult si existe
|
||
from tl_popup import PlotResult
|
||
if isinstance(result, PlotResult):
|
||
return True
|
||
except ImportError:
|
||
pass
|
||
|
||
return isinstance(result, sympy.Matrix) or \
|
||
(isinstance(result, list) and len(result) > 3)
|
||
|
||
|
||
def _create_interactive_tag_debug(result):
|
||
"""Simula la creación de tags interactivos"""
|
||
try:
|
||
from tl_popup import PlotResult
|
||
if isinstance(result, PlotResult):
|
||
return f"plot_{id(result)}", f"📊 Ver {result.plot_type.title()}"
|
||
except ImportError:
|
||
pass
|
||
|
||
if isinstance(result, sympy.Matrix):
|
||
rows, cols = result.shape
|
||
return f"matrix_{id(result)}", f"📋 Ver Matriz {rows}×{cols}"
|
||
elif isinstance(result, list) and len(result) > 5:
|
||
return f"list_{id(result)}", f"📋 Ver Lista ({len(result)} elementos)"
|
||
elif isinstance(result, dict) and len(result) > 3:
|
||
return f"dict_{id(result)}", f"🔍 Ver Diccionario ({len(result)} entradas)"
|
||
|
||
return None, None
|
||
|
||
|
||
def _format_output_parts_debug(output_parts):
|
||
"""Simula cómo _display_output formatea las partes para mostrar"""
|
||
formatted_lines = []
|
||
|
||
for part_idx, (tag, content) in enumerate(output_parts):
|
||
if not content:
|
||
continue
|
||
|
||
prefix = ""
|
||
if part_idx > 0:
|
||
prev_tag, prev_content = output_parts[part_idx-1] if part_idx > 0 else (None, None)
|
||
|
||
if tag not in ["class_hint", "numeric", "info"] and prev_content:
|
||
prefix = " ; "
|
||
elif tag in ["numeric", "info"] and prev_content:
|
||
prefix = " "
|
||
|
||
formatted_lines.append(f"{prefix}[{tag}]{content}")
|
||
|
||
return "".join(formatted_lines)
|
||
|
||
|
||
def _get_tag_color_info():
|
||
"""Información sobre los colores de los tags como en setup_output_tags"""
|
||
return {
|
||
"error": {"fg": "#ff6b6b", "font": "bold"},
|
||
"result": {"fg": "#abdbe3"},
|
||
"symbolic": {"fg": "#82aaff"},
|
||
"numeric": {"fg": "#c3e88d"},
|
||
"equation": {"fg": "#c792ea"},
|
||
"info": {"fg": "#ffcb6b"},
|
||
"comment": {"fg": "#546e7a"},
|
||
"class_hint": {"fg": "#888888"},
|
||
"type_hint": {"fg": "#6a6a6a"},
|
||
"custom_type": {"fg": "#f9a825"},
|
||
"hex": {"fg": "#f9a825"},
|
||
"bin": {"fg": "#4fc3f7"},
|
||
"ip": {"fg": "#fff176"},
|
||
"date": {"fg": "#ff8a80"},
|
||
"chr_type": {"fg": "#80cbc4"},
|
||
"helper": {"fg": "#ffd700", "font": "italic"}
|
||
}
|
||
|
||
|
||
def run_debug(input_file: str, output_file: str = None, verbose: bool = False):
|
||
"""
|
||
Ejecuta las queries de debug desde un archivo JSON y guarda los resultados.
|
||
|
||
Args:
|
||
input_file: Archivo JSON con las queries
|
||
output_file: Archivo de salida (opcional)
|
||
verbose: Modo verboso
|
||
"""
|
||
|
||
# Cargar queries del archivo de entrada
|
||
try:
|
||
with open(input_file, 'r', encoding='utf-8') as f:
|
||
data = json.load(f)
|
||
except FileNotFoundError:
|
||
print(f"Error: No se encontró el archivo '{input_file}'")
|
||
sys.exit(1)
|
||
except json.JSONDecodeError as e:
|
||
print(f"Error: JSON inválido en '{input_file}': {e}")
|
||
sys.exit(1)
|
||
|
||
if 'queries' not in data:
|
||
print("Error: El archivo JSON debe contener una clave 'queries'")
|
||
sys.exit(1)
|
||
|
||
# Crear motor de evaluación
|
||
if verbose:
|
||
print("Iniciando motor de evaluación...")
|
||
|
||
engine = HybridEvaluationEngine()
|
||
results = []
|
||
successful = 0
|
||
failed = 0
|
||
|
||
# Ejecutar cada query
|
||
for query in data['queries']:
|
||
if verbose:
|
||
print(f"Ejecutando query {query.get('index', '?')}: {query.get('content', '')[:50]}...")
|
||
|
||
try:
|
||
if query['type'] == 'input':
|
||
# Query de tipo input: evaluar expresión como si fuera entrada del usuario
|
||
result = engine.evaluate_line(query['content'])
|
||
|
||
output = {
|
||
'index': query['index'],
|
||
'input': query['content'],
|
||
'output': str(result.result) if hasattr(result, 'result') else str(result),
|
||
'result_type': type(result.result).__name__ if hasattr(result, 'result') else type(result).__name__,
|
||
'success': not (hasattr(result, 'is_error') and result.is_error),
|
||
'error': result.error if hasattr(result, 'is_error') and result.is_error else None
|
||
}
|
||
|
||
# Generar output_raw: resultado exacto como se muestra en la aplicación
|
||
try:
|
||
output_raw_data = _process_evaluation_result_for_debug(result, engine)
|
||
output['output_raw'] = output_raw_data
|
||
except Exception as e:
|
||
output['output_raw'] = f"Error generando output_raw: {e}"
|
||
|
||
# Añadir información adicional si está disponible
|
||
if hasattr(result, 'result') and hasattr(result.result, '__class__'):
|
||
output['display_class'] = f"[{result.result.__class__.__name__}]"
|
||
|
||
if not (hasattr(result, 'is_error') and result.is_error):
|
||
successful += 1
|
||
else:
|
||
failed += 1
|
||
|
||
elif query['type'] == 'exec':
|
||
# Query de tipo exec: ejecutar código Python para inspeccionar el estado
|
||
exec_result = eval(query['content'], {'engine': engine})
|
||
|
||
output = {
|
||
'index': query['index'],
|
||
'input': query['content'],
|
||
'output': str(exec_result),
|
||
'result_type': type(exec_result).__name__,
|
||
'success': True,
|
||
'error': None,
|
||
'exec_result': exec_result
|
||
}
|
||
successful += 1
|
||
|
||
else:
|
||
raise ValueError(f"Tipo de query desconocido: {query['type']}")
|
||
|
||
except Exception as e:
|
||
# Manejar errores en la ejecución
|
||
output = {
|
||
'index': query['index'],
|
||
'input': query['content'],
|
||
'output': None,
|
||
'result_type': None,
|
||
'success': False,
|
||
'error': str(e)
|
||
}
|
||
failed += 1
|
||
|
||
if verbose:
|
||
print(f" Error: {str(e)}")
|
||
|
||
results.append(output)
|
||
|
||
# Preparar salida final
|
||
final_output = {
|
||
'execution_info': {
|
||
'timestamp': datetime.now().isoformat() + 'Z',
|
||
'total_queries': len(data['queries']),
|
||
'successful': successful,
|
||
'failed': failed,
|
||
'input_file': input_file
|
||
},
|
||
'results': results
|
||
}
|
||
|
||
# Determinar archivo de salida
|
||
if output_file is None:
|
||
base_name = Path(input_file).stem
|
||
output_file = f"{base_name}_results.json"
|
||
|
||
# Guardar resultados
|
||
try:
|
||
# Usar un encoder personalizado para objetos no serializables
|
||
def json_serializer(obj):
|
||
"""Serializar objetos no estándar a string"""
|
||
try:
|
||
# Intentar conversión normal primero
|
||
json.dumps(obj)
|
||
return obj
|
||
except TypeError:
|
||
# Si no es serializable, convertir a string
|
||
return str(obj)
|
||
|
||
# Aplicar serialización personalizada a exec_result
|
||
for result in final_output['results']:
|
||
if 'exec_result' in result:
|
||
result['exec_result'] = json_serializer(result['exec_result'])
|
||
|
||
with open(output_file, 'w', encoding='utf-8') as f:
|
||
json.dump(final_output, f, indent=2, ensure_ascii=False, default=str)
|
||
|
||
if verbose:
|
||
print(f"\nResultados guardados en: {output_file}")
|
||
print(f"Queries exitosas: {successful}")
|
||
print(f"Queries con error: {failed}")
|
||
|
||
except Exception as e:
|
||
print(f"Error al guardar resultados: {e}")
|
||
sys.exit(1)
|
||
|
||
return final_output
|
||
|
||
|
||
def main():
|
||
"""Función principal del CLI"""
|
||
parser = argparse.ArgumentParser(
|
||
description='Simple Debug API para Calculadora MAV CAS',
|
||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||
epilog="""
|
||
Ejemplos de uso:
|
||
python simple_debug.py debug_input.json
|
||
python simple_debug.py debug_input.json --output custom_results.json
|
||
python simple_debug.py debug_input.json --verbose
|
||
"""
|
||
)
|
||
|
||
parser.add_argument('input_file',
|
||
help='Archivo JSON con las queries de debug')
|
||
parser.add_argument('--output', '-o',
|
||
help='Archivo de salida (por defecto: <input>_results.json)')
|
||
parser.add_argument('--verbose', '-v',
|
||
action='store_true',
|
||
help='Modo verboso')
|
||
|
||
args = parser.parse_args()
|
||
|
||
# Verificar que el archivo de entrada existe
|
||
if not Path(args.input_file).exists():
|
||
print(f"Error: El archivo '{args.input_file}' no existe")
|
||
sys.exit(1)
|
||
|
||
# Ejecutar debug
|
||
try:
|
||
results = run_debug(args.input_file, args.output, args.verbose)
|
||
|
||
# Mostrar resumen si no es modo verboso
|
||
if not args.verbose:
|
||
info = results['execution_info']
|
||
print(f"Debug completado: {info['successful']}/{info['total_queries']} exitosas")
|
||
if args.output:
|
||
print(f"Resultados en: {args.output}")
|
||
else:
|
||
base_name = Path(args.input_file).stem
|
||
print(f"Resultados en: {base_name}_results.json")
|
||
|
||
except KeyboardInterrupt:
|
||
print("\nInterrumpido por el usuario")
|
||
sys.exit(1)
|
||
except Exception as e:
|
||
print(f"Error inesperado: {e}")
|
||
sys.exit(1)
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main() |