Implementación de la clase IP4Mask para gestionar máscaras de direcciones IPv4 y mejoras en la clase Class_IP4 para soportar nuevas sintaxis de entrada. Se actualiza el autocompletado y se añaden métodos para obtener la máscara de red y la longitud del prefijo. Se mejora la documentación de ayuda contextual.

This commit is contained in:
Miguel 2025-06-02 19:53:21 +02:00
parent bc768e9ca7
commit 23676b9ef9
4 changed files with 319 additions and 157 deletions

View File

@ -2,9 +2,93 @@
Clase híbrida para direcciones IPv4 Clase híbrida para direcciones IPv4
""" """
from sympy_Base import SympyClassBase from sympy_Base import SympyClassBase
from typing import Optional, Tuple from typing import Optional, Union
import re import re
class IP4Mask:
"""
Helper class to manage IPv4 masks.
It can be initialized with an integer prefix (0-32) or a netmask string (e.g., "255.255.255.0").
"""
_prefix: int
_mask_int: int
def __init__(self, mask_input: Union[int, str]):
if isinstance(mask_input, int):
if not (0 <= mask_input <= 32):
raise ValueError(f"Invalid prefix length: {mask_input}. Must be between 0 and 32.")
self._prefix = mask_input
elif isinstance(mask_input, str):
try:
# Try to interpret as prefix length string, e.g., "24"
prefix_val = int(mask_input)
if not (0 <= prefix_val <= 32):
raise ValueError(f"Invalid prefix string: '{mask_input}'. Must be between 0 and 32.")
self._prefix = prefix_val
except ValueError:
# Try to interpret as netmask string, e.g., "255.255.255.0"
parsed_prefix = self._netmask_str_to_prefix(mask_input)
if parsed_prefix is None:
raise ValueError(f"Invalid netmask string format: '{mask_input}'.")
self._prefix = parsed_prefix
else:
raise TypeError(f"Invalid type for mask_input: {type(mask_input)}. Must be int or str.")
self._mask_int = self._prefix_to_mask_int(self._prefix)
@staticmethod
def _is_valid_ip_octet_str(octet_str: str) -> bool:
try:
octet_val = int(octet_str)
return 0 <= octet_val <= 255
except ValueError:
return False
@staticmethod
def _is_valid_ip_string(ip_str: str) -> bool:
parts = ip_str.split('.')
if len(parts) != 4:
return False
return all(IP4Mask._is_valid_ip_octet_str(p.strip()) for p in parts)
@staticmethod
def _netmask_str_to_prefix(netmask_str: str) -> Optional[int]:
if not IP4Mask._is_valid_ip_string(netmask_str):
return None
octets = [int(x) for x in netmask_str.split('.')]
mask_val = (octets[0] << 24) | (octets[1] << 16) | (octets[2] << 8) | octets[3]
binary_mask = bin(mask_val)[2:].zfill(32)
if not re.fullmatch(r"1*0*", binary_mask): # Must be contiguous 1s followed by 0s
return None
return binary_mask.count('1')
@staticmethod
def _prefix_to_mask_int(prefix: int) -> int:
if not (0 <= prefix <= 32): # Should be validated before calling
raise ValueError("Prefix out of range")
if prefix == 0:
return 0
return (0xFFFFFFFF << (32 - prefix)) & 0xFFFFFFFF
def get_mask_str(self) -> str:
return f"{(self._mask_int >> 24) & 0xFF}.{(self._mask_int >> 16) & 0xFF}.{(self._mask_int >> 8) & 0xFF}.{self._mask_int & 0xFF}"
def get_prefix_int(self) -> int:
return self._prefix
def __str__(self) -> str:
return self.get_mask_str()
def __repr__(self) -> str:
return f"IP4Mask({self._prefix})"
def __eq__(self, other):
if isinstance(other, IP4Mask):
return self._prefix == other._prefix
return False
class Class_IP4(SympyClassBase): class Class_IP4(SympyClassBase):
"""Clase híbrida para direcciones IPv4""" """Clase híbrida para direcciones IPv4"""
@ -16,96 +100,88 @@ class Class_IP4(SympyClassBase):
def __init__(self, *args): def __init__(self, *args):
"""Inicialización de IP4""" """Inicialización de IP4"""
if len(args) == 1: self._raw_constructor_args = args # Store for __repr__
# Formato: "192.168.1.1/24" o "192.168.1.1 255.255.255.0"
input_str = args[0]
if '/' in input_str:
# Formato CIDR
ip_str, prefix_str = input_str.split('/')
prefix = int(prefix_str)
else:
# Formato con máscara
parts = input_str.split()
if len(parts) == 2:
ip_str, netmask_str = parts
prefix = self._netmask_str_to_prefix(netmask_str)
else:
ip_str = input_str
prefix = None
else:
# Formato: ("192.168.1.1", 24) o ("192.168.1.1", "255.255.255.0")
ip_str = args[0]
if len(args) > 1:
if isinstance(args[1], int):
prefix = args[1]
else:
prefix = self._netmask_str_to_prefix(args[1])
else:
prefix = None
if not self._is_valid_ip_string(ip_str): ip_str: str
self._mask_obj: Optional[IP4Mask] = None
if not args:
raise ValueError("IP4 constructor requires at least one argument (the IP string).")
ip_str_candidate = args[0]
if not isinstance(ip_str_candidate, str):
raise TypeError(f"First argument to IP4 must be a string (IP address), got {type(ip_str_candidate)}")
if len(args) == 1:
input_str = args[0]
# Try "ip/prefix"
if '/' in input_str:
parts = input_str.split('/', 1)
ip_str = parts[0].strip()
if len(parts) > 1 and parts[1].strip():
self._mask_obj = IP4Mask(parts[1].strip())
# If no prefix after '/', it's just an IP
# Try "ip mask_str" (space separated)
elif ' ' in input_str:
parts = input_str.split()
ip_str = parts[0].strip()
if len(parts) > 1 and parts[1].strip():
self._mask_obj = IP4Mask(parts[1].strip())
# If no mask after space, it's just an IP
else: # Just an IP string
ip_str = input_str.strip()
elif len(args) == 2:
ip_str = args[0].strip()
mask_arg = args[1]
if isinstance(mask_arg, IP4Mask): # Allow passing IP4Mask instance directly
self._mask_obj = mask_arg
elif isinstance(mask_arg, (str, int)): # Parser will pass str, programmatic use might pass int
self._mask_obj = IP4Mask(mask_arg)
else:
raise TypeError(f"Second argument (mask) for IP4 must be int, str, or IP4Mask instance, got {type(mask_arg)}")
else:
raise ValueError(f"IP4 constructor takes 1 or 2 arguments, got {len(args)}: {args}")
if not IP4Mask._is_valid_ip_string(ip_str): # Use IP4Mask's validator
raise ValueError(f"Invalid IP address format: {ip_str}") raise ValueError(f"Invalid IP address format: {ip_str}")
if prefix is not None and not self._is_valid_prefix(prefix):
raise ValueError(f"Invalid prefix length: {prefix}")
# Convertir IP a entero para almacenamiento
ip_parts = [int(x) for x in ip_str.split('.')]
ip_int = (ip_parts[0] << 24) + (ip_parts[1] << 16) + (ip_parts[2] << 8) + ip_parts[3]
# Almacenar valores
self._ip_int = ip_int
self._prefix = prefix
self._ip_str = ip_str self._ip_str = ip_str
ip_parts = [int(x) for x in ip_str.split('.')]
self._ip_int = (ip_parts[0] << 24) | (ip_parts[1] << 16) | (ip_parts[2] << 8) | ip_parts[3]
# Llamar al constructor base # Determine the 'original_str' for SympyClassBase's _sympystr.
super().__init__(ip_int, input_str) # This string is what appears inside ClassName(...) in Sympy output.
# It should reflect the arguments as they would be if typed in brackets.
sympy_base_original_str: str
if len(args) == 1 and isinstance(args[0], str):
# Covers "1.1.1.1/24", "1.1.1.1 255.255.0.0", or just "1.1.1.1"
sympy_base_original_str = args[0]
elif len(args) == 2:
# Reconstruct as "ip_str;mask_representation"
mask_arg_for_repr = args[1]
if isinstance(mask_arg_for_repr, IP4Mask): # Should not happen from parser
mask_repr_str = str(mask_arg_for_repr.get_prefix_int())
elif isinstance(mask_arg_for_repr, int):
mask_repr_str = str(mask_arg_for_repr)
else: # string
mask_repr_str = str(mask_arg_for_repr) # e.g., "24" or "255.255.255.0"
sympy_base_original_str = f"{args[0]};{mask_repr_str}"
else: # Only ip_str was derived, no mask (or error caught earlier)
sympy_base_original_str = self._ip_str
def _is_valid_ip_string(self, ip_str: str) -> bool: super().__init__(self._ip_int, sympy_base_original_str)
"""Verifica si el string es una IP válida"""
pattern = r'^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$'
match = re.match(pattern, ip_str)
if not match:
return False
# Verificar que cada octeto esté en rango def __repr__(self):
for octet in match.groups(): # This should be a valid Python expression to recreate the object.
if not 0 <= int(octet) <= 255: arg_reprs = [repr(arg) for arg in self._raw_constructor_args]
return False return f"{self.__class__.__name__}({', '.join(arg_reprs)})"
return True
def _is_valid_prefix(self, prefix: int) -> bool:
"""Verifica si el prefijo es válido"""
return 0 <= prefix <= 32
def _netmask_str_to_prefix(self, netmask_str: str) -> Optional[int]:
"""Convierte máscara de red a longitud de prefijo"""
if not self._is_valid_ip_string(netmask_str):
return None
# Convertir máscara a binario
parts = [int(x) for x in netmask_str.split('.')]
binary = ''.join(f'{x:08b}' for x in parts)
# Contar 1's consecutivos desde la izquierda
prefix = 0
for bit in binary:
if bit == '1':
prefix += 1
else:
break
# Verificar que el resto sean 0's
if '1' in binary[prefix:]:
return None
return prefix
def __str__(self): def __str__(self):
"""Representación string para display""" """Representación string para display"""
if self._prefix is not None: if self._mask_obj:
return f"{self._ip_str}/{self._prefix}" return f"{self._ip_str}/{self._mask_obj.get_prefix_int()}"
return self._ip_str return self._ip_str
def _sympystr(self, printer): def _sympystr(self, printer):
@ -116,7 +192,8 @@ class Class_IP4(SympyClassBase):
def Helper(input_str): def Helper(input_str):
"""Ayuda contextual para IP4""" """Ayuda contextual para IP4"""
if re.match(r"^\s*IP4\b", input_str, re.IGNORECASE): if re.match(r"^\s*IP4\b", input_str, re.IGNORECASE):
return 'Ej: IP4[192.168.1.1/24], IP4[10.0.0.1, 8], o IP4[172.16.0.5, 255.255.0.0]\nFunciones: NetworkAddress(), BroadcastAddress(), Nodes()' return ('Ej: IP4[192.168.1.1/24], IP4[10.0.0.1;8], IP4[172.16.0.5;255.255.0.0]\n'
'Funciones: NetworkAddress(), BroadcastAddress(), Nodes(), get_netmask_str(), get_prefix_length()')
return None return None
@staticmethod @staticmethod
@ -125,57 +202,61 @@ class Class_IP4(SympyClassBase):
return [ return [
("NetworkAddress", "Obtiene la dirección de red"), ("NetworkAddress", "Obtiene la dirección de red"),
("BroadcastAddress", "Obtiene la dirección de broadcast"), ("BroadcastAddress", "Obtiene la dirección de broadcast"),
("Nodes", "Cantidad de nodos disponibles"), ("Nodes", "Cantidad de nodos usables en la subred"),
("get_netmask_str", "Obtiene la máscara de red (ej: 255.255.255.0)"),
("get_prefix_length", "Obtiene la longitud del prefijo CIDR (ej: 24)"),
] ]
def get_netmask_str(self) -> Optional[str]:
"""Returns the netmask as a string (e.g., "255.255.255.0") if a mask is defined."""
return self._mask_obj.get_mask_str() if self._mask_obj else None
def get_prefix_length(self) -> Optional[int]:
"""Returns the prefix length (e.g., 24) if a mask is defined."""
return self._mask_obj.get_prefix_int() if self._mask_obj else None
def NetworkAddress(self): def NetworkAddress(self):
"""Obtiene la dirección de red""" """Obtiene la dirección de red"""
if self._prefix is None: prefix = self.get_prefix_length()
raise ValueError("No prefix/mask defined") if prefix is None:
raise ValueError("No prefix/mask defined for NetworkAddress calculation.")
# Calcular máscara de red mask_int = IP4Mask._prefix_to_mask_int(prefix)
mask = (0xffffffff >> (32 - self._prefix)) << (32 - self._prefix) network_int = self._ip_int & mask_int
# Aplicar máscara
network = self._ip_int & mask
# Convertir a string
parts = [ parts = [
(network >> 24) & 0xff, (network_int >> 24) & 0xFF,
(network >> 16) & 0xff, (network_int >> 16) & 0xFF,
(network >> 8) & 0xff, (network_int >> 8) & 0xFF,
network & 0xff network_int & 0xFF
] ]
network_str = '.'.join(str(x) for x in parts) network_str = '.'.join(str(x) for x in parts)
return Class_IP4(network_str, prefix) # Return new IP4 object for the network
return Class_IP4(f"{network_str}/{self._prefix}")
def BroadcastAddress(self): def BroadcastAddress(self):
"""Obtiene la dirección de broadcast""" """Obtiene la dirección de broadcast"""
if self._prefix is None: prefix = self.get_prefix_length()
raise ValueError("No prefix/mask defined") if prefix is None:
raise ValueError("No prefix/mask defined for BroadcastAddress calculation.")
# Calcular máscara de red mask_int = IP4Mask._prefix_to_mask_int(prefix)
mask = (0xffffffff >> (32 - self._prefix)) << (32 - self._prefix) broadcast_int = self._ip_int | (~mask_int & 0xFFFFFFFF)
# Calcular broadcast
broadcast = self._ip_int | (~mask & 0xffffffff)
# Convertir a string
parts = [ parts = [
(broadcast >> 24) & 0xff, (broadcast_int >> 24) & 0xFF,
(broadcast >> 16) & 0xff, (broadcast_int >> 16) & 0xFF,
(broadcast >> 8) & 0xff, (broadcast_int >> 8) & 0xFF,
broadcast & 0xff broadcast_int & 0xFF
] ]
broadcast_str = '.'.join(str(x) for x in parts) broadcast_str = '.'.join(str(x) for x in parts)
return Class_IP4(broadcast_str, prefix) # Return new IP4 object for broadcast
return Class_IP4(f"{broadcast_str}/{self._prefix}")
def Nodes(self): def Nodes(self):
"""Obtiene el número de nodos disponibles""" """Obtiene el número de nodos disponibles"""
if self._prefix is None: prefix = self.get_prefix_length()
raise ValueError("No prefix/mask defined") if prefix is None:
raise ValueError("No prefix/mask defined for Nodes calculation.")
# 2^(32-prefix) - 2 (red y broadcast) if prefix >= 31: # For /31 and /32, typically 0 usable host addresses in standard subnetting
return 2 ** (32 - self._prefix) - 2 return 0
return (2 ** (32 - prefix)) - 2

View File

@ -13,7 +13,8 @@ import re
# Importar componentes del CAS híbrido # Importar componentes del CAS híbrido
from main_evaluation import HybridEvaluationEngine, EvaluationResult from main_evaluation import HybridEvaluationEngine, EvaluationResult
from tl_popup import InteractiveResultManager from sympy_Base import SympyClassBase
from tl_popup import InteractiveResultManager, PlotResult # <--- Asegurar que PlotResult se importa
from ip4_type import Class_IP4 from ip4_type import Class_IP4
from hex_type import Class_Hex from hex_type import Class_Hex
from bin_type import Class_Bin from bin_type import Class_Bin
@ -267,6 +268,7 @@ class HybridCalculatorApp:
self.output_text.tag_configure("equation", foreground="#c792ea") self.output_text.tag_configure("equation", foreground="#c792ea")
self.output_text.tag_configure("info", foreground="#ffcb6b") self.output_text.tag_configure("info", foreground="#ffcb6b")
self.output_text.tag_configure("comment", foreground="#546e7a") self.output_text.tag_configure("comment", foreground="#546e7a")
self.output_text.tag_configure("class_hint", foreground="#888888") # Gris para la pista de clase
self.output_text.tag_configure("type_hint", foreground="#6a6a6a") self.output_text.tag_configure("type_hint", foreground="#6a6a6a")
# Tags para tipos especializados # Tags para tipos especializados
@ -291,6 +293,7 @@ class HybridCalculatorApp:
self._debounce_job = self.root.after(300, self._evaluate_and_update) self._debounce_job = self.root.after(300, self._evaluate_and_update)
def _handle_dot_autocomplete(self): def _handle_dot_autocomplete(self):
"""Maneja el autocompletado cuando se escribe un punto."""
self._close_autocomplete_popup() self._close_autocomplete_popup()
cursor_index_str = self.input_text.index(tk.INSERT) cursor_index_str = self.input_text.index(tk.INSERT)
line_num_str, char_num_str = cursor_index_str.split('.') line_num_str, char_num_str = cursor_index_str.split('.')
@ -298,23 +301,28 @@ class HybridCalculatorApp:
char_idx_after_dot = int(char_num_str) char_idx_after_dot = int(char_num_str)
if char_idx_after_dot == 0: # Should not happen if a dot was typed if char_idx_after_dot == 0: # Should not happen if a dot was typed
print("DEBUG: _handle_dot_autocomplete called with cursor at beginning of line somehow.") print("DEBUG: Autocomplete: Cursor at beginning of line after dot. No action.")
return return
text_before_dot = self.input_text.get(f"{current_line_num}.0", f"{current_line_num}.{char_idx_after_dot - 1}") # Índice del punto en la línea actual (0-based)
dot_char_index_in_line = char_idx_after_dot - 1
# Texto en la línea actual HASTA el punto (sin incluirlo)
text_on_line_up_to_dot = self.input_text.get(f"{current_line_num}.0", f"{current_line_num}.{dot_char_index_in_line}")
# Si no hay nada o solo espacios antes del punto, ofrecer sugerencias globales stripped_text_before_dot = text_on_line_up_to_dot.strip()
if not text_before_dot.strip():
# 1. Determinar si es un popup GLOBAL
if not stripped_text_before_dot:
print("DEBUG: Dot on empty line or after spaces. Offering global suggestions.") print("DEBUG: Dot on empty line or after spaces. Offering global suggestions.")
suggestions = [] suggestions = []
custom_classes_suggestions = [ custom_types_suggestions = [
("Hex", "Tipo Hexadecimal. Ej: Hex[FF]"), ("Hex", "Tipo Hexadecimal. Ej: Hex[FF]"),
("Bin", "Tipo Binario. Ej: Bin[1010]"), ("Bin", "Tipo Binario. Ej: Bin[1010]"),
("Dec", "Tipo Decimal. Ej: Dec[42]"), ("Dec", "Tipo Decimal. Ej: Dec[42]"),
("IP4", "Tipo Dirección IPv4. Ej: IP4[1.2.3.4/24]"), ("IP4", "Tipo Dirección IPv4. Ej: IP4[1.2.3.4/24]"),
("Chr", "Tipo Carácter. Ej: Chr[A]"), ("Chr", "Tipo Carácter. Ej: Chr[A]"),
] ]
suggestions.extend(custom_classes_suggestions) suggestions.extend(custom_types_suggestions)
try: try:
sympy_functions = SympyHelper.PopupFunctionList() sympy_functions = SympyHelper.PopupFunctionList()
@ -327,16 +335,34 @@ class HybridCalculatorApp:
self._show_autocomplete_popup(suggestions, is_global_popup=True) self._show_autocomplete_popup(suggestions, is_global_popup=True)
return return
# Si hay texto antes del punto, es para autocompletado de métodos de un objeto # 2. Es un popup de OBJETO. Extraer la expresión del objeto.
obj_expr_str = text_before_dot.strip() obj_expr_str_candidate = ""
print(f"DEBUG: Autocomplete triggered for object. Expression: '{obj_expr_str}'") # 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 not obj_expr_str: if match:
# Esto no debería ocurrir si la lógica anterior para popup global es correcta obj_expr_str_candidate = match.group(1).replace(" ", "") # Quitar espacios como en "obj . method"
print("DEBUG: Object expression is empty. No autocomplete.") 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(("+", "-", "*", "/", "(", ",")):
print(f"DEBUG: Extracted expr '{obj_expr_str_candidate}' from '{stripped_text_before_dot}' not a valid object for dot autocomplete.")
return return
# Caso especial para el módulo sympy 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.
print("DEBUG: Object expression is empty after extraction. No autocomplete.")
return
# 3. Caso especial para el módulo sympy
if obj_expr_str == "sympy": if obj_expr_str == "sympy":
print(f"DEBUG: Detected 'sympy.', using SympyHelper for suggestions.") print(f"DEBUG: Detected 'sympy.', using SympyHelper for suggestions.")
try: try:
@ -349,20 +375,22 @@ class HybridCalculatorApp:
print(f"DEBUG: Error calling SympyHelper.PopupFunctionList(): {e}") print(f"DEBUG: Error calling SympyHelper.PopupFunctionList(): {e}")
return return
# Preprocesar para convertir sintaxis de corchetes a llamada de clase # 4. Preprocesar con BracketParser para sintaxis Clase[arg] y metodo[]
# Ejemplo: Hex[FF] -> Hex('FF') # Es importante transformar obj_expr_str ANTES de pasarlo a eval().
bracket_match = re.match(r"([A-Za-z_][A-Za-z0-9_]*)\[(.*)\]$", obj_expr_str) if '[' in obj_expr_str: # Optimización: solo llamar si hay corchetes
if bracket_match: original_for_debug = obj_expr_str
class_name, arg = bracket_match.groups() # self.engine.parser es una instancia de BracketParser
if arg.isdigit(): obj_expr_str = self.engine.parser._transform_brackets(obj_expr_str)
obj_expr_str = f"{class_name}({arg})" if obj_expr_str != original_for_debug:
else: print(f"DEBUG: Preprocessed by BracketParser: '{original_for_debug}' -> '{obj_expr_str}'")
obj_expr_str = f"{class_name}('{arg}')"
print(f"DEBUG: Preprocessed bracket syntax to: '{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 {} eval_context = self.engine._get_full_context() if hasattr(self.engine, '_get_full_context') else {}
obj = None obj = None
try: try:
if not obj_expr_str.strip(): # Seguridad adicional
print("DEBUG: Object expression became empty before eval. No action.")
return
print(f"DEBUG: Attempting to eval: '{obj_expr_str}'") print(f"DEBUG: Attempting to eval: '{obj_expr_str}'")
obj = eval(obj_expr_str, eval_context) obj = eval(obj_expr_str, eval_context)
print(f"DEBUG: Eval successful. Object: {type(obj)}, Value: {obj}") print(f"DEBUG: Eval successful. Object: {type(obj)}, Value: {obj}")
@ -370,10 +398,13 @@ class HybridCalculatorApp:
print(f"DEBUG: Error evaluating object expression '{obj_expr_str}': {e}") print(f"DEBUG: Error evaluating object expression '{obj_expr_str}': {e}")
return return
# 6. Mostrar popup de autocompletado para el objeto
if obj is not None and hasattr(obj, 'PopupFunctionList'): if obj is not None and hasattr(obj, 'PopupFunctionList'):
methods = obj.PopupFunctionList() methods = obj.PopupFunctionList()
if methods: if methods:
self._show_autocomplete_popup(methods, is_global_popup=False) self._show_autocomplete_popup(methods, is_global_popup=False)
# else: Podríamos añadir un fallback a dir(obj) aquí si se desea para objetos genéricos
# print(f"DEBUG: Object {type(obj)} has no PopupFunctionList. dir(obj) could be used.")
def _show_autocomplete_popup(self, suggestions, is_global_popup=False): def _show_autocomplete_popup(self, suggestions, is_global_popup=False):
# suggestions: lista de tuplas (nombre, hint) # suggestions: lista de tuplas (nombre, hint)
@ -570,13 +601,43 @@ class HybridCalculatorApp:
else: else:
output_parts.append((tag, str(result.result))) output_parts.append((tag, str(result.result)))
# 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 class_display_name:
output_parts.append(("class_hint", f"[{class_display_name}]"))
# Mostrar evaluación numérica si existe # Mostrar evaluación numérica si existe
if result.numeric_result is not None and result.numeric_result != result.result: if result.numeric_result is not None and result.numeric_result != result.result:
output_parts.append(("numeric", f"{result.numeric_result}")) output_parts.append(("numeric", f"{result.numeric_result}")) # El espacio se controlará en _display_output
# Mostrar información adicional # Mostrar información adicional
if result.info: if result.info:
output_parts.append(("info", f" ({result.info})")) output_parts.append(("info", f"({result.info})")) # El espacio se controlará en _display_output
return output_parts return output_parts
@ -606,14 +667,24 @@ class HybridCalculatorApp:
pass pass
else: else:
# Mostrar partes de la línea # Mostrar partes de la línea
first_part = True for part_idx, (tag, content) in enumerate(line_parts):
for tag, content in line_parts: if not content: # Omitir contenido vacío
if not first_part and content: continue
self.output_text.insert(tk.END, " ; ")
if content: # 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
self.output_text.insert(tk.END, str(content), tag) self.output_text.insert(tk.END, str(content), tag)
first_part = False
# Añadir nueva línea excepto para la última línea # Añadir nueva línea excepto para la última línea
if line_idx < len(output_data) - 1: if line_idx < len(output_data) - 1:

View File

@ -156,11 +156,19 @@ class BracketParser:
# Caso: Class[] → Class() # Caso: Class[] → Class()
return f'{class_name}()' return f'{class_name}()'
else: else:
# Caso: Class[args] → Class("args") # Split arguments by semicolon if present
# Escapar comillas dobles en el contenido # Each argument will be individually quoted.
escaped_content = args_content.replace('"', '\\"') # Example: Class[arg1; arg2] -> Class("arg1", "arg2")
return f'{class_name}("{escaped_content}")' # Example: Class[arg1] -> Class("arg1")
args_list = [arg.strip() for arg in args_content.split(';')]
processed_args = []
for arg_val in args_list:
# Escape backslashes first, then double quotes for string literals
escaped_arg = arg_val.replace('\\', '\\\\').replace('"', '\\"')
processed_args.append(f'"{escaped_arg}"')
return f'{class_name}({", ".join(processed_args)})'
# Aplicar transformación repetidamente hasta que no haya más cambios # Aplicar transformación repetidamente hasta que no haya más cambios
transformed = line transformed = line
while True: while True:
@ -228,6 +236,8 @@ def test_bracket_parser():
# Sintaxis con corchetes # Sintaxis con corchetes
("Hex[FF]", 'Hex("FF")', "bracket_transform"), ("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[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"), ("IP4[192.168.1.1/24].NetworkAddress[]", 'IP4("192.168.1.1/24").NetworkAddress()', "bracket_transform"),
("Bin[1010]", 'Bin("1010")', "bracket_transform"), ("Bin[1010]", 'Bin("1010")', "bracket_transform"),