Refactorizado de archivos y nombres
This commit is contained in:
parent
3aadae3dc6
commit
261b20df5c
|
@ -1,6 +1,67 @@
|
||||||
# Calculadora MAV - CAS Híbrido
|
# Calculadora MAV - CAS Híbrido
|
||||||
|
|
||||||
Sistema de Álgebra Computacional híbrido que combina SymPy con clases especializadas.
|
## Estructura del Proyecto
|
||||||
|
|
||||||
|
```
|
||||||
|
📁 Calcv2/
|
||||||
|
├── main_calc.py # 🚀 Launcher principal
|
||||||
|
├── main_calc_app.py # 🖥️ Interfaz gráfica
|
||||||
|
├── main_evaluation.py # 🧮 Motor CAS
|
||||||
|
├── hybrid_base.py # 🏗️ Clase base
|
||||||
|
├── ip4_type.py # 🌐 Clase IP4
|
||||||
|
├── hex_type.py # 🔢 Clase Hex
|
||||||
|
├── bin_type.py # 🔢 Clase Bin
|
||||||
|
├── dec_type.py # 🔢 Clase Dec
|
||||||
|
├── chr_type.py # 🔤 Clase Chr
|
||||||
|
├── tl_bracket_parser.py # 📝 Parser sintaxis
|
||||||
|
└── tl_popup.py # 📊 Resultados clickeables
|
||||||
|
```
|
||||||
|
|
||||||
|
## Inicio Rápido
|
||||||
|
|
||||||
|
1. Instalar dependencias:
|
||||||
|
```bash
|
||||||
|
pip install sympy matplotlib numpy
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Ejecutar la aplicación:
|
||||||
|
```bash
|
||||||
|
python main_calc.py
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Ejemplos de uso:
|
||||||
|
```python
|
||||||
|
# Tipos especializados
|
||||||
|
Hex[FF] + 1
|
||||||
|
IP4[192.168.1.100/24].NetworkAddress[]
|
||||||
|
Bin[1010] * 2
|
||||||
|
|
||||||
|
# Matemáticas simbólicas
|
||||||
|
x + 2*y
|
||||||
|
diff(x**2 + sin(x), x)
|
||||||
|
integrate(x**2, x)
|
||||||
|
|
||||||
|
# Ecuaciones
|
||||||
|
x**2 + 2*x - 8 = 0
|
||||||
|
solve(x**2 + 2*x - 8, x)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Características
|
||||||
|
|
||||||
|
- ✅ Motor algebraico completo (SymPy)
|
||||||
|
- ✅ Sintaxis simplificada con corchetes
|
||||||
|
- ✅ Detección automática de ecuaciones
|
||||||
|
- ✅ Resultados interactivos clickeables
|
||||||
|
- ✅ Tipos especializados (IP4, Hex, Bin, etc.)
|
||||||
|
- ✅ Variables SymPy puras
|
||||||
|
- ✅ Plotting integrado
|
||||||
|
|
||||||
|
## Comandos Útiles
|
||||||
|
|
||||||
|
- `python main_calc.py --help` - Muestra ayuda
|
||||||
|
- `python main_calc.py --test` - Ejecuta tests
|
||||||
|
- `python main_calc.py --setup` - Instala dependencias
|
||||||
|
- `python main_calc.py --debug` - Activa logging detallado
|
||||||
|
|
||||||
## 🚀 Inicio Rápido
|
## 🚀 Inicio Rápido
|
||||||
|
|
||||||
|
|
|
@ -298,6 +298,153 @@ solve([x + y == 10, x - y == 2], [x, y]) # Resultado: "📋 Ver Soluciones"
|
||||||
- **Ejemplos interactivos**: Templates para operaciones comunes
|
- **Ejemplos interactivos**: Templates para operaciones comunes
|
||||||
- **Cheat sheet**: Referencia rápida de sintaxis nueva vs antigua
|
- **Cheat sheet**: Referencia rápida de sintaxis nueva vs antigua
|
||||||
|
|
||||||
|
|
||||||
|
## Sistema de Autocompletado y Ayuda Contextual (Helpers)
|
||||||
|
|
||||||
|
### Objetivo
|
||||||
|
|
||||||
|
Proveer al usuario de una experiencia de ayuda y autocompletado inteligente, no invasiva y extensible, que facilite el uso de funciones avanzadas y objetos personalizados, integrando tanto métodos propios como todas las funciones de SymPy.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1. **Helpers: Ayuda Contextual por Tipo**
|
||||||
|
|
||||||
|
Cada tipo de objeto (por ejemplo, `IP4`, `Hex`, etc.) debe definir una función `Helper(input_str)` que:
|
||||||
|
|
||||||
|
- Recibe el string de entrada del usuario.
|
||||||
|
- Decide, usando su propia lógica (por ejemplo, expresiones regulares), si puede ofrecer una ayuda relevante.
|
||||||
|
- Devuelve un string de ayuda (ejemplo de uso, sintaxis, sugerencia) o `None` si no aplica.
|
||||||
|
|
||||||
|
**Ejemplo:**
|
||||||
|
```python
|
||||||
|
# En ip4_type.py
|
||||||
|
def Helper(input_str):
|
||||||
|
if re.match(r"^\s*IP4(\b|\s*\[.*)?", 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]'
|
||||||
|
return None
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ventajas:**
|
||||||
|
- Cada Helper es autocontenido y puede evolucionar de forma independiente.
|
||||||
|
- Permite personalizar la ayuda para cada tipo de objeto o función.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. **Helper para SymPy**
|
||||||
|
|
||||||
|
Dado que ahora todas las funciones de SymPy están disponibles, se debe agregar un Helper general para SymPy que:
|
||||||
|
|
||||||
|
- Detecte si el usuario está comenzando a escribir el nombre de una función de SymPy (por ejemplo, `diff`, `integrate`, `solve`, `limit`, etc.).
|
||||||
|
- Sugiera la sintaxis básica y ejemplos de uso para la función detectada.
|
||||||
|
- Puede usar una lista de funciones comunes de SymPy y sus descripciones.
|
||||||
|
|
||||||
|
**Ejemplo:**
|
||||||
|
```python
|
||||||
|
# En sympy_helper.py
|
||||||
|
def Helper(input_str):
|
||||||
|
sympy_funcs = {
|
||||||
|
"diff": "Derivada: diff(expr, var). Ej: diff(sin(x), x)",
|
||||||
|
"integrate": "Integral: integrate(expr, var). Ej: integrate(x**2, x)",
|
||||||
|
"solve": "Resolver ecuaciones: solve(expr, var). Ej: solve(x**2-1, x)",
|
||||||
|
"limit": "Límite: limit(expr, var, valor). Ej: limit(sin(x)/x, x, 0)",
|
||||||
|
# ...agregar más funciones comunes
|
||||||
|
}
|
||||||
|
for func, ayuda in sympy_funcs.items():
|
||||||
|
if input_str.strip().startswith(func):
|
||||||
|
return ayuda
|
||||||
|
return None
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. **Manejo Centralizado de Helpers**
|
||||||
|
|
||||||
|
En el motor principal de la aplicación, se debe mantener una lista de todas las funciones Helper disponibles (incluyendo la de SymPy).
|
||||||
|
Al evaluar la línea de entrada:
|
||||||
|
|
||||||
|
- Se llama a cada Helper en orden.
|
||||||
|
- Si alguna Helper retorna una ayuda, se muestra esa ayuda al usuario (en la línea de resultado, tooltip, etc.).
|
||||||
|
- Si ninguna Helper ayuda, se muestra el mensaje de error real.
|
||||||
|
|
||||||
|
**Ejemplo:**
|
||||||
|
```python
|
||||||
|
HELPERS = [
|
||||||
|
IP4.Helper,
|
||||||
|
Hex.Helper,
|
||||||
|
SympyHelper.Helper,
|
||||||
|
# ...otros helpers
|
||||||
|
]
|
||||||
|
|
||||||
|
def obtener_ayuda(input_str):
|
||||||
|
for helper in HELPERS:
|
||||||
|
ayuda = helper(input_str)
|
||||||
|
if ayuda:
|
||||||
|
return ayuda
|
||||||
|
return None
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. **Autocompletado de Métodos y Funciones (Popup tras el punto)**
|
||||||
|
|
||||||
|
- Cuando el usuario escribe un punto (`.`) después de un objeto válido, se evalúa el objeto y se obtiene la lista de métodos disponibles.
|
||||||
|
- Se muestra un popup de autocompletado con los métodos relevantes (filtrando los no útiles).
|
||||||
|
- El usuario puede seleccionar un método con el teclado o mouse, y se inserta automáticamente (con paréntesis si corresponde).
|
||||||
|
- El popup solo aparece tras el punto, no en cada pulsación de tecla, para no ser invasivo.
|
||||||
|
|
||||||
|
**Puntos clave:**
|
||||||
|
- Priorizar métodos útiles y públicos.
|
||||||
|
- Permitir que cada tipo defina una lista de métodos sugeridos.
|
||||||
|
- Cerrar el popup fácilmente (Escape, clic fuera, etc.).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. **Flujo de Evaluación y Ayuda**
|
||||||
|
|
||||||
|
1. El usuario escribe una línea.
|
||||||
|
2. Si presiona `.`, se muestra el popup de autocompletado de métodos.
|
||||||
|
3. Si la línea tiene error:
|
||||||
|
- Se consulta a los Helpers para mostrar ayuda contextual.
|
||||||
|
- Si ninguna Helper ayuda, se muestra el mensaje de error real.
|
||||||
|
4. Si la línea es válida, se evalúa normalmente.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. **Extensibilidad**
|
||||||
|
|
||||||
|
- Para agregar ayuda para un nuevo tipo, solo se debe definir un nuevo Helper y registrarlo en la lista central.
|
||||||
|
- El Helper de SymPy puede expandirse para cubrir más funciones y ejemplos.
|
||||||
|
- El sistema de autocompletado puede ampliarse para sugerir funciones de SymPy al escribir el nombre de la función (no solo tras el punto).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. **Resumen de Beneficios**
|
||||||
|
|
||||||
|
- **No invasivo:** El autocompletado solo aparece cuando el usuario lo solicita (tras el punto).
|
||||||
|
- **Ayuda contextual:** Los Helpers ofrecen ayuda específica y relevante según el contexto.
|
||||||
|
- **Extensible:** Fácil de agregar nuevos tipos y funciones de ayuda.
|
||||||
|
- **Consistente:** El usuario recibe ayuda o autocompletado solo cuando es útil, no en cada pulsación.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8. **Ejemplo de Integración**
|
||||||
|
|
||||||
|
```python
|
||||||
|
# En el motor principal:
|
||||||
|
ayuda = obtener_ayuda(linea_usuario)
|
||||||
|
if ayuda:
|
||||||
|
mostrar_ayuda(ayuda)
|
||||||
|
elif error:
|
||||||
|
mostrar_error(error)
|
||||||
|
else:
|
||||||
|
mostrar_resultado(resultado)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Este sistema permite que la ayuda y el autocompletado evolucionen de forma incremental, mejorando la experiencia del usuario sin ser molestos ni invasivos.**
|
||||||
|
|
||||||
|
|
||||||
### **Gestión de Variables Puras SymPy**
|
### **Gestión de Variables Puras SymPy**
|
||||||
- **Solo símbolos SymPy**: Todas las variables son `Symbol()` automáticamente
|
- **Solo símbolos SymPy**: Todas las variables son `Symbol()` automáticamente
|
||||||
- **Sin variables Python**: Eliminación de `eval()` como mecanismo por defecto
|
- **Sin variables Python**: Eliminación de `eval()` como mecanismo por defecto
|
||||||
|
|
|
@ -0,0 +1,52 @@
|
||||||
|
"""
|
||||||
|
Clase híbrida para números binarios
|
||||||
|
"""
|
||||||
|
from hybrid_base import HybridCalcType
|
||||||
|
|
||||||
|
|
||||||
|
class HybridBin(HybridCalcType):
|
||||||
|
"""Clase híbrida para números binarios"""
|
||||||
|
|
||||||
|
def __new__(cls, value_input):
|
||||||
|
"""Crear objeto SymPy válido"""
|
||||||
|
obj = HybridCalcType.__new__(cls)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def __init__(self, value_input):
|
||||||
|
"""Inicialización de Bin"""
|
||||||
|
# Convertir input a entero
|
||||||
|
if isinstance(value_input, str):
|
||||||
|
# Remover prefijo 0b si existe
|
||||||
|
if value_input.startswith('0b'):
|
||||||
|
value_input = value_input[2:]
|
||||||
|
# Convertir a entero
|
||||||
|
value = int(value_input, 2)
|
||||||
|
else:
|
||||||
|
value = int(value_input)
|
||||||
|
|
||||||
|
# Llamar al constructor base
|
||||||
|
super().__init__(value, f"0b{value:08b}")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
"""Representación string para display"""
|
||||||
|
return f"0b{self._value:08b}"
|
||||||
|
|
||||||
|
def _sympystr(self, printer):
|
||||||
|
"""Representación SymPy string"""
|
||||||
|
return str(self)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def Helper(input_str):
|
||||||
|
"""Ayuda contextual para Bin"""
|
||||||
|
return """
|
||||||
|
Formato Bin:
|
||||||
|
- Con prefijo: 0b1010
|
||||||
|
- Sin prefijo: 1010
|
||||||
|
|
||||||
|
Conversiones:
|
||||||
|
- toDecimal(): Convierte a decimal
|
||||||
|
"""
|
||||||
|
|
||||||
|
def toDecimal(self):
|
||||||
|
"""Convierte a decimal"""
|
||||||
|
return self._value
|
|
@ -0,0 +1,63 @@
|
||||||
|
"""
|
||||||
|
Clase híbrida para caracteres
|
||||||
|
"""
|
||||||
|
from hybrid_base import HybridCalcType
|
||||||
|
|
||||||
|
|
||||||
|
class HybridChr(HybridCalcType):
|
||||||
|
"""Clase híbrida para caracteres"""
|
||||||
|
|
||||||
|
def __new__(cls, str_input):
|
||||||
|
"""Crear objeto SymPy válido"""
|
||||||
|
obj = HybridCalcType.__new__(cls)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def __init__(self, str_input: str):
|
||||||
|
"""Inicialización de Chr"""
|
||||||
|
# Convertir input a entero (código ASCII)
|
||||||
|
if isinstance(str_input, str):
|
||||||
|
if len(str_input) == 1:
|
||||||
|
value = ord(str_input)
|
||||||
|
else:
|
||||||
|
raise ValueError("Input must be a single character")
|
||||||
|
else:
|
||||||
|
value = int(str_input)
|
||||||
|
if not 0 <= value <= 255:
|
||||||
|
raise ValueError("Value must be between 0 and 255")
|
||||||
|
|
||||||
|
# Llamar al constructor base
|
||||||
|
super().__init__(value, chr(value))
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
"""Representación string para display"""
|
||||||
|
return f"'{self._original_str}' ({self._value})"
|
||||||
|
|
||||||
|
def _sympystr(self, printer):
|
||||||
|
"""Representación SymPy string"""
|
||||||
|
return str(self)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def Helper(input_str):
|
||||||
|
"""Ayuda contextual para Chr"""
|
||||||
|
return """
|
||||||
|
Formato Chr:
|
||||||
|
- Carácter: 'A'
|
||||||
|
- Código ASCII: 65
|
||||||
|
|
||||||
|
Conversiones:
|
||||||
|
- toDecimal(): Obtiene código ASCII
|
||||||
|
- toHex(): Convierte a hexadecimal
|
||||||
|
- toBin(): Convierte a binario
|
||||||
|
"""
|
||||||
|
|
||||||
|
def toDecimal(self):
|
||||||
|
"""Obtiene código ASCII"""
|
||||||
|
return self._value
|
||||||
|
|
||||||
|
def toHex(self):
|
||||||
|
"""Convierte a hexadecimal"""
|
||||||
|
return f"0x{self._value:02x}"
|
||||||
|
|
||||||
|
def toBin(self):
|
||||||
|
"""Convierte a binario"""
|
||||||
|
return f"0b{self._value:08b}"
|
|
@ -0,0 +1,52 @@
|
||||||
|
"""
|
||||||
|
Clase híbrida para números decimales
|
||||||
|
"""
|
||||||
|
from hybrid_base import HybridCalcType
|
||||||
|
|
||||||
|
|
||||||
|
class HybridDec(HybridCalcType):
|
||||||
|
"""Clase híbrida para números decimales"""
|
||||||
|
|
||||||
|
def __new__(cls, value_input):
|
||||||
|
"""Crear objeto SymPy válido"""
|
||||||
|
obj = HybridCalcType.__new__(cls)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def __init__(self, value_input):
|
||||||
|
"""Inicialización de Dec"""
|
||||||
|
# Convertir input a entero
|
||||||
|
if isinstance(value_input, str):
|
||||||
|
value = int(value_input)
|
||||||
|
else:
|
||||||
|
value = int(value_input)
|
||||||
|
|
||||||
|
# Llamar al constructor base
|
||||||
|
super().__init__(value, str(value))
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
"""Representación string para display"""
|
||||||
|
return str(self._value)
|
||||||
|
|
||||||
|
def _sympystr(self, printer):
|
||||||
|
"""Representación SymPy string"""
|
||||||
|
return str(self)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def Helper(input_str):
|
||||||
|
"""Ayuda contextual para Dec"""
|
||||||
|
return """
|
||||||
|
Formato Dec:
|
||||||
|
- Número entero: 42
|
||||||
|
|
||||||
|
Conversiones:
|
||||||
|
- toHex(): Convierte a hexadecimal
|
||||||
|
- toBin(): Convierte a binario
|
||||||
|
"""
|
||||||
|
|
||||||
|
def toHex(self):
|
||||||
|
"""Convierte a hexadecimal"""
|
||||||
|
return f"0x{self._value:02x}"
|
||||||
|
|
||||||
|
def toBin(self):
|
||||||
|
"""Convierte a binario"""
|
||||||
|
return f"0b{self._value:08b}"
|
|
@ -0,0 +1,52 @@
|
||||||
|
"""
|
||||||
|
Clase híbrida para números hexadecimales
|
||||||
|
"""
|
||||||
|
from hybrid_base import HybridCalcType
|
||||||
|
|
||||||
|
|
||||||
|
class HybridHex(HybridCalcType):
|
||||||
|
"""Clase híbrida para números hexadecimales"""
|
||||||
|
|
||||||
|
def __new__(cls, value_input):
|
||||||
|
"""Crear objeto SymPy válido"""
|
||||||
|
obj = HybridCalcType.__new__(cls)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def __init__(self, value_input):
|
||||||
|
"""Inicialización de Hex"""
|
||||||
|
# Convertir input a entero
|
||||||
|
if isinstance(value_input, str):
|
||||||
|
# Remover prefijo 0x si existe
|
||||||
|
if value_input.startswith('0x'):
|
||||||
|
value_input = value_input[2:]
|
||||||
|
# Convertir a entero
|
||||||
|
value = int(value_input, 16)
|
||||||
|
else:
|
||||||
|
value = int(value_input)
|
||||||
|
|
||||||
|
# Llamar al constructor base
|
||||||
|
super().__init__(value, f"0x{value:02x}")
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
"""Representación string para display"""
|
||||||
|
return f"0x{self._value:02x}"
|
||||||
|
|
||||||
|
def _sympystr(self, printer):
|
||||||
|
"""Representación SymPy string"""
|
||||||
|
return str(self)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def Helper(input_str):
|
||||||
|
"""Ayuda contextual para Hex"""
|
||||||
|
return """
|
||||||
|
Formato Hex:
|
||||||
|
- Con prefijo: 0x1A
|
||||||
|
- Sin prefijo: 1A
|
||||||
|
|
||||||
|
Conversiones:
|
||||||
|
- toDecimal(): Convierte a decimal
|
||||||
|
"""
|
||||||
|
|
||||||
|
def toDecimal(self):
|
||||||
|
"""Convierte a decimal"""
|
||||||
|
return self._value
|
|
@ -0,0 +1,246 @@
|
||||||
|
"""
|
||||||
|
Clase base híbrida que combina SymPy con funcionalidad especializada
|
||||||
|
"""
|
||||||
|
import sympy
|
||||||
|
from sympy import Basic, Symbol, sympify
|
||||||
|
from typing import Any, Optional, Dict
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
class HybridCalcType(Basic):
|
||||||
|
"""
|
||||||
|
Clase base híbrida que combina SymPy Basic con funcionalidad de calculadora
|
||||||
|
Todas las clases especializadas deben heredar de esta
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __new__(cls, *args, **kwargs):
|
||||||
|
"""Crear objeto SymPy válido"""
|
||||||
|
obj = Basic.__new__(cls)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
def __init__(self, value, original_str=""):
|
||||||
|
"""Inicialización de funcionalidad especializada"""
|
||||||
|
self._value = value
|
||||||
|
self._original_str = original_str
|
||||||
|
self._init_specialized()
|
||||||
|
|
||||||
|
def _init_specialized(self):
|
||||||
|
"""Override en subclases para inicialización especializada"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def value(self):
|
||||||
|
"""Acceso al valor interno"""
|
||||||
|
return self._value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def original_str(self):
|
||||||
|
"""String original de entrada"""
|
||||||
|
return self._original_str
|
||||||
|
|
||||||
|
# Propiedades requeridas por SymPy
|
||||||
|
@property
|
||||||
|
def args(self):
|
||||||
|
"""Argumentos para SymPy - retorna valor como argumento"""
|
||||||
|
return (sympify(self._value),)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def func(self):
|
||||||
|
"""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)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
"""Representación string para display"""
|
||||||
|
return str(self._value)
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
"""Representación para debugging"""
|
||||||
|
return f"{self.__class__.__name__}('{self._original_str}')"
|
||||||
|
|
||||||
|
def evalf(self, n=15, **options):
|
||||||
|
"""Evaluación numérica si es posible"""
|
||||||
|
if isinstance(self._value, (int, float, complex)):
|
||||||
|
return sympy.Float(self._value)
|
||||||
|
return self
|
||||||
|
|
||||||
|
def _eval_evalf(self, prec):
|
||||||
|
"""Evaluación numérica para SymPy"""
|
||||||
|
if isinstance(self._value, (int, float, complex)):
|
||||||
|
return sympy.Float(self._value, prec)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def __dec__(self):
|
||||||
|
"""Conversión a decimal para compatibilidad"""
|
||||||
|
if isinstance(self._value, (int, float, complex)):
|
||||||
|
return self._value
|
||||||
|
raise TypeError(f"Cannot convert {self.__class__.__name__} to decimal")
|
||||||
|
|
||||||
|
# Métodos requeridos por SymPy para manipulación algebraica
|
||||||
|
def as_coeff_Mul(self, rational=True):
|
||||||
|
"""Descomponer como coeficiente * términos"""
|
||||||
|
if isinstance(self._value, (int, float)):
|
||||||
|
return sympify(self._value), sympify(1)
|
||||||
|
return sympify(1), self
|
||||||
|
|
||||||
|
def as_coeff_Add(self, rational=True):
|
||||||
|
"""Descomponer como coeficiente + términos"""
|
||||||
|
if isinstance(self._value, (int, float)):
|
||||||
|
return sympify(self._value), sympify(0)
|
||||||
|
return sympify(0), self
|
||||||
|
|
||||||
|
def as_base_exp(self):
|
||||||
|
"""Descomponer como base^exponente"""
|
||||||
|
return self, sympify(1)
|
||||||
|
|
||||||
|
def as_numer_denom(self):
|
||||||
|
"""Descomponer como numerador/denominador"""
|
||||||
|
return self, sympify(1)
|
||||||
|
|
||||||
|
# Propiedades de tipo para SymPy
|
||||||
|
@property
|
||||||
|
def is_number(self):
|
||||||
|
"""Indica si es un número"""
|
||||||
|
return isinstance(self._value, (int, float, complex))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_real(self):
|
||||||
|
"""Indica si es real"""
|
||||||
|
return isinstance(self._value, (int, float)) and not isinstance(self._value, complex)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_integer(self):
|
||||||
|
"""Indica si es entero"""
|
||||||
|
return isinstance(self._value, int)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_rational(self):
|
||||||
|
"""Indica si es racional"""
|
||||||
|
return isinstance(self._value, (int, float))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_irrational(self):
|
||||||
|
"""Indica si es irracional"""
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_positive(self):
|
||||||
|
"""Indica si es positivo"""
|
||||||
|
if isinstance(self._value, (int, float)):
|
||||||
|
return self._value > 0
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_negative(self):
|
||||||
|
"""Indica si es negativo"""
|
||||||
|
if isinstance(self._value, (int, float)):
|
||||||
|
return self._value < 0
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_zero(self):
|
||||||
|
"""Indica si es cero"""
|
||||||
|
if isinstance(self._value, (int, float)):
|
||||||
|
return self._value == 0
|
||||||
|
return None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_finite(self):
|
||||||
|
"""Indica si es finito"""
|
||||||
|
return isinstance(self._value, (int, float, complex))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_infinite(self):
|
||||||
|
"""Indica si es infinito"""
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_commutative(self):
|
||||||
|
"""Indica si es conmutativo"""
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Operaciones aritméticas simples que retornan el valor numérico para integración con SymPy
|
||||||
|
def __add__(self, other):
|
||||||
|
"""Suma que integra con SymPy"""
|
||||||
|
if isinstance(other, (int, float, complex)):
|
||||||
|
# Retornar valor numérico para que SymPy maneje la operación
|
||||||
|
return sympify(self._value + other)
|
||||||
|
elif hasattr(other, '_value'):
|
||||||
|
return sympify(self._value + other._value)
|
||||||
|
else:
|
||||||
|
# Dejar que SymPy maneje la operación
|
||||||
|
return sympify(self._value) + sympify(other)
|
||||||
|
|
||||||
|
def __radd__(self, other):
|
||||||
|
"""Suma reversa"""
|
||||||
|
return sympify(other) + sympify(self._value)
|
||||||
|
|
||||||
|
def __mul__(self, other):
|
||||||
|
"""Multiplicación que integra con SymPy"""
|
||||||
|
if isinstance(other, (int, float, complex)):
|
||||||
|
return sympify(self._value * other)
|
||||||
|
elif hasattr(other, '_value'):
|
||||||
|
return sympify(self._value * other._value)
|
||||||
|
else:
|
||||||
|
return sympify(self._value) * sympify(other)
|
||||||
|
|
||||||
|
def __rmul__(self, other):
|
||||||
|
"""Multiplicación reversa"""
|
||||||
|
return sympify(other) * sympify(self._value)
|
||||||
|
|
||||||
|
def __sub__(self, other):
|
||||||
|
"""Resta"""
|
||||||
|
if isinstance(other, (int, float, complex)):
|
||||||
|
return sympify(self._value - other)
|
||||||
|
elif hasattr(other, '_value'):
|
||||||
|
return sympify(self._value - other._value)
|
||||||
|
else:
|
||||||
|
return sympify(self._value) - sympify(other)
|
||||||
|
|
||||||
|
def __rsub__(self, other):
|
||||||
|
"""Resta reversa"""
|
||||||
|
return sympify(other) - sympify(self._value)
|
||||||
|
|
||||||
|
def __truediv__(self, other):
|
||||||
|
"""División"""
|
||||||
|
if isinstance(other, (int, float, complex)):
|
||||||
|
if other == 0:
|
||||||
|
raise ZeroDivisionError("Division by zero")
|
||||||
|
return sympify(self._value / other)
|
||||||
|
elif hasattr(other, '_value'):
|
||||||
|
if other._value == 0:
|
||||||
|
raise ZeroDivisionError("Division by zero")
|
||||||
|
return sympify(self._value / other._value)
|
||||||
|
else:
|
||||||
|
return sympify(self._value) / sympify(other)
|
||||||
|
|
||||||
|
def __rtruediv__(self, other):
|
||||||
|
"""División reversa"""
|
||||||
|
if self._value == 0:
|
||||||
|
raise ZeroDivisionError("Division by zero")
|
||||||
|
return sympify(other) / sympify(self._value)
|
||||||
|
|
||||||
|
def __pow__(self, other):
|
||||||
|
"""Potencia"""
|
||||||
|
if isinstance(other, (int, float, complex)):
|
||||||
|
return sympify(self._value ** other)
|
||||||
|
elif hasattr(other, '_value'):
|
||||||
|
return sympify(self._value ** other._value)
|
||||||
|
else:
|
||||||
|
return sympify(self._value) ** sympify(other)
|
||||||
|
|
||||||
|
def __rpow__(self, other):
|
||||||
|
"""Potencia reversa"""
|
||||||
|
return sympify(other) ** sympify(self._value)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def Helper(input_str):
|
||||||
|
"""Override en subclases para ayuda contextual"""
|
||||||
|
return None
|
|
@ -1,606 +0,0 @@
|
||||||
"""
|
|
||||||
Clases base híbridas que combinan SymPy con funcionalidad especializada
|
|
||||||
"""
|
|
||||||
import sympy
|
|
||||||
from sympy import Basic, Symbol, sympify
|
|
||||||
from typing import Any, Optional, Dict
|
|
||||||
import re
|
|
||||||
|
|
||||||
|
|
||||||
class HybridCalcType(Basic):
|
|
||||||
"""
|
|
||||||
Clase base híbrida que combina SymPy Basic con funcionalidad de calculadora
|
|
||||||
Todas las clases especializadas deben heredar de esta
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __new__(cls, *args, **kwargs):
|
|
||||||
"""Crear objeto SymPy válido"""
|
|
||||||
obj = Basic.__new__(cls)
|
|
||||||
return obj
|
|
||||||
|
|
||||||
def __init__(self, value, original_str=""):
|
|
||||||
"""Inicialización de funcionalidad especializada"""
|
|
||||||
self._value = value
|
|
||||||
self._original_str = original_str
|
|
||||||
self._init_specialized()
|
|
||||||
|
|
||||||
def _init_specialized(self):
|
|
||||||
"""Override en subclases para inicialización especializada"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@property
|
|
||||||
def value(self):
|
|
||||||
"""Acceso al valor interno"""
|
|
||||||
return self._value
|
|
||||||
|
|
||||||
@property
|
|
||||||
def original_str(self):
|
|
||||||
"""String original de entrada"""
|
|
||||||
return self._original_str
|
|
||||||
|
|
||||||
# Propiedades requeridas por SymPy
|
|
||||||
@property
|
|
||||||
def args(self):
|
|
||||||
"""Argumentos para SymPy - retorna valor como argumento"""
|
|
||||||
return (sympify(self._value),)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def func(self):
|
|
||||||
"""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)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
"""Representación string para display"""
|
|
||||||
return str(self._value)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
"""Representación para debugging"""
|
|
||||||
return f"{self.__class__.__name__}('{self._original_str}')"
|
|
||||||
|
|
||||||
def evalf(self, n=15, **options):
|
|
||||||
"""Evaluación numérica si es posible"""
|
|
||||||
if isinstance(self._value, (int, float, complex)):
|
|
||||||
return sympy.Float(self._value)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def _eval_evalf(self, prec):
|
|
||||||
"""Evaluación numérica para SymPy"""
|
|
||||||
if isinstance(self._value, (int, float, complex)):
|
|
||||||
return sympy.Float(self._value, prec)
|
|
||||||
return None
|
|
||||||
|
|
||||||
def __dec__(self):
|
|
||||||
"""Conversión a decimal para compatibilidad"""
|
|
||||||
if isinstance(self._value, (int, float, complex)):
|
|
||||||
return self._value
|
|
||||||
raise TypeError(f"Cannot convert {self.__class__.__name__} to decimal")
|
|
||||||
|
|
||||||
# Métodos requeridos por SymPy para manipulación algebraica
|
|
||||||
def as_coeff_Mul(self, rational=True):
|
|
||||||
"""Descomponer como coeficiente * términos"""
|
|
||||||
if isinstance(self._value, (int, float)):
|
|
||||||
return sympify(self._value), sympify(1)
|
|
||||||
return sympify(1), self
|
|
||||||
|
|
||||||
def as_coeff_Add(self, rational=True):
|
|
||||||
"""Descomponer como coeficiente + términos"""
|
|
||||||
if isinstance(self._value, (int, float)):
|
|
||||||
return sympify(self._value), sympify(0)
|
|
||||||
return sympify(0), self
|
|
||||||
|
|
||||||
def as_base_exp(self):
|
|
||||||
"""Descomponer como base^exponente"""
|
|
||||||
return self, sympify(1)
|
|
||||||
|
|
||||||
def as_numer_denom(self):
|
|
||||||
"""Descomponer como numerador/denominador"""
|
|
||||||
return self, sympify(1)
|
|
||||||
|
|
||||||
# Propiedades de tipo para SymPy
|
|
||||||
@property
|
|
||||||
def is_number(self):
|
|
||||||
"""Indica si es un número"""
|
|
||||||
return isinstance(self._value, (int, float, complex))
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_real(self):
|
|
||||||
"""Indica si es real"""
|
|
||||||
return isinstance(self._value, (int, float)) and not isinstance(self._value, complex)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_integer(self):
|
|
||||||
"""Indica si es entero"""
|
|
||||||
return isinstance(self._value, int)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_rational(self):
|
|
||||||
"""Indica si es racional"""
|
|
||||||
return isinstance(self._value, (int, float))
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_irrational(self):
|
|
||||||
"""Indica si es irracional"""
|
|
||||||
return False
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_positive(self):
|
|
||||||
"""Indica si es positivo"""
|
|
||||||
if isinstance(self._value, (int, float)):
|
|
||||||
return self._value > 0
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_negative(self):
|
|
||||||
"""Indica si es negativo"""
|
|
||||||
if isinstance(self._value, (int, float)):
|
|
||||||
return self._value < 0
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_zero(self):
|
|
||||||
"""Indica si es cero"""
|
|
||||||
if isinstance(self._value, (int, float)):
|
|
||||||
return self._value == 0
|
|
||||||
return None
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_finite(self):
|
|
||||||
"""Indica si es finito"""
|
|
||||||
return isinstance(self._value, (int, float, complex))
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_infinite(self):
|
|
||||||
"""Indica si es infinito"""
|
|
||||||
return False
|
|
||||||
|
|
||||||
@property
|
|
||||||
def is_commutative(self):
|
|
||||||
"""Indica si es conmutativo"""
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Operaciones aritméticas simples que retornan el valor numérico para integración con SymPy
|
|
||||||
def __add__(self, other):
|
|
||||||
"""Suma que integra con SymPy"""
|
|
||||||
if isinstance(other, (int, float, complex)):
|
|
||||||
# Retornar valor numérico para que SymPy maneje la operación
|
|
||||||
return sympify(self._value + other)
|
|
||||||
elif hasattr(other, '_value'):
|
|
||||||
return sympify(self._value + other._value)
|
|
||||||
else:
|
|
||||||
# Dejar que SymPy maneje la operación
|
|
||||||
return sympify(self._value) + sympify(other)
|
|
||||||
|
|
||||||
def __radd__(self, other):
|
|
||||||
"""Suma reversa"""
|
|
||||||
return sympify(other) + sympify(self._value)
|
|
||||||
|
|
||||||
def __mul__(self, other):
|
|
||||||
"""Multiplicación que integra con SymPy"""
|
|
||||||
if isinstance(other, (int, float, complex)):
|
|
||||||
return sympify(self._value * other)
|
|
||||||
elif hasattr(other, '_value'):
|
|
||||||
return sympify(self._value * other._value)
|
|
||||||
else:
|
|
||||||
return sympify(self._value) * sympify(other)
|
|
||||||
|
|
||||||
def __rmul__(self, other):
|
|
||||||
"""Multiplicación reversa"""
|
|
||||||
return sympify(other) * sympify(self._value)
|
|
||||||
|
|
||||||
def __sub__(self, other):
|
|
||||||
"""Resta"""
|
|
||||||
if isinstance(other, (int, float, complex)):
|
|
||||||
return sympify(self._value - other)
|
|
||||||
elif hasattr(other, '_value'):
|
|
||||||
return sympify(self._value - other._value)
|
|
||||||
else:
|
|
||||||
return sympify(self._value) - sympify(other)
|
|
||||||
|
|
||||||
def __rsub__(self, other):
|
|
||||||
"""Resta reversa"""
|
|
||||||
return sympify(other) - sympify(self._value)
|
|
||||||
|
|
||||||
def __truediv__(self, other):
|
|
||||||
"""División"""
|
|
||||||
if isinstance(other, (int, float, complex)):
|
|
||||||
if other == 0:
|
|
||||||
raise ZeroDivisionError("Division by zero")
|
|
||||||
return sympify(self._value / other)
|
|
||||||
elif hasattr(other, '_value'):
|
|
||||||
if other._value == 0:
|
|
||||||
raise ZeroDivisionError("Division by zero")
|
|
||||||
return sympify(self._value / other._value)
|
|
||||||
else:
|
|
||||||
return sympify(self._value) / sympify(other)
|
|
||||||
|
|
||||||
def __rtruediv__(self, other):
|
|
||||||
"""División reversa"""
|
|
||||||
if self._value == 0:
|
|
||||||
raise ZeroDivisionError("Division by zero")
|
|
||||||
return sympify(other) / sympify(self._value)
|
|
||||||
|
|
||||||
def __pow__(self, other):
|
|
||||||
"""Potencia"""
|
|
||||||
if isinstance(other, (int, float, complex)):
|
|
||||||
return sympify(self._value ** other)
|
|
||||||
elif hasattr(other, '_value'):
|
|
||||||
return sympify(self._value ** other._value)
|
|
||||||
else:
|
|
||||||
return sympify(self._value) ** sympify(other)
|
|
||||||
|
|
||||||
def __rpow__(self, other):
|
|
||||||
"""Potencia reversa"""
|
|
||||||
return sympify(other) ** sympify(self._value)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def Helper(input_str):
|
|
||||||
"""Override en subclases para ayuda contextual"""
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class HybridHex(HybridCalcType):
|
|
||||||
"""Clase híbrida para números hexadecimales"""
|
|
||||||
|
|
||||||
def __new__(cls, value_input):
|
|
||||||
obj = HybridCalcType.__new__(cls)
|
|
||||||
return obj
|
|
||||||
|
|
||||||
def __init__(self, value_input):
|
|
||||||
processed_value = None
|
|
||||||
original_str = str(value_input)
|
|
||||||
|
|
||||||
if isinstance(value_input, HybridCalcType):
|
|
||||||
processed_value = int(value_input.__dec__())
|
|
||||||
elif isinstance(value_input, (int, float)):
|
|
||||||
processed_value = int(value_input)
|
|
||||||
else:
|
|
||||||
# String input
|
|
||||||
str_input = str(value_input).lower()
|
|
||||||
if str_input.startswith("0x"):
|
|
||||||
processed_value = int(str_input, 16)
|
|
||||||
else:
|
|
||||||
processed_value = int(str_input, 16)
|
|
||||||
|
|
||||||
super().__init__(processed_value, original_str)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"0x{self._value:X}"
|
|
||||||
|
|
||||||
def _sympystr(self, printer):
|
|
||||||
return f"Hex({self._value})"
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def Helper(input_str):
|
|
||||||
if re.match(r"^\s*Hex(\b|\s*[\[\(].*)?", input_str, re.IGNORECASE):
|
|
||||||
return 'Ej: Hex[FF] o Hex[255]'
|
|
||||||
return None
|
|
||||||
|
|
||||||
def toDecimal(self):
|
|
||||||
"""Convertir a decimal"""
|
|
||||||
return self._value
|
|
||||||
|
|
||||||
|
|
||||||
class HybridBin(HybridCalcType):
|
|
||||||
"""Clase híbrida para números binarios"""
|
|
||||||
|
|
||||||
def __new__(cls, value_input):
|
|
||||||
obj = HybridCalcType.__new__(cls)
|
|
||||||
return obj
|
|
||||||
|
|
||||||
def __init__(self, value_input):
|
|
||||||
processed_value = None
|
|
||||||
original_str = str(value_input)
|
|
||||||
|
|
||||||
if isinstance(value_input, HybridCalcType):
|
|
||||||
processed_value = int(value_input.__dec__())
|
|
||||||
elif isinstance(value_input, (int, float)):
|
|
||||||
processed_value = int(value_input)
|
|
||||||
else:
|
|
||||||
# String input
|
|
||||||
str_input = str(value_input).lower()
|
|
||||||
if str_input.startswith("0b"):
|
|
||||||
processed_value = int(str_input, 2)
|
|
||||||
else:
|
|
||||||
processed_value = int(str_input, 2)
|
|
||||||
|
|
||||||
super().__init__(processed_value, original_str)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return f"0b{bin(self._value)[2:]}"
|
|
||||||
|
|
||||||
def _sympystr(self, printer):
|
|
||||||
return f"Bin({self._value})"
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def Helper(input_str):
|
|
||||||
if re.match(r"^\s*Bin(\b|\s*[\[\(].*)?", input_str, re.IGNORECASE):
|
|
||||||
return 'Ej: Bin[1011] o Bin[11]'
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class HybridDec(HybridCalcType):
|
|
||||||
"""Clase híbrida para números decimales"""
|
|
||||||
|
|
||||||
def __new__(cls, value_input):
|
|
||||||
obj = HybridCalcType.__new__(cls)
|
|
||||||
return obj
|
|
||||||
|
|
||||||
def __init__(self, value_input):
|
|
||||||
processed_value = None
|
|
||||||
original_str = str(value_input)
|
|
||||||
|
|
||||||
if isinstance(value_input, HybridCalcType):
|
|
||||||
processed_value = float(value_input.__dec__())
|
|
||||||
elif isinstance(value_input, (int, float)):
|
|
||||||
processed_value = float(value_input)
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
processed_value = float(str(value_input))
|
|
||||||
except (ValueError, TypeError) as e:
|
|
||||||
raise ValueError(f"Invalid decimal value: '{value_input}'") from e
|
|
||||||
|
|
||||||
super().__init__(processed_value, original_str)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
if self._value == int(self._value):
|
|
||||||
return str(int(self._value))
|
|
||||||
return str(self._value)
|
|
||||||
|
|
||||||
def _sympystr(self, printer):
|
|
||||||
return f"Dec({self._value})"
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def Helper(input_str):
|
|
||||||
if re.match(r"^\s*Dec(\b|\s*[\[\(].*)?", input_str, re.IGNORECASE):
|
|
||||||
return 'Ej: Dec[10.5] o Dec[10]'
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class HybridIP4(HybridCalcType):
|
|
||||||
"""Clase híbrida para direcciones IPv4"""
|
|
||||||
|
|
||||||
def __new__(cls, *args):
|
|
||||||
obj = HybridCalcType.__new__(cls)
|
|
||||||
return obj
|
|
||||||
|
|
||||||
def __init__(self, *args):
|
|
||||||
import socket
|
|
||||||
import struct
|
|
||||||
|
|
||||||
if not args:
|
|
||||||
raise ValueError("IP4() constructor requires at least one argument.")
|
|
||||||
|
|
||||||
ip_str = None
|
|
||||||
prefix = None
|
|
||||||
|
|
||||||
if len(args) == 1:
|
|
||||||
arg = args[0]
|
|
||||||
if not isinstance(arg, str):
|
|
||||||
raise ValueError(f"IP4 single argument must be a string, got '{arg}'")
|
|
||||||
|
|
||||||
if "/" in arg: # CIDR notation
|
|
||||||
parts = arg.split('/', 1)
|
|
||||||
ip_str = parts[0]
|
|
||||||
if len(parts) == 2 and parts[1].isdigit():
|
|
||||||
prefix_val = int(parts[1])
|
|
||||||
if not self._is_valid_prefix(prefix_val):
|
|
||||||
raise ValueError(f"Invalid CIDR prefix '{parts[1]}' in '{arg}'")
|
|
||||||
prefix = prefix_val
|
|
||||||
else:
|
|
||||||
raise ValueError(f"Invalid CIDR format: '{arg}'")
|
|
||||||
else:
|
|
||||||
ip_str = arg
|
|
||||||
|
|
||||||
elif len(args) == 2:
|
|
||||||
ip_arg, mask_arg = args[0], args[1]
|
|
||||||
if not isinstance(ip_arg, str):
|
|
||||||
raise ValueError(f"IP4 first argument must be an IP string")
|
|
||||||
ip_str = ip_arg
|
|
||||||
|
|
||||||
if isinstance(mask_arg, int):
|
|
||||||
if not self._is_valid_prefix(mask_arg):
|
|
||||||
raise ValueError(f"Invalid prefix length: {mask_arg}")
|
|
||||||
prefix = mask_arg
|
|
||||||
elif isinstance(mask_arg, str):
|
|
||||||
if mask_arg.isdigit():
|
|
||||||
prefix_val = int(mask_arg)
|
|
||||||
if self._is_valid_prefix(prefix_val):
|
|
||||||
prefix = prefix_val
|
|
||||||
else:
|
|
||||||
parsed_prefix = self._netmask_str_to_prefix(mask_arg)
|
|
||||||
if parsed_prefix is None:
|
|
||||||
raise ValueError(f"Invalid mask string: '{mask_arg}'")
|
|
||||||
prefix = parsed_prefix
|
|
||||||
else:
|
|
||||||
parsed_prefix = self._netmask_str_to_prefix(mask_arg)
|
|
||||||
if parsed_prefix is None:
|
|
||||||
raise ValueError(f"Invalid netmask string: '{mask_arg}'")
|
|
||||||
prefix = parsed_prefix
|
|
||||||
else:
|
|
||||||
raise ValueError(f"IP4() constructor takes 1 or 2 arguments, got {len(args)}")
|
|
||||||
|
|
||||||
if not self._is_valid_ip_string(ip_str):
|
|
||||||
raise ValueError(f"Invalid IP address string: '{ip_str}'")
|
|
||||||
|
|
||||||
self.ip_address = ip_str
|
|
||||||
self.prefix = prefix
|
|
||||||
|
|
||||||
# Valor para SymPy - usar representación numérica de la IP
|
|
||||||
ip_as_int = struct.unpack("!I", socket.inet_aton(ip_str))[0]
|
|
||||||
canonical_repr = f"{ip_str}/{prefix}" if prefix is not None else ip_str
|
|
||||||
|
|
||||||
super().__init__(ip_as_int, canonical_repr)
|
|
||||||
|
|
||||||
def _is_valid_ip_string(self, ip_str: str) -> bool:
|
|
||||||
"""Verifica si es una dirección IP válida"""
|
|
||||||
import socket
|
|
||||||
try:
|
|
||||||
socket.inet_aton(ip_str)
|
|
||||||
parts = ip_str.split(".")
|
|
||||||
return len(parts) == 4 and all(0 <= int(part) <= 255 for part in parts)
|
|
||||||
except (socket.error, ValueError, TypeError):
|
|
||||||
return False
|
|
||||||
|
|
||||||
def _is_valid_prefix(self, prefix: int) -> bool:
|
|
||||||
"""Verifica si es un prefijo CIDR válido"""
|
|
||||||
return isinstance(prefix, int) and 0 <= prefix <= 32
|
|
||||||
|
|
||||||
def _netmask_str_to_prefix(self, netmask_str: str) -> Optional[int]:
|
|
||||||
"""Convierte máscara decimal a prefijo CIDR"""
|
|
||||||
import socket
|
|
||||||
import struct
|
|
||||||
|
|
||||||
if not self._is_valid_ip_string(netmask_str):
|
|
||||||
return None
|
|
||||||
try:
|
|
||||||
mask_int = struct.unpack("!I", socket.inet_aton(netmask_str))[0]
|
|
||||||
except socket.error:
|
|
||||||
return None
|
|
||||||
|
|
||||||
binary_mask = bin(mask_int)[2:].zfill(32)
|
|
||||||
if '0' in binary_mask and '1' in binary_mask[binary_mask.find('0'):]:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return binary_mask.count('1')
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
if self.prefix is not None:
|
|
||||||
return f"{self.ip_address}/{self.prefix}"
|
|
||||||
return self.ip_address
|
|
||||||
|
|
||||||
def _sympystr(self, printer):
|
|
||||||
return f"IP4({self.__str__()})"
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def Helper(input_str):
|
|
||||||
if re.match(r"^\s*IP4(\b|\s*[\[\(].*)?", input_str, re.IGNORECASE):
|
|
||||||
return 'Ej: IP4[192.168.1.1/24] o IP4[10.0.0.1]'
|
|
||||||
return None
|
|
||||||
|
|
||||||
def NetworkAddress(self):
|
|
||||||
"""Retorna la dirección de red"""
|
|
||||||
import socket
|
|
||||||
import struct
|
|
||||||
|
|
||||||
if self.prefix is None:
|
|
||||||
raise ValueError("Mask not set, cannot calculate network address.")
|
|
||||||
|
|
||||||
ip_int = struct.unpack("!I", socket.inet_aton(self.ip_address))[0]
|
|
||||||
mask_int = (0xFFFFFFFF << (32 - self.prefix)) & 0xFFFFFFFF if self.prefix > 0 else 0
|
|
||||||
net_addr_int = ip_int & mask_int
|
|
||||||
net_addr_str = socket.inet_ntoa(struct.pack("!I", net_addr_int))
|
|
||||||
return HybridIP4(net_addr_str, self.prefix)
|
|
||||||
|
|
||||||
def BroadcastAddress(self):
|
|
||||||
"""Retorna la dirección de broadcast"""
|
|
||||||
import socket
|
|
||||||
import struct
|
|
||||||
|
|
||||||
if self.prefix is None:
|
|
||||||
raise ValueError("Mask not set, cannot calculate broadcast address.")
|
|
||||||
|
|
||||||
net_addr_obj = self.NetworkAddress()
|
|
||||||
net_addr_int = struct.unpack("!I", socket.inet_aton(net_addr_obj.ip_address))[0]
|
|
||||||
wildcard_mask = ((1 << (32 - self.prefix)) - 1) if self.prefix < 32 else 0
|
|
||||||
bcast_addr_int = net_addr_int | wildcard_mask
|
|
||||||
bcast_addr_str = socket.inet_ntoa(struct.pack("!I", bcast_addr_int))
|
|
||||||
return HybridIP4(bcast_addr_str, self.prefix)
|
|
||||||
|
|
||||||
def Nodes(self):
|
|
||||||
"""Retorna el número de hosts disponibles"""
|
|
||||||
if self.prefix is None:
|
|
||||||
raise ValueError("Mask not set, cannot calculate number of nodes.")
|
|
||||||
|
|
||||||
host_bits = 32 - self.prefix
|
|
||||||
if host_bits < 2:
|
|
||||||
return 0
|
|
||||||
return (1 << host_bits) - 2
|
|
||||||
|
|
||||||
|
|
||||||
class HybridChr(HybridCalcType):
|
|
||||||
"""Clase híbrida para caracteres ASCII"""
|
|
||||||
|
|
||||||
def __new__(cls, str_input):
|
|
||||||
obj = HybridCalcType.__new__(cls)
|
|
||||||
return obj
|
|
||||||
|
|
||||||
def __init__(self, str_input: str):
|
|
||||||
if not isinstance(str_input, str):
|
|
||||||
raise TypeError(f"Chr input must be a string, got {type(str_input).__name__}")
|
|
||||||
if not str_input:
|
|
||||||
raise ValueError("Chr input string cannot be empty.")
|
|
||||||
|
|
||||||
if len(str_input) == 1:
|
|
||||||
processed_value = ord(str_input)
|
|
||||||
else:
|
|
||||||
processed_value = [ord(c) for c in str_input]
|
|
||||||
|
|
||||||
super().__init__(processed_value, str_input)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
if isinstance(self._value, int):
|
|
||||||
return chr(self._value)
|
|
||||||
elif isinstance(self._value, list):
|
|
||||||
return "".join(map(chr, self._value))
|
|
||||||
return super().__str__()
|
|
||||||
|
|
||||||
def _sympystr(self, printer):
|
|
||||||
return f"Chr({self._original_str})"
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def Helper(input_str):
|
|
||||||
if re.match(r"^\s*Chr(\b|\s*[\[\(].*)?", input_str, re.IGNORECASE):
|
|
||||||
return 'Ej: Chr[A] o Chr[Hello]. Representa caracteres ASCII.'
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
# Alias para compatibilidad
|
|
||||||
Hex = HybridHex
|
|
||||||
Bin = HybridBin
|
|
||||||
Dec = HybridDec
|
|
||||||
IP4 = HybridIP4
|
|
||||||
Chr = HybridChr
|
|
||||||
|
|
||||||
|
|
||||||
# Funciones de testing
|
|
||||||
def test_hybrid_classes():
|
|
||||||
"""Test de las clases híbridas"""
|
|
||||||
print("=== Test Clases Híbridas ===")
|
|
||||||
|
|
||||||
# Test Hex
|
|
||||||
h = Hex("FF")
|
|
||||||
print(f"Hex('FF'): {h} (type: {type(h)})")
|
|
||||||
print(f" SymPy str: {h._sympystr(None)}")
|
|
||||||
print(f" Is SymPy Basic: {isinstance(h, sympy.Basic)}")
|
|
||||||
print(f" Value: {h._value}")
|
|
||||||
|
|
||||||
# Test operación simple
|
|
||||||
try:
|
|
||||||
result = h + 1
|
|
||||||
print(f" Hex + 1: {result} (type: {type(result)})")
|
|
||||||
except Exception as e:
|
|
||||||
print(f" Error en Hex + 1: {e}")
|
|
||||||
|
|
||||||
# Test IP4
|
|
||||||
ip = IP4("192.168.1.100/24")
|
|
||||||
print(f"IP4('192.168.1.100/24'): {ip}")
|
|
||||||
print(f" NetworkAddress: {ip.NetworkAddress()}")
|
|
||||||
print(f" Is SymPy Basic: {isinstance(ip, sympy.Basic)}")
|
|
||||||
|
|
||||||
# Test Chr
|
|
||||||
c = Chr("A")
|
|
||||||
print(f"Chr('A'): {c}")
|
|
||||||
print(f" ASCII value: {c._value}")
|
|
||||||
print(f" Is SymPy Basic: {isinstance(c, sympy.Basic)}")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
test_hybrid_classes()
|
|
|
@ -1,2 +1,5 @@
|
||||||
Hex[FF]
|
Hex[FF]
|
||||||
ip=IP4[192.168.1.100/24]
|
ip=IP4[192.168.1.100/24]
|
||||||
|
|
||||||
|
|
||||||
|
500/25
|
|
@ -0,0 +1,180 @@
|
||||||
|
"""
|
||||||
|
Clase híbrida para direcciones IPv4
|
||||||
|
"""
|
||||||
|
from hybrid_base import HybridCalcType
|
||||||
|
from typing import Optional, Tuple
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
class HybridIP4(HybridCalcType):
|
||||||
|
"""Clase híbrida para direcciones IPv4"""
|
||||||
|
|
||||||
|
def __new__(cls, *args):
|
||||||
|
"""Crear objeto SymPy válido"""
|
||||||
|
obj = HybridCalcType.__new__(cls)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
if not self._is_valid_ip_string(ip_str):
|
||||||
|
raise ValueError(f"Invalid IP address format: {ip_str}")
|
||||||
|
|
||||||
|
if prefix is not None and not self._is_valid_prefix(prefix):
|
||||||
|
raise ValueError(f"Invalid prefix length: {prefix}")
|
||||||
|
|
||||||
|
# Convertir IP a entero para almacenamiento
|
||||||
|
ip_parts = [int(x) for x in ip_str.split('.')]
|
||||||
|
ip_int = (ip_parts[0] << 24) + (ip_parts[1] << 16) + (ip_parts[2] << 8) + ip_parts[3]
|
||||||
|
|
||||||
|
# Almacenar valores
|
||||||
|
self._ip_int = ip_int
|
||||||
|
self._prefix = prefix
|
||||||
|
self._ip_str = ip_str
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
"""Representación string para display"""
|
||||||
|
if self._prefix is not None:
|
||||||
|
return f"{self._ip_str}/{self._prefix}"
|
||||||
|
return self._ip_str
|
||||||
|
|
||||||
|
def _sympystr(self, printer):
|
||||||
|
"""Representación SymPy string"""
|
||||||
|
return str(self)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def Helper(input_str):
|
||||||
|
"""Ayuda contextual para IP4"""
|
||||||
|
return """
|
||||||
|
Formato IP4:
|
||||||
|
- CIDR: 192.168.1.1/24
|
||||||
|
- Con máscara: 192.168.1.1 255.255.255.0
|
||||||
|
- Solo IP: 192.168.1.1
|
||||||
|
|
||||||
|
Métodos disponibles:
|
||||||
|
- NetworkAddress(): Obtiene dirección de red
|
||||||
|
- BroadcastAddress(): Obtiene dirección de broadcast
|
||||||
|
- Nodes(): Obtiene número de nodos disponibles
|
||||||
|
"""
|
||||||
|
|
||||||
|
def NetworkAddress(self):
|
||||||
|
"""Obtiene la dirección de red"""
|
||||||
|
if self._prefix is None:
|
||||||
|
raise ValueError("No prefix/mask defined")
|
||||||
|
|
||||||
|
# Calcular máscara de red
|
||||||
|
mask = (0xffffffff >> (32 - self._prefix)) << (32 - self._prefix)
|
||||||
|
|
||||||
|
# Aplicar máscara
|
||||||
|
network = self._ip_int & mask
|
||||||
|
|
||||||
|
# Convertir a string
|
||||||
|
parts = [
|
||||||
|
(network >> 24) & 0xff,
|
||||||
|
(network >> 16) & 0xff,
|
||||||
|
(network >> 8) & 0xff,
|
||||||
|
network & 0xff
|
||||||
|
]
|
||||||
|
network_str = '.'.join(str(x) for x in parts)
|
||||||
|
|
||||||
|
return HybridIP4(f"{network_str}/{self._prefix}")
|
||||||
|
|
||||||
|
def BroadcastAddress(self):
|
||||||
|
"""Obtiene la dirección de broadcast"""
|
||||||
|
if self._prefix is None:
|
||||||
|
raise ValueError("No prefix/mask defined")
|
||||||
|
|
||||||
|
# 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_str = '.'.join(str(x) for x in parts)
|
||||||
|
|
||||||
|
return HybridIP4(f"{broadcast_str}/{self._prefix}")
|
||||||
|
|
||||||
|
def Nodes(self):
|
||||||
|
"""Obtiene el número de nodos disponibles"""
|
||||||
|
if self._prefix is None:
|
||||||
|
raise ValueError("No prefix/mask defined")
|
||||||
|
|
||||||
|
# 2^(32-prefix) - 2 (red y broadcast)
|
||||||
|
return 2 ** (32 - self._prefix) - 2
|
|
@ -334,11 +334,16 @@ def check_app_files():
|
||||||
logger.info("Verificando archivos de la aplicación...")
|
logger.info("Verificando archivos de la aplicación...")
|
||||||
|
|
||||||
required_files = [
|
required_files = [
|
||||||
'bracket_parser.py',
|
'tl_bracket_parser.py',
|
||||||
'hybrid_base_types.py',
|
'hybrid_base.py',
|
||||||
'hybrid_evaluation_engine.py',
|
'ip4_type.py',
|
||||||
'interactive_results.py',
|
'hex_type.py',
|
||||||
'hybrid_calc_app.py'
|
'bin_type.py',
|
||||||
|
'dec_type.py',
|
||||||
|
'chr_type.py',
|
||||||
|
'main_evaluation.py',
|
||||||
|
'tl_popup.py',
|
||||||
|
'main_calc_app.py'
|
||||||
]
|
]
|
||||||
|
|
||||||
current_dir = Path(__file__).parent
|
current_dir = Path(__file__).parent
|
||||||
|
@ -386,7 +391,7 @@ def launch_application():
|
||||||
try:
|
try:
|
||||||
# Importar y ejecutar la aplicación
|
# Importar y ejecutar la aplicación
|
||||||
logger.info("Importando módulo principal...")
|
logger.info("Importando módulo principal...")
|
||||||
from hybrid_calc_app import HybridCalculatorApp
|
from main_calc_app import HybridCalculatorApp
|
||||||
|
|
||||||
logger.info("Creando ventana principal...")
|
logger.info("Creando ventana principal...")
|
||||||
root = tk.Tk()
|
root = tk.Tk()
|
|
@ -10,9 +10,13 @@ import threading
|
||||||
from typing import List, Dict, Any, Optional
|
from typing import List, Dict, Any, Optional
|
||||||
|
|
||||||
# Importar componentes del CAS híbrido
|
# Importar componentes del CAS híbrido
|
||||||
from hybrid_evaluation_engine import HybridEvaluationEngine, EvaluationResult
|
from main_evaluation import HybridEvaluationEngine, EvaluationResult
|
||||||
from interactive_results import InteractiveResultManager
|
from tl_popup import InteractiveResultManager
|
||||||
from hybrid_base_types import Hex, Bin, Dec, IP4, Chr
|
from ip4_type import HybridIP4 as IP4
|
||||||
|
from hex_type import HybridHex as Hex
|
||||||
|
from bin_type import HybridBin as Bin
|
||||||
|
from dec_type import HybridDec as Dec
|
||||||
|
from chr_type import HybridChr as Chr
|
||||||
import sympy
|
import sympy
|
||||||
|
|
||||||
|
|
|
@ -8,12 +8,14 @@ import ast
|
||||||
import re
|
import re
|
||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
|
|
||||||
from bracket_parser import BracketParser
|
from tl_bracket_parser import BracketParser
|
||||||
from interactive_results import PlotResult # 🔧 IMPORTACIÓN CORREGIDA
|
from tl_popup import PlotResult
|
||||||
from hybrid_base_types import (
|
from hybrid_base import HybridCalcType
|
||||||
HybridCalcType, HybridHex, HybridBin, HybridDec,
|
from ip4_type import HybridIP4 as IP4
|
||||||
HybridIP4, HybridChr, Hex, Bin, Dec, IP4, Chr
|
from hex_type import HybridHex as Hex
|
||||||
)
|
from bin_type import HybridBin as Bin
|
||||||
|
from dec_type import HybridDec as Dec
|
||||||
|
from chr_type import HybridChr as Chr
|
||||||
|
|
||||||
|
|
||||||
class HybridEvaluationEngine:
|
class HybridEvaluationEngine:
|
||||||
|
@ -472,9 +474,6 @@ class EvaluationResult:
|
||||||
@property
|
@property
|
||||||
def is_interactive(self) -> bool:
|
def is_interactive(self) -> bool:
|
||||||
"""Determina si el resultado requiere interactividad"""
|
"""Determina si el resultado requiere interactividad"""
|
||||||
# 🔧 CORRECCIÓN: Importar PlotResult desde el lugar correcto
|
|
||||||
from interactive_results import PlotResult
|
|
||||||
|
|
||||||
return isinstance(self.result, (PlotResult, sympy.Matrix)) or \
|
return isinstance(self.result, (PlotResult, sympy.Matrix)) or \
|
||||||
(isinstance(self.result, list) and len(self.result) > 3)
|
(isinstance(self.result, list) and len(self.result) > 3)
|
||||||
|
|
Loading…
Reference in New Issue