From 23676b9ef95a99ba85f6e7b51a1213f844ea0100 Mon Sep 17 00:00:00 2001 From: Miguel Date: Mon, 2 Jun 2025 19:53:21 +0200 Subject: [PATCH] =?UTF-8?q?Implementaci=C3=B3n=20de=20la=20clase=20IP4Mask?= =?UTF-8?q?=20para=20gestionar=20m=C3=A1scaras=20de=20direcciones=20IPv4?= =?UTF-8?q?=20y=20mejoras=20en=20la=20clase=20Class=5FIP4=20para=20soporta?= =?UTF-8?q?r=20nuevas=20sintaxis=20de=20entrada.=20Se=20actualiza=20el=20a?= =?UTF-8?q?utocompletado=20y=20se=20a=C3=B1aden=20m=C3=A9todos=20para=20ob?= =?UTF-8?q?tener=20la=20m=C3=A1scara=20de=20red=20y=20la=20longitud=20del?= =?UTF-8?q?=20prefijo.=20Se=20mejora=20la=20documentaci=C3=B3n=20de=20ayud?= =?UTF-8?q?a=20contextual.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...Guida_Desarrollo.md => Guia_Desarrollo.md} | 0 ip4_type.py | 317 +++++++++++------- main_calc_app.py | 137 ++++++-- tl_bracket_parser.py | 22 +- 4 files changed, 319 insertions(+), 157 deletions(-) rename .doc/{Guida_Desarrollo.md => Guia_Desarrollo.md} (100%) diff --git a/.doc/Guida_Desarrollo.md b/.doc/Guia_Desarrollo.md similarity index 100% rename from .doc/Guida_Desarrollo.md rename to .doc/Guia_Desarrollo.md diff --git a/ip4_type.py b/ip4_type.py index 38a21c5..e2a1680 100644 --- a/ip4_type.py +++ b/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 \ No newline at end of file + if prefix >= 31: # For /31 and /32, typically 0 usable host addresses in standard subnetting + return 0 + return (2 ** (32 - prefix)) - 2 \ No newline at end of file diff --git a/main_calc_app.py b/main_calc_app.py index ef5a71b..4e91fe8 100644 --- a/main_calc_app.py +++ b/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: diff --git a/tl_bracket_parser.py b/tl_bracket_parser.py index 40de54b..a1e020a 100644 --- a/tl_bracket_parser.py +++ b/tl_bracket_parser.py @@ -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"),