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:
parent
bc768e9ca7
commit
23676b9ef9
317
ip4_type.py
317
ip4_type.py
|
@ -2,9 +2,93 @@
|
|||
Clase híbrida para direcciones IPv4
|
||||
"""
|
||||
from sympy_Base import SympyClassBase
|
||||
from typing import Optional, Tuple
|
||||
from typing import Optional, Union
|
||||
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):
|
||||
"""Clase híbrida para direcciones IPv4"""
|
||||
|
@ -16,96 +100,88 @@ class Class_IP4(SympyClassBase):
|
|||
|
||||
def __init__(self, *args):
|
||||
"""Inicialización de IP4"""
|
||||
if len(args) == 1:
|
||||
# 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
|
||||
self._raw_constructor_args = args # Store for __repr__
|
||||
|
||||
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}")
|
||||
|
||||
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
|
||||
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
|
||||
super().__init__(ip_int, input_str)
|
||||
|
||||
def _is_valid_ip_string(self, ip_str: str) -> bool:
|
||||
"""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
|
||||
for octet in match.groups():
|
||||
if not 0 <= int(octet) <= 255:
|
||||
return False
|
||||
|
||||
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
|
||||
# Determine the 'original_str' for SympyClassBase's _sympystr.
|
||||
# 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
|
||||
|
||||
super().__init__(self._ip_int, sympy_base_original_str)
|
||||
|
||||
def __repr__(self):
|
||||
# This should be a valid Python expression to recreate the object.
|
||||
arg_reprs = [repr(arg) for arg in self._raw_constructor_args]
|
||||
return f"{self.__class__.__name__}({', '.join(arg_reprs)})"
|
||||
|
||||
def __str__(self):
|
||||
"""Representación string para display"""
|
||||
if self._prefix is not None:
|
||||
return f"{self._ip_str}/{self._prefix}"
|
||||
if self._mask_obj:
|
||||
return f"{self._ip_str}/{self._mask_obj.get_prefix_int()}"
|
||||
return self._ip_str
|
||||
|
||||
def _sympystr(self, printer):
|
||||
|
@ -116,7 +192,8 @@ class Class_IP4(SympyClassBase):
|
|||
def Helper(input_str):
|
||||
"""Ayuda contextual para IP4"""
|
||||
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
|
||||
|
||||
@staticmethod
|
||||
|
@ -125,57 +202,61 @@ class Class_IP4(SympyClassBase):
|
|||
return [
|
||||
("NetworkAddress", "Obtiene la dirección de red"),
|
||||
("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):
|
||||
"""Obtiene la dirección de red"""
|
||||
if self._prefix is None:
|
||||
raise ValueError("No prefix/mask defined")
|
||||
prefix = self.get_prefix_length()
|
||||
if prefix is None:
|
||||
raise ValueError("No prefix/mask defined for NetworkAddress calculation.")
|
||||
|
||||
# Calcular máscara de red
|
||||
mask = (0xffffffff >> (32 - self._prefix)) << (32 - self._prefix)
|
||||
mask_int = IP4Mask._prefix_to_mask_int(prefix)
|
||||
network_int = self._ip_int & mask_int
|
||||
|
||||
# Aplicar máscara
|
||||
network = self._ip_int & mask
|
||||
|
||||
# Convertir a string
|
||||
parts = [
|
||||
(network >> 24) & 0xff,
|
||||
(network >> 16) & 0xff,
|
||||
(network >> 8) & 0xff,
|
||||
network & 0xff
|
||||
(network_int >> 24) & 0xFF,
|
||||
(network_int >> 16) & 0xFF,
|
||||
(network_int >> 8) & 0xFF,
|
||||
network_int & 0xFF
|
||||
]
|
||||
network_str = '.'.join(str(x) for x in parts)
|
||||
|
||||
return Class_IP4(f"{network_str}/{self._prefix}")
|
||||
return Class_IP4(network_str, prefix) # Return new IP4 object for the network
|
||||
|
||||
def BroadcastAddress(self):
|
||||
"""Obtiene la dirección de broadcast"""
|
||||
if self._prefix is None:
|
||||
raise ValueError("No prefix/mask defined")
|
||||
prefix = self.get_prefix_length()
|
||||
if prefix is None:
|
||||
raise ValueError("No prefix/mask defined for BroadcastAddress calculation.")
|
||||
|
||||
mask_int = IP4Mask._prefix_to_mask_int(prefix)
|
||||
broadcast_int = self._ip_int | (~mask_int & 0xFFFFFFFF)
|
||||
|
||||
# Calcular máscara de red
|
||||
mask = (0xffffffff >> (32 - self._prefix)) << (32 - self._prefix)
|
||||
|
||||
# Calcular broadcast
|
||||
broadcast = self._ip_int | (~mask & 0xffffffff)
|
||||
|
||||
# Convertir a string
|
||||
parts = [
|
||||
(broadcast >> 24) & 0xff,
|
||||
(broadcast >> 16) & 0xff,
|
||||
(broadcast >> 8) & 0xff,
|
||||
broadcast & 0xff
|
||||
(broadcast_int >> 24) & 0xFF,
|
||||
(broadcast_int >> 16) & 0xFF,
|
||||
(broadcast_int >> 8) & 0xFF,
|
||||
broadcast_int & 0xFF
|
||||
]
|
||||
broadcast_str = '.'.join(str(x) for x in parts)
|
||||
|
||||
return Class_IP4(f"{broadcast_str}/{self._prefix}")
|
||||
return Class_IP4(broadcast_str, prefix) # Return new IP4 object for broadcast
|
||||
|
||||
def Nodes(self):
|
||||
"""Obtiene el número de nodos disponibles"""
|
||||
if self._prefix is None:
|
||||
raise ValueError("No prefix/mask defined")
|
||||
prefix = self.get_prefix_length()
|
||||
if prefix is None:
|
||||
raise ValueError("No prefix/mask defined for Nodes calculation.")
|
||||
|
||||
# 2^(32-prefix) - 2 (red y broadcast)
|
||||
return 2 ** (32 - self._prefix) - 2
|
||||
if prefix >= 31: # For /31 and /32, typically 0 usable host addresses in standard subnetting
|
||||
return 0
|
||||
return (2 ** (32 - prefix)) - 2
|
137
main_calc_app.py
137
main_calc_app.py
|
@ -13,7 +13,8 @@ import re
|
|||
|
||||
# Importar componentes del CAS híbrido
|
||||
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 hex_type import Class_Hex
|
||||
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("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("type_hint", foreground="#6a6a6a")
|
||||
|
||||
# Tags para tipos especializados
|
||||
|
@ -291,6 +293,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."""
|
||||
self._close_autocomplete_popup()
|
||||
cursor_index_str = self.input_text.index(tk.INSERT)
|
||||
line_num_str, char_num_str = cursor_index_str.split('.')
|
||||
|
@ -298,23 +301,28 @@ class HybridCalculatorApp:
|
|||
char_idx_after_dot = int(char_num_str)
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
if not text_before_dot.strip():
|
||||
stripped_text_before_dot = text_on_line_up_to_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.")
|
||||
suggestions = []
|
||||
custom_classes_suggestions = [
|
||||
custom_types_suggestions = [
|
||||
("Hex", "Tipo Hexadecimal. Ej: Hex[FF]"),
|
||||
("Bin", "Tipo Binario. Ej: Bin[1010]"),
|
||||
("Dec", "Tipo Decimal. Ej: Dec[42]"),
|
||||
("IP4", "Tipo Dirección IPv4. Ej: IP4[1.2.3.4/24]"),
|
||||
("Chr", "Tipo Carácter. Ej: Chr[A]"),
|
||||
]
|
||||
suggestions.extend(custom_classes_suggestions)
|
||||
suggestions.extend(custom_types_suggestions)
|
||||
|
||||
try:
|
||||
sympy_functions = SympyHelper.PopupFunctionList()
|
||||
|
@ -327,16 +335,34 @@ class HybridCalculatorApp:
|
|||
self._show_autocomplete_popup(suggestions, is_global_popup=True)
|
||||
return
|
||||
|
||||
# Si hay texto antes del punto, es para autocompletado de métodos de un objeto
|
||||
obj_expr_str = text_before_dot.strip()
|
||||
print(f"DEBUG: Autocomplete triggered for object. Expression: '{obj_expr_str}'")
|
||||
# 2. Es un popup de OBJETO. Extraer la expresión del 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 not obj_expr_str:
|
||||
# Esto no debería ocurrir si la lógica anterior para popup global es correcta
|
||||
print("DEBUG: Object expression is empty. No autocomplete.")
|
||||
if match:
|
||||
obj_expr_str_candidate = match.group(1).replace(" ", "") # Quitar espacios como en "obj . method"
|
||||
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
|
||||
|
||||
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
|
||||
|
||||
# Caso especial para el módulo sympy
|
||||
# 3. Caso especial para el módulo sympy
|
||||
if obj_expr_str == "sympy":
|
||||
print(f"DEBUG: Detected 'sympy.', using SympyHelper for suggestions.")
|
||||
try:
|
||||
|
@ -349,20 +375,22 @@ class HybridCalculatorApp:
|
|||
print(f"DEBUG: Error calling SympyHelper.PopupFunctionList(): {e}")
|
||||
return
|
||||
|
||||
# Preprocesar para convertir sintaxis de corchetes a llamada de clase
|
||||
# Ejemplo: Hex[FF] -> Hex('FF')
|
||||
bracket_match = re.match(r"([A-Za-z_][A-Za-z0-9_]*)\[(.*)\]$", obj_expr_str)
|
||||
if bracket_match:
|
||||
class_name, arg = bracket_match.groups()
|
||||
if arg.isdigit():
|
||||
obj_expr_str = f"{class_name}({arg})"
|
||||
else:
|
||||
obj_expr_str = f"{class_name}('{arg}')"
|
||||
print(f"DEBUG: Preprocessed bracket syntax to: '{obj_expr_str}'")
|
||||
# 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
|
||||
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 {}
|
||||
obj = None
|
||||
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}'")
|
||||
obj = eval(obj_expr_str, eval_context)
|
||||
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}")
|
||||
return
|
||||
|
||||
# 6. Mostrar popup de autocompletado para el objeto
|
||||
if obj is not None and hasattr(obj, 'PopupFunctionList'):
|
||||
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)
|
||||
|
@ -570,13 +601,43 @@ class HybridCalculatorApp:
|
|||
else:
|
||||
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
|
||||
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
|
||||
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
|
||||
|
||||
|
@ -606,14 +667,24 @@ class HybridCalculatorApp:
|
|||
pass
|
||||
else:
|
||||
# Mostrar partes de la línea
|
||||
first_part = True
|
||||
for tag, content in line_parts:
|
||||
if not first_part and content:
|
||||
self.output_text.insert(tk.END, " ; ")
|
||||
|
||||
if content:
|
||||
for part_idx, (tag, content) in enumerate(line_parts):
|
||||
if not content: # Omitir contenido vacío
|
||||
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
|
||||
self.output_text.insert(tk.END, str(content), tag)
|
||||
first_part = False
|
||||
|
||||
# Añadir nueva línea excepto para la última línea
|
||||
if line_idx < len(output_data) - 1:
|
||||
|
|
|
@ -151,16 +151,24 @@ class BracketParser:
|
|||
def replace_match(match):
|
||||
class_name = match.group(1)
|
||||
args_content = match.group(2).strip()
|
||||
|
||||
|
||||
if not args_content:
|
||||
# Caso: Class[] → Class()
|
||||
return f'{class_name}()'
|
||||
else:
|
||||
# Caso: Class[args] → Class("args")
|
||||
# Escapar comillas dobles en el contenido
|
||||
escaped_content = args_content.replace('"', '\\"')
|
||||
return f'{class_name}("{escaped_content}")'
|
||||
|
||||
# Split arguments by semicolon if present
|
||||
# Each argument will be individually quoted.
|
||||
# Example: Class[arg1; arg2] -> Class("arg1", "arg2")
|
||||
# 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
|
||||
transformed = line
|
||||
while True:
|
||||
|
@ -228,6 +236,8 @@ def test_bracket_parser():
|
|||
# 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"),
|
||||
|
||||
|
|
Loading…
Reference in New Issue