From 19aa8841beca5bc05f11380581a24e4383e619ae Mon Sep 17 00:00:00 2001 From: Sebastian Martinez Date: Wed, 4 Jun 2025 20:30:13 -0500 Subject: [PATCH 01/11] Feat: Registro de funciones personalizadas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Asociar un nombre a una función de Python provista por el usuario - Asegurar que los nombres de funciones básicas o reservadas no puedan ser sobrescritos --- src/extension.py | 55 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/src/extension.py b/src/extension.py index e69de29..e69d445 100644 --- a/src/extension.py +++ b/src/extension.py @@ -0,0 +1,55 @@ +# extension.py + +# Este módulo permite al usuario registrar funciones personalizadas +# y luego usarlas desde otras partes del programa. +import types +import inspect + +class FunctionRegistry: + """ + Clase para registrar y evaluar funciones personalizadas de forma segura. + + Asegura que las funciones registradas: + - No usen nombres reservados como 'SUM', 'IF', etc. + """ + def __init__(self): + """Inicializa el registro con funciones permitidas""" + self._functions = {} + self._reserved = {"SUM", "AVG", "IF", "MAX", "MIN"} + + 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. + + 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 + + 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 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() + ] + \ No newline at end of file From 0735a23a3334c51ce3b47dd395e093978c1a3902 Mon Sep 17 00:00:00 2001 From: Sebastian Martinez Date: Wed, 4 Jun 2025 20:31:45 -0500 Subject: [PATCH 02/11] docs: agregar un readme para los modulos MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Se agrega un readme con la descripción del modulo, describiendo el uso de la funcionalidad registro de funciones personalizadas. --- src/readme.md | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/readme.md diff --git a/src/readme.md b/src/readme.md new file mode 100644 index 0000000..1468c85 --- /dev/null +++ b/src/readme.md @@ -0,0 +1,32 @@ +# 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. + +## Uso + +# Crear Registro +registry = FunctionRegistry() + +# Registrar una función +def suma(a, b): + return a + b + +registry.register("suma", suma) + +# Evaluar la función +**No implementado** + +# Guardar en JSON +**No implementado** + +# Cargar del JSON +**No implementado** \ No newline at end of file From 1c4062308335b29358307b020409b291ecdff291 Mon Sep 17 00:00:00 2001 From: Sebastian Martinez Date: Wed, 4 Jun 2025 20:32:22 -0500 Subject: [PATCH 03/11] test: agrega test para clase FunctionRegistry --- test/test_extension.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 test/test_extension.py diff --git a/test/test_extension.py b/test/test_extension.py new file mode 100644 index 0000000..059f0b8 --- /dev/null +++ b/test/test_extension.py @@ -0,0 +1,31 @@ +import sys +import os +import unittest + +sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'src'))) + +from extension import FunctionRegistry + + +# --- Funciones de prueba --- +def suma(a, b): + return a + b + +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()) + +if __name__ == "__main__": + unittest.main() \ No newline at end of file From beba57f934c005cedce81a39592e884ccfc41ecd Mon Sep 17 00:00:00 2001 From: Sebastian Martinez Date: Wed, 4 Jun 2025 20:38:45 -0500 Subject: [PATCH 04/11] =?UTF-8?q?Feat:=20Evaluaci=C3=B3n=20segura?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/extension.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/extension.py b/src/extension.py index e69d445..0022083 100644 --- a/src/extension.py +++ b/src/extension.py @@ -40,6 +40,26 @@ def register(self, name: str, func): self._functions[name] = {"func": func, "source": source} print(f"Función '{name}' registrada con éxito.") + 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): """ From 860c3e1c9aac03010e1580398befaf70972ec552 Mon Sep 17 00:00:00 2001 From: Sebastian Martinez Date: Wed, 4 Jun 2025 20:39:47 -0500 Subject: [PATCH 05/11] docs: actualizar README con la nueva feature --- src/readme.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/readme.md b/src/readme.md index 1468c85..54443f2 100644 --- a/src/readme.md +++ b/src/readme.md @@ -23,7 +23,8 @@ def suma(a, b): registry.register("suma", suma) # Evaluar la función -**No implementado** +resultado = registry.evaluate("suma", 2, 3) +print(resultado) # 5 # Guardar en JSON **No implementado** From 19eb84ee8fff595a0d4b9665efad4d13514ed14b Mon Sep 17 00:00:00 2001 From: Sebastian Martinez Date: Wed, 4 Jun 2025 20:40:09 -0500 Subject: [PATCH 06/11] =?UTF-8?q?test:=20agregar=20test=20para=20la=20func?= =?UTF-8?q?ionalidad=20evaluaci=C3=B3n=20segura?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/test_extension.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/test_extension.py b/test/test_extension.py index 059f0b8..ab08c23 100644 --- a/test/test_extension.py +++ b/test/test_extension.py @@ -11,6 +11,10 @@ def suma(a, b): return a + b +def error_func(a, b): + return a / 0 # Genera error al evaluar + + class TestFunctionRegistry(unittest.TestCase): def setUp(self): @@ -27,5 +31,23 @@ def test_register_reserved_name(self): self.registry.register("SUM", suma) self.assertNotIn("SUM", 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) + if __name__ == "__main__": unittest.main() \ No newline at end of file From 3ee2da38be2c98d4a5c2841fb4741f33bf87ef99 Mon Sep 17 00:00:00 2001 From: Sebastian Martinez Date: Wed, 4 Jun 2025 20:43:09 -0500 Subject: [PATCH 07/11] =?UTF-8?q?feat:=20Gesti=C3=B3n=20de=20errores=20y?= =?UTF-8?q?=20seguridad?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/extension.py | 31 +++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/src/extension.py b/src/extension.py index 0022083..3313151 100644 --- a/src/extension.py +++ b/src/extension.py @@ -10,16 +10,20 @@ 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""" + """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. + 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. @@ -34,12 +38,35 @@ def register(self, name: str, func): 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. From caad36f88553fdab90953bf9fdcaffdaa8ada9eb Mon Sep 17 00:00:00 2001 From: Sebastian Martinez Date: Wed, 4 Jun 2025 20:43:50 -0500 Subject: [PATCH 08/11] =?UTF-8?q?test:=20agregar=20test=20para=20feature?= =?UTF-8?q?=20gesti=C3=B3n=20de=20errores=20y=20seguridad?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/test_extension.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/test_extension.py b/test/test_extension.py index ab08c23..7a85ade 100644 --- a/test/test_extension.py +++ b/test/test_extension.py @@ -11,6 +11,10 @@ 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 @@ -31,6 +35,10 @@ 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()) From 67d039b175077651ae8e3f1de38c965bd4c57007 Mon Sep 17 00:00:00 2001 From: Sebastian Martinez Date: Wed, 4 Jun 2025 20:47:56 -0500 Subject: [PATCH 09/11] feat: agregar persistencia (solo guardar) --- src/extension.py | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/src/extension.py b/src/extension.py index 3313151..a06a84e 100644 --- a/src/extension.py +++ b/src/extension.py @@ -3,6 +3,7 @@ # 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: @@ -99,4 +100,35 @@ def get_registered_functions(self): {"name": name, "source": data["source"]} for name, data in self._functions.items() ] - \ No newline at end of file + +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 [] \ No newline at end of file From 38013f0328b789627bb0c3aa7ab163ff90380272 Mon Sep 17 00:00:00 2001 From: Sebastian Martinez Date: Wed, 4 Jun 2025 20:51:25 -0500 Subject: [PATCH 10/11] =?UTF-8?q?docs:=20actualizaci=C3=B3n=20del=20readme?= =?UTF-8?q?=20con=20la=20nueva=20feature=20persistencia?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/readme.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/readme.md b/src/readme.md index 54443f2..a357d32 100644 --- a/src/readme.md +++ b/src/readme.md @@ -10,24 +10,25 @@ Este módulo permite **registrar, evaluar, guardar y cargar** de forma segura fu ## Estructura - `FunctionRegistry`: Clase principal para registrar y evaluar funciones. +- `FunctionSave`: Clase auxiliar para guardar y cargar funciones desde archivos JSON. -## Uso +# Uso -# Crear Registro +## Crear Registro registry = FunctionRegistry() -# Registrar una función +## Registrar una función def suma(a, b): return a + b registry.register("suma", suma) -# Evaluar la función +## Evaluar la función resultado = registry.evaluate("suma", 2, 3) print(resultado) # 5 -# Guardar en JSON -**No implementado** +## Guardar en JSON +FunctionSave.save_functions(registry, "mis_funciones.json") -# Cargar del JSON +## Cargar del JSON **No implementado** \ No newline at end of file From c1d647fa2db72782d6e7c4bc3dca1e8d88e569a8 Mon Sep 17 00:00:00 2001 From: Sebastian Martinez Date: Wed, 4 Jun 2025 20:51:42 -0500 Subject: [PATCH 11/11] test: agregar los test para la clase FunctionSave --- test/test_extension.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/test/test_extension.py b/test/test_extension.py index 7a85ade..aa525da 100644 --- a/test/test_extension.py +++ b/test/test_extension.py @@ -4,7 +4,7 @@ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'src'))) -from extension import FunctionRegistry +from extension import FunctionRegistry, FunctionSave # --- Funciones de prueba --- @@ -57,5 +57,17 @@ def test_evaluate_function_with_error(self): 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() \ No newline at end of file