Skip to content
Open
134 changes: 134 additions & 0 deletions src/extension.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
# extension.py

# Este módulo permite al usuario registrar funciones personalizadas
# y luego usarlas desde otras partes del programa.
import types
import json
import inspect

class FunctionRegistry:
"""
Clase para registrar y evaluar funciones personalizadas de forma segura.

Asegura que las funciones registradas:
- No utilicen palabras clave peligrosas (como 'os', 'eval', etc.).
- No usen nombres reservados como 'SUM', 'IF', etc.
"""
def __init__(self):
"""Inicializa el registro con funciones permitidas y palabras clave peligrosas."""
self._functions = {}
self._reserved = {"SUM", "AVG", "IF", "MAX", "MIN"}
self._dangerous_keywords = [
"os", "open", "eval", "exec", "__import__"
]

def register(self, name: str, func):
"""
Registra una función en el sistema si es segura, no usa el nombre de una función reservada y es válida.

Parámetros:
name (str): Nombre de la función.
func (function): Objeto de función a registrar.

Retorna:
None
"""
if name.upper() in self._reserved:
print(f"Error: El nombre '{name}' está reservado y no se puede usar.")
return
if not isinstance(func, types.FunctionType):
print(f"Error: lo que intentas registrar no es una función.")
return

if not self.isSafe(func):
print(f"Error: lo que intentas registrar no esta permitido.")
return

print(f"Registrando función {name}")
source = inspect.getsource(func)
self._functions[name] = {"func": func, "source": source}
print(f"Función '{name}' registrada con éxito.")

def isSafe(self,func):
"""
Verifica si el código fuente de una función contiene palabras clave peligrosas.

Parámetros:
func (function): Función a inspeccionar.

Retorna:
bool: True si es segura, False si contiene código peligroso.
"""
try:
source = inspect.getsource(func)
for word in self._dangerous_keywords:
if word in source:
return False
return True
except Exception:
return False

def evaluate(self, name, *args):
"""
Evalúa una función registrada con los argumentos dados.

Parámetros:
name (str): Nombre de la función.
*args: Argumentos para la función.

Retorna:
Resultado de la función, o None si hay error o no está registrada.
"""
if name not in self._functions:
print(f"Error: La función '{name}' no ha sido registrada.")
return
try:
result = self._functions[name]["func"](*args)
return result
except Exception as e:
print(f"Error: fallo al ejecutar la función '{name}':", e)
return

def get_registered_functions(self):
"""
Retorna una lista de funciones registradas, incluyendo nombre y código fuente.

Retorna:
list[dict]: Lista de funciones con claves 'name' y 'source'.
"""
return [
{"name": name, "source": data["source"]}
for name, data in self._functions.items()
]

class FunctionSave:
"""
Clase auxiliar para guardar y cargar funciones registradas desde archivos JSON.
"""
def save_functions(self, path="functions.json"):
"""
Guarda las funciones registradas en un archivo JSON.

Parámetros:
registry (FunctionRegistry): Registro de funciones.
path (str): Ruta del archivo JSON.
"""
with open(path, "w", encoding="utf-8") as f:
json.dump(self.get_registered_functions(), f, indent=4, ensure_ascii=False)

def load_functions(path="functions.json"):
"""
Carga funciones desde un archivo JSON.

Parámetros:
path (str): Ruta del archivo JSON.

Retorna:
list[dict]: Lista de funciones con claves 'name' y 'source'.
"""
try:
with open(path, "r") as f:
data = json.load(f)
return data
except FileNotFoundError:
return []
34 changes: 34 additions & 0 deletions src/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Function Extension Module
Este módulo permite **registrar, evaluar, guardar y cargar** de forma segura funciones personalizadas provistas por el usuario.

## Caracteristicas
- Registro de funciones Python puras.
- Validación contra palabras clave peligrosas (`os`, `eval`, `exec`, etc.).
- Prevención de nombres reservados (`SUM`, `IF`, `AVG`, etc.).
- Evaluación de funciones con argumentos dinámicos.
- Serialización y deserialización de funciones en formato JSON.

## Estructura
- `FunctionRegistry`: Clase principal para registrar y evaluar funciones.
- `FunctionSave`: Clase auxiliar para guardar y cargar funciones desde archivos JSON.

# Uso

## Crear Registro
registry = FunctionRegistry()

## Registrar una función
def suma(a, b):
return a + b

registry.register("suma", suma)

## Evaluar la función
resultado = registry.evaluate("suma", 2, 3)
print(resultado) # 5

## Guardar en JSON
FunctionSave.save_functions(registry, "mis_funciones.json")

## Cargar del JSON
**No implementado**
73 changes: 73 additions & 0 deletions test/test_extension.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import sys
import os
import unittest

sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'src')))

from extension import FunctionRegistry, FunctionSave


# --- Funciones de prueba ---
def suma(a, b):
return a + b

def peligrosa():
import os
return os.system("ls")

def error_func(a, b):
return a / 0 # Genera error al evaluar


class TestFunctionRegistry(unittest.TestCase):

def setUp(self):
self.registry = FunctionRegistry()

def registered_names(self):
return [f["name"] for f in self.registry.get_registered_functions()]

def test_register_success(self):
self.registry.register("mi_suma", suma)
self.assertIn("mi_suma", self.registered_names())

def test_register_reserved_name(self):
self.registry.register("SUM", suma)
self.assertNotIn("SUM", self.registered_names())

def test_register_dangerous_function(self):
self.registry.register("insegura", peligrosa)
self.assertNotIn("insegura", self.registered_names())

def test_register_non_function(self):
self.registry.register("no_funcion", 123)
self.assertNotIn("no_funcion", self.registered_names())

def test_evaluate_success(self):
self.registry.register("mi_suma", suma)
resultado = self.registry.evaluate("mi_suma", 3, 4)
self.assertEqual(resultado, 7)

def test_evaluate_unregistered_function(self):
resultado = self.registry.evaluate("desconocida", 1, 2)
self.assertIsNone(resultado)

def test_evaluate_function_with_error(self):
self.registry.register("error_func", error_func)
resultado = self.registry.evaluate("error_func", 1, 2)
self.assertIsNone(resultado)

def test_save_and_load_functions(self):
self.registry.register("mi_suma", suma)
FunctionSave.save_functions(self.registry, path="test_functions.json")
loaded = FunctionSave.load_functions(path="test_functions.json")
self.assertIn("mi_suma", [f["name"] for f in loaded])

@classmethod
def tearDownClass(cls):
if os.path.exists("test_functions.json"):
os.remove("test_functions.json")


if __name__ == "__main__":
unittest.main()