From 036eeb4291366aec294f91859c1c0ab47e97f51c Mon Sep 17 00:00:00 2001 From: Miguel Date: Mon, 2 Jun 2025 20:18:10 +0200 Subject: [PATCH] =?UTF-8?q?"""=20Actualizaci=C3=B3n=20de=20la=20clase=20IP?= =?UTF-8?q?4Mask=20con=20nuevos=20m=C3=A9todos=20para=20obtener=20la=20m?= =?UTF-8?q?=C3=A1scara=20y=20el=20conteo=20de=20hosts=20disponibles.=20Se?= =?UTF-8?q?=20mejora=20la=20integraci=C3=B3n=20con=20la=20clase=20Class=5F?= =?UTF-8?q?IP4=20y=20se=20ajusta=20el=20autocompletado=20en=20la=20aplicac?= =?UTF-8?q?i=C3=B3n=20principal.=20Se=20modifica=20la=20configuraci=C3=B3n?= =?UTF-8?q?=20de=20la=20ventana=20y=20se=20a=C3=B1aden=20sugerencias=20con?= =?UTF-8?q?textuales=20para=20el=20autocompletado.=20"""?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- class_base.py | 38 ++++++++++ hybrid_calc_history.txt | 10 ++- hybrid_calc_settings.json | 2 +- ip4_type.py | 149 ++++++++++++++++++++++++-------------- main_calc_app.py | 44 ++++++++--- main_evaluation.py | 12 +-- sympy_Base.py | 33 +++++---- tl_bracket_parser.py | 2 +- 8 files changed, 199 insertions(+), 91 deletions(-) create mode 100644 class_base.py diff --git a/class_base.py b/class_base.py new file mode 100644 index 0000000..1cc90a4 --- /dev/null +++ b/class_base.py @@ -0,0 +1,38 @@ +import sympy + +class ClassBase: + """Clase base para todas las clases del sistema""" + + def __init__(self, value, original_str=""): + self._value = value + self._original_str = original_str + + @property + def value(self): + return self._value + + @property + def original_str(self): + return self._original_str + + # Sistema de ayuda y autocompletado + @staticmethod + def Helper(input_str): + """Override en subclases""" + return None + + @staticmethod + def PopupFunctionList(): + """Override en subclases""" + return [] + + # Métodos básicos comunes + def __str__(self): + return str(self._value) + + def __repr__(self): + return f"{self.__class__.__name__}('{self._original_str}')" + +# Necesitaremos importar sympy en los archivos que usen SympyClassBase. +# sympy.sympify también se menciona. +# from sympy import sympify (si es necesario globalmente aquí o en las clases hijas) \ No newline at end of file diff --git a/hybrid_calc_history.txt b/hybrid_calc_history.txt index d559ad4..a0d7769 100644 --- a/hybrid_calc_history.txt +++ b/hybrid_calc_history.txt @@ -12,6 +12,12 @@ Hex[ff] Hex[ff].toDecimal() -IP4[110.1.30.70,255.255.255.0] +n=IP4[110.1.30.70;255.255.255.0] -IP4[110.1.30.70,255.255.255.0] \ No newline at end of file +n.mask() + +m=IP4Mask[23] + +IP4Mask[22] + +IP4[110.1.30.70;255.255.255.0] \ No newline at end of file diff --git a/hybrid_calc_settings.json b/hybrid_calc_settings.json index bf1a2fc..8ab1f30 100644 --- a/hybrid_calc_settings.json +++ b/hybrid_calc_settings.json @@ -1,4 +1,4 @@ { "window_geometry": "1020x700+2638+160", - "sash_pos_x": 303 + "sash_pos_x": 332 } \ No newline at end of file diff --git a/ip4_type.py b/ip4_type.py index e2a1680..0abe240 100644 --- a/ip4_type.py +++ b/ip4_type.py @@ -1,11 +1,13 @@ """ Clase híbrida para direcciones IPv4 """ +from class_base import ClassBase from sympy_Base import SympyClassBase from typing import Optional, Union import re +import sympy -class IP4Mask: +class IP4Mask(ClassBase): """ 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"). @@ -14,27 +16,32 @@ class IP4Mask: _mask_int: int def __init__(self, mask_input: Union[int, str]): + prefix = self._parse_mask(mask_input) + super().__init__(prefix, str(mask_input)) + self._prefix = prefix + self._mask_int = self._prefix_to_mask_int(self._prefix) + + def _parse_mask(self, mask_input: Union[int, str]) -> int: + """Helper to parse mask_input and return prefix.""" + prefix_val: int 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 + prefix_val = 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): + parsed_int = int(mask_input) + if not (0 <= parsed_int <= 32): raise ValueError(f"Invalid prefix string: '{mask_input}'. Must be between 0 and 32.") - self._prefix = prefix_val + prefix_val = parsed_int 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: + parsed_prefix_from_str = self._netmask_str_to_prefix(mask_input) + if parsed_prefix_from_str is None: raise ValueError(f"Invalid netmask string format: '{mask_input}'.") - self._prefix = parsed_prefix + prefix_val = parsed_prefix_from_str 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) + return prefix_val @staticmethod def _is_valid_ip_octet_str(octet_str: str) -> bool: @@ -90,17 +97,61 @@ class IP4Mask: return self._prefix == other._prefix return False + @staticmethod + def Helper(input_str): + if re.match(r"^\s*IP4Mask\\b", input_str, re.IGNORECASE): + return 'Máscara: IP4Mask[24], IP4Mask[255.255.255.0]. Funciones: get_prefix_int(), get_netmask_str(), hosts_count()' + return None + + @staticmethod + def PopupFunctionList(): + return [ + ("get_prefix_int", "Obtiene prefijo CIDR (ej: 24)"), + ("get_netmask_str", "Obtiene máscara como string (ej: 255.255.255.0)"), + ("hosts_count", "Número de hosts disponibles"), + ("to_sympy", "Convierte a expresión SymPy para álgebra"), + ] + + def hosts_count(self): + """Número de hosts disponibles""" + return 2**(32 - self._prefix) - 2 if self._prefix < 31 else 0 + + def to_sympy(self): + """Convierte a SymPy cuando se necesite álgebra""" + return sympy.sympify(self._prefix) + class Class_IP4(SympyClassBase): """Clase híbrida para direcciones IPv4""" - def __new__(cls, *args): - """Crear objeto SymPy válido""" - obj = SympyClassBase.__new__(cls) + def __new__(cls, *args, **kwargs): + temp_ip_str_candidate = args[0] if args else "" + temp_ip_str = "" + if isinstance(temp_ip_str_candidate, str): + if '/' in temp_ip_str_candidate: + temp_ip_str = temp_ip_str_candidate.split('/', 1)[0].strip() + elif ' ' in temp_ip_str_candidate: + temp_ip_str = temp_ip_str_candidate.split(' ', 1)[0].strip() + else: + temp_ip_str = temp_ip_str_candidate.strip() + + if IP4Mask._is_valid_ip_string(temp_ip_str): + ip_parts_for_new = [int(x) for x in temp_ip_str.split('.')] + ip_int_for_new = (ip_parts_for_new[0] << 24) | \ + (ip_parts_for_new[1] << 16) | \ + (ip_parts_for_new[2] << 8) | \ + ip_parts_for_new[3] + obj = SympyClassBase.__new__(cls, ip_int_for_new) + else: + raise ValueError(f"Invalid IP address format for __new__: {temp_ip_str}") + else: + if not args: + raise ValueError("IP4 constructor requires arguments.") + obj = SympyClassBase.__new__(cls, *args) + return obj def __init__(self, *args): - """Inicialización de IP4""" - self._raw_constructor_args = args # Store for __repr__ + self._raw_constructor_args = args ip_str: str self._mask_obj: Optional[IP4Mask] = None @@ -114,109 +165,93 @@ class Class_IP4(SympyClassBase): 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() + 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 mask after space, it's just an IP - else: # Just an IP string + else: 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 + if isinstance(mask_arg, IP4Mask): self._mask_obj = mask_arg - elif isinstance(mask_arg, (str, int)): # Parser will pass str, programmatic use might pass int + elif isinstance(mask_arg, (str, 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 + if not IP4Mask._is_valid_ip_string(ip_str): raise ValueError(f"Invalid IP address format: {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] - # 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 + if isinstance(mask_arg_for_repr, IP4Mask): 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" + else: + mask_repr_str = str(mask_arg_for_repr) sympy_base_original_str = f"{args[0]};{mask_repr_str}" - else: # Only ip_str was derived, no mask (or error caught earlier) + else: 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._mask_obj: return f"{self._ip_str}/{self._mask_obj.get_prefix_int()}" return self._ip_str def _sympystr(self, printer): - """Representación SymPy string""" return str(self) @staticmethod 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], IP4[172.16.0.5;255.255.0.0]\n' - 'Funciones: NetworkAddress(), BroadcastAddress(), Nodes(), get_netmask_str(), get_prefix_length()') + 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], IP4[172.16.0.5;255.255.0.0]\\n' + 'Funciones: NetworkAddress(), BroadcastAddress(), Nodes(), get_netmask_str(), get_prefix_length(), mask()') return None @staticmethod def PopupFunctionList(): - """Lista de métodos sugeridos para autocompletado de IP4""" return [ - ("NetworkAddress", "Obtiene la dirección de red"), - ("BroadcastAddress", "Obtiene la dirección de broadcast"), - ("Nodes", "Cantidad de nodos usables en la subred"), + ("NetworkAddress", "Dirección de red"), + ("BroadcastAddress", "Dirección de broadcast"), + ("Nodes", "Hosts disponibles"), + ("mask", "Objeto máscara con sus métodos"), ("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""" prefix = self.get_prefix_length() if prefix is None: raise ValueError("No prefix/mask defined for NetworkAddress calculation.") @@ -231,10 +266,9 @@ class Class_IP4(SympyClassBase): network_int & 0xFF ] 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(network_str, prefix) def BroadcastAddress(self): - """Obtiene la dirección de broadcast""" prefix = self.get_prefix_length() if prefix is None: raise ValueError("No prefix/mask defined for BroadcastAddress calculation.") @@ -249,14 +283,19 @@ class Class_IP4(SympyClassBase): broadcast_int & 0xFF ] broadcast_str = '.'.join(str(x) for x in parts) - return Class_IP4(broadcast_str, prefix) # Return new IP4 object for broadcast + return Class_IP4(broadcast_str, prefix) def Nodes(self): - """Obtiene el número de nodos disponibles""" prefix = self.get_prefix_length() if prefix is None: raise ValueError("No prefix/mask defined for Nodes calculation.") - if prefix >= 31: # For /31 and /32, typically 0 usable host addresses in standard subnetting + if prefix >= 31: return 0 - return (2 ** (32 - prefix)) - 2 \ No newline at end of file + return (2 ** (32 - prefix)) - 2 + + def mask(self) -> Optional[IP4Mask]: + """Retorna objeto máscara para autocompletado""" + if not self._mask_obj: + raise ValueError("No mask defined for this IP4 object.") + return self._mask_obj \ No newline at end of file diff --git a/main_calc_app.py b/main_calc_app.py index 4e91fe8..803e96d 100644 --- a/main_calc_app.py +++ b/main_calc_app.py @@ -300,13 +300,11 @@ class HybridCalculatorApp: current_line_num = int(line_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: print("DEBUG: Autocomplete: Cursor at beginning of line after dot. No action.") return - # Í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}") stripped_text_before_dot = text_on_line_up_to_dot.strip() @@ -315,23 +313,45 @@ class HybridCalculatorApp: if not stripped_text_before_dot: print("DEBUG: Dot on empty line or after spaces. Offering global suggestions.") 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_types_suggestions) + # MODIFIED: Get suggestions from HybridEvaluationEngine's base_context + if hasattr(self.engine, 'base_context') and isinstance(self.engine.base_context, dict): + for name, class_or_func in self.engine.base_context.items(): + # Solo queremos clases (tipos) y funciones para el autocompletado global principal. + # Evitamos alias en minúscula si la versión capitalizada ya está (heurística simple). + if name[0].isupper(): # Prioritize capitalized names for classes/main functions + hint = f"Tipo o función: {name}" + if hasattr(class_or_func, '__doc__') and class_or_func.__doc__: + first_line_doc = class_or_func.__doc__.strip().split('\n')[0] + hint = f"{name} - {first_line_doc}" + elif hasattr(class_or_func, 'Helper'): # Usar Helper si está disponible + # Para obtener un hint del Helper, necesitamos llamarlo. + # Algunas clases Helper esperan el nombre de la clase. + try: + helper_text = class_or_func.Helper(name) # Pasar el nombre de la clase + if helper_text: + hint = helper_text.split('\n')[0] # Primera línea del helper + except Exception as e_helper: + print(f"DEBUG: Error calling Helper for {name}: {e_helper}") + pass # Mantener el hint genérico + suggestions.append((name, hint)) + + # Añadir funciones de SympyHelper (si no están ya en base_context de forma similar) + # Considerar si SympyHelper.PopupFunctionList() devuelve cosas ya cubiertas. try: sympy_functions = SympyHelper.PopupFunctionList() if sympy_functions: - suggestions.extend(sympy_functions) + # Evitar duplicados si los nombres ya están de base_context + current_suggestion_names = {s[0] for s in suggestions} + for fname, fhint in sympy_functions: + if fname not in current_suggestion_names: + suggestions.append((fname, fhint)) except Exception as e: print(f"DEBUG: Error calling SympyHelper.PopupFunctionList() for global: {e}") if suggestions: + # Ordenar alfabéticamente para consistencia + suggestions.sort(key=lambda x: x[0]) self._show_autocomplete_popup(suggestions, is_global_popup=True) return diff --git a/main_evaluation.py b/main_evaluation.py index b367d86..696a8fb 100644 --- a/main_evaluation.py +++ b/main_evaluation.py @@ -11,11 +11,11 @@ from contextlib import contextmanager from tl_bracket_parser import BracketParser from tl_popup import PlotResult from sympy_Base import SympyClassBase -from ip4_type import Class_IP4 as Class_IP4 -from hex_type import Class_Hex as Class_Hex -from bin_type import Class_Bin as Class_Bin -from dec_type import Class_Dec as Class_Dec -from chr_type import Class_Chr as Class_Chr +from ip4_type import Class_IP4, IP4Mask +from hex_type import Class_Hex +from bin_type import Class_Bin +from dec_type import Class_Dec +from chr_type import Class_Chr class HybridEvaluationEngine: @@ -90,12 +90,14 @@ class HybridEvaluationEngine: 'Dec': Class_Dec, 'IP4': Class_IP4, 'Chr': Class_Chr, + 'IP4Mask': IP4Mask, # Alias en minúsculas 'hex': Class_Hex, 'bin': Class_Bin, 'dec': Class_Dec, 'ip4': Class_IP4, 'chr': Class_Chr, + 'ip4mask': IP4Mask, } # Funciones de utilidad diff --git a/sympy_Base.py b/sympy_Base.py index f0a49b1..7595682 100644 --- a/sympy_Base.py +++ b/sympy_Base.py @@ -5,24 +5,31 @@ import sympy from sympy import Basic, Symbol, sympify from typing import Any, Optional, Dict import re +from class_base import ClassBase -class SympyClassBase(Basic): - """ - Clase base híbrida que combina SymPy Basic con funcionalidad de calculadora - Todas las clases especializadas deben heredar de esta - """ +class SympyClassBase(ClassBase, sympy.Basic): + """Para clases que necesitan álgebra completa""" def __new__(cls, *args, **kwargs): - """Crear objeto SymPy válido""" - obj = Basic.__new__(cls) + # La subclase (ej. Class_IP4) es responsable de pasar los argumentos correctos + # que sympy.Basic.__new__ espera. + obj = sympy.Basic.__new__(cls, *args, **kwargs) return obj def __init__(self, value, original_str=""): - """Inicialización de funcionalidad especializada""" - self._value = value - self._original_str = original_str - self._init_specialized() + ClassBase.__init__(self, value, original_str) + # La inicialización de sympy.Basic se maneja principalmente a través de __new__. + # Atributos adicionales de sympy como self.args son establecidos por sympy.Basic + # basado en los argumentos pasados a __new__. + + def _sympystr(self, printer): + # Las subclases deben implementar esto para una representación SymPy adecuada. + # Por defecto, usa el __str__ de ClassBase, que es str(self.value). + # Si self.value es un objeto Sympy, str(self.value) ya dará una buena representación. + # Si self.value no es un objeto Sympy pero se desea una forma SymPy específica, + # esto debería ser sobreescrito. + return str(self.value) def _init_specialized(self): """Override en subclases para inicialización especializada""" @@ -49,10 +56,6 @@ class SympyClassBase(Basic): """Función constructora para SymPy""" return self.__class__ - def _sympystr(self, printer): - """Representación SymPy string""" - return f"{self.__class__.__name__}({self._original_str})" - def _latex(self, printer): """Representación LaTeX""" return self._sympystr(printer) diff --git a/tl_bracket_parser.py b/tl_bracket_parser.py index a1e020a..51502fb 100644 --- a/tl_bracket_parser.py +++ b/tl_bracket_parser.py @@ -10,7 +10,7 @@ class BracketParser: """Parser que convierte sintaxis con corchetes y detecta ecuaciones contextualmente""" # Clases que soportan sintaxis con corchetes - BRACKET_CLASSES = {'IP4', 'Hex', 'Bin', 'Date', 'Dec', 'Chr'} + BRACKET_CLASSES = {'IP4', 'Hex', 'Bin', 'Date', 'Dec', 'Chr', 'IP4Mask'} # Operadores de comparación que pueden formar ecuaciones EQUATION_OPERATORS = {'==', '!=', '<', '<=', '>', '>=', '='}