Actualización de la clase IP4Mask con nuevos métodos para obtener la máscara y el conteo de hosts disponibles. Se mejora la integración con la clase Class_IP4 y se ajusta el autocompletado en la aplicación principal. Se modifica la configuración de la ventana y se añaden sugerencias contextuales para el autocompletado.
"""
This commit is contained in:
Miguel 2025-06-02 20:18:10 +02:00
parent 23676b9ef9
commit 036eeb4291
8 changed files with 199 additions and 91 deletions

38
class_base.py Normal file
View File

@ -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)

View File

@ -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]
n.mask()
m=IP4Mask[23]
IP4Mask[22]
IP4[110.1.30.70;255.255.255.0]

View File

@ -1,4 +1,4 @@
{
"window_geometry": "1020x700+2638+160",
"sash_pos_x": 303
"sash_pos_x": 332
}

View File

@ -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
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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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 = {'==', '!=', '<', '<=', '>', '>=', '='}