Skip to content

Commit 3deaed0

Browse files
committed
style: cerrar deuda de estilo ruff en src/ y tests/
Limpia los 80 hallazgos de `ruff check` acumulados en el repo (tipos Optional/Dict/List modernizados a sintaxis X|None/dict/list, isinstance duplicados fusionados, condicionales anidados aplanados, ClassVar en dicts de clase de solo lectura, y manejo explícito de check=False en subprocess.run). Incluye dos mejoras acotadas: mermaid_renderer.py cierra el archivo temporal antes de invocar el subproceso externo (requerido en Windows), y flowchart_agent.py documenta explícitamente por qué su except Exception amplio es intencional. Revisado por python-reviewer y security-reviewer sin hallazgos CRITICAL/HIGH. Suite completa (207 tests) en verde.
1 parent b8d9ba8 commit 3deaed0

11 files changed

Lines changed: 113 additions & 102 deletions

src/multiagent_core/code_auditor_agent.py

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
import subprocess
1414
import sys
1515
from pathlib import Path
16-
from typing import Any, Dict, List, Optional
16+
from typing import Any
1717

1818
SKILL_METADATA = {
1919
"name": "code_auditor_agent",
@@ -31,7 +31,7 @@ class CodeAuditorAgent:
3131
def __init__(self):
3232
pass
3333

34-
def audit_style(self, code: str) -> List[str]:
34+
def audit_style(self, code: str) -> list[str]:
3535
"""Audita el estilo básico del código (PEP 8 simplificado)."""
3636
issues = []
3737
lines = code.split("\n")
@@ -50,7 +50,7 @@ def audit_style(self, code: str) -> List[str]:
5050

5151
return issues
5252

53-
def audit_security(self, code: str) -> List[str]:
53+
def audit_security(self, code: str) -> list[str]:
5454
"""
5555
Escaneo estático de seguridad buscando riesgos asociados a código generado por IA
5656
y OWASP (ej. llaves expuestas, entradas no validadas, funciones peligrosas).
@@ -66,13 +66,16 @@ def audit_security(self, code: str) -> List[str]:
6666

6767
# 1. Buscar uso de funciones peligrosas (eval, exec)
6868
for node in ast.walk(tree):
69-
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
70-
if node.func.id in ["eval", "exec"]:
71-
issues.append(
72-
f"Riesgo de Seguridad: Uso de `{node.func.id}()`. "
73-
"Ejecutar cadenas de texto arbitrarias expone al sistema a inyecciones de código "
74-
"y ejecución remota de comandos no autorizados (Riesgo OWASP LLM-02)."
75-
)
69+
if (
70+
isinstance(node, ast.Call)
71+
and isinstance(node.func, ast.Name)
72+
and node.func.id in ["eval", "exec"]
73+
):
74+
issues.append(
75+
f"Riesgo de Seguridad: Uso de `{node.func.id}()`. "
76+
"Ejecutar cadenas de texto arbitrarias expone al sistema a inyecciones de código "
77+
"y ejecución remota de comandos no autorizados (Riesgo OWASP LLM-02)."
78+
)
7679

7780
# 2. Buscar API Keys expuestas mediante heurística de variables
7881
api_key_pattern = re.compile(
@@ -89,7 +92,7 @@ def audit_security(self, code: str) -> List[str]:
8992

9093
return issues
9194

92-
def run_pytest(self, test_file_path: Path) -> Dict[str, Any]:
95+
def run_pytest(self, test_file_path: Path) -> dict[str, Any]:
9396
"""Ejecuta pytest sobre el archivo de pruebas especificado y retorna el resultado."""
9497
try:
9598
result = subprocess.run(
@@ -117,7 +120,7 @@ def run_pytest(self, test_file_path: Path) -> Dict[str, Any]:
117120
}
118121

119122
def generate_report(
120-
self, student_code: str, test_file_path: Optional[Path] = None
123+
self, student_code: str, test_file_path: Path | None = None
121124
) -> str:
122125
"""Genera un reporte pedagógico completo en Markdown."""
123126
style_issues = self.audit_style(student_code)

src/multiagent_core/content_auditor_agent.py

Lines changed: 47 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@
1111

1212
import ast
1313
import re
14-
from functools import lru_cache
14+
from functools import cache
1515
from pathlib import Path
16-
from typing import Any, Dict, List, Optional
16+
from typing import Any
1717

1818
from .code_auditor_agent import CodeAuditorAgent
1919
from .mermaid_renderer import MermaidRenderer
@@ -38,8 +38,8 @@
3838
_ARGUMENTOS_SIN_TIPO_ESPERADO = {"self", "cls"}
3939

4040

41-
@lru_cache(maxsize=None)
42-
def _parse_programa_oficial(programa_path: Path) -> Dict[int, dict]:
41+
@cache
42+
def _parse_programa_oficial(programa_path: Path) -> dict[int, dict]:
4343
"""Parsea el programa de asignatura oficial en subtemas y semanas por unidad.
4444
4545
El resultado se cachea por ruta (`lru_cache`) porque `audit_all_units`
@@ -59,7 +59,7 @@ def _parse_programa_oficial(programa_path: Path) -> Dict[int, dict]:
5959
content = programa_path.read_text(encoding="utf-8")
6060
lines = content.split("\n")
6161

62-
mapeo: Dict[int, dict] = {}
62+
mapeo: dict[int, dict] = {}
6363
unidad_actual = None
6464

6565
for line in lines:
@@ -95,8 +95,8 @@ class ContentAuditorAgent:
9595

9696
def __init__(
9797
self,
98-
programa_path: Optional[Path] = None,
99-
mermaid_renderer: Optional[MermaidRenderer] = None,
98+
programa_path: Path | None = None,
99+
mermaid_renderer: MermaidRenderer | None = None,
100100
) -> None:
101101
self.code_auditor = CodeAuditorAgent()
102102
self.programa_path = (
@@ -106,7 +106,7 @@ def __init__(
106106
output_dir=Path.cwd() / "notebooks" / "assets" / "diagramas"
107107
)
108108

109-
def _audit_latex(self, content: str) -> List[str]:
109+
def _audit_latex(self, content: str) -> list[str]:
110110
"""Detecta delimitadores LaTeX desbalanceados y comandos mal formados.
111111
112112
Args:
@@ -115,7 +115,7 @@ def _audit_latex(self, content: str) -> List[str]:
115115
Returns:
116116
Lista de descripciones de hallazgos; vacía si no hay problemas.
117117
"""
118-
hallazgos: List[str] = []
118+
hallazgos: list[str] = []
119119

120120
sin_fences = re.sub(r"```.*?```", "", content, flags=re.DOTALL)
121121
tokens = re.findall(r"\$\$|\$", sin_fences)
@@ -142,7 +142,7 @@ def _audit_latex(self, content: str) -> List[str]:
142142

143143
return hallazgos
144144

145-
def _audit_codigo(self, python_blocks: List[str]) -> List[str]:
145+
def _audit_codigo(self, python_blocks: list[str]) -> list[str]:
146146
"""Audita bloques de código Python de ejemplo (docstrings, type hints, estilo).
147147
148148
Ignora la longitud de línea (PEP8 79 cols) porque el código de
@@ -160,7 +160,7 @@ def _audit_codigo(self, python_blocks: List[str]) -> List[str]:
160160
Returns:
161161
Lista de descripciones de hallazgos; vacía si no hay problemas.
162162
"""
163-
hallazgos: List[str] = []
163+
hallazgos: list[str] = []
164164

165165
for code in python_blocks:
166166
if _CELDA_MAGICA_IPYTHON.search(code):
@@ -200,7 +200,7 @@ def _audit_codigo(self, python_blocks: List[str]) -> List[str]:
200200

201201
return hallazgos
202202

203-
def _audit_pedagogico(self, bloques: List[tuple], content: str) -> List[str]:
203+
def _audit_pedagogico(self, bloques: list[tuple], content: str) -> list[str]:
204204
"""Verifica coherencia pedagógica: Hilo de Oro, analogías, y sintaxis Mermaid.
205205
206206
Args:
@@ -210,7 +210,7 @@ def _audit_pedagogico(self, bloques: List[tuple], content: str) -> List[str]:
210210
Returns:
211211
Lista de descripciones de hallazgos; vacía si no hay problemas.
212212
"""
213-
hallazgos: List[str] = []
213+
hallazgos: list[str] = []
214214

215215
idiomas_presentes = {lang for _, lang, _ in bloques}
216216
hilo_de_oro_esperado = {"pseudocodigo", "mermaid", "python", "pytest"}
@@ -244,7 +244,7 @@ def _audit_pedagogico(self, bloques: List[tuple], content: str) -> List[str]:
244244

245245
return hallazgos
246246

247-
def _audit_curricular(self, md_path: Path, content: str) -> List[str]:
247+
def _audit_curricular(self, md_path: Path, content: str) -> list[str]:
248248
"""Verifica cobertura temática y duración contra el programa oficial.
249249
250250
Args:
@@ -264,7 +264,7 @@ def _audit_curricular(self, md_path: Path, content: str) -> List[str]:
264264
if numero_unidad not in mapeo:
265265
return []
266266

267-
hallazgos: List[str] = []
267+
hallazgos: list[str] = []
268268
info = mapeo[numero_unidad]
269269
content_lower = content.lower()
270270

@@ -297,8 +297,8 @@ def _audit_curricular(self, md_path: Path, content: str) -> List[str]:
297297
_UNIT_NUMBER_PATTERN = re.compile(r"unit_number\s*=\s*(\d+)")
298298

299299
def _verifica_unit_number(
300-
self, python_blocks: List[str], md_path: Path
301-
) -> List[str]:
300+
self, python_blocks: list[str], md_path: Path
301+
) -> list[str]:
302302
"""Verifica que unit_number en la celda de auto-evaluación coincida
303303
con el número de unidad indicado por el nombre del archivo.
304304
@@ -316,7 +316,7 @@ def _verifica_unit_number(
316316
return []
317317
numero_esperado = int(unidad_match.group(1))
318318

319-
hallazgos: List[str] = []
319+
hallazgos: list[str] = []
320320
for code in python_blocks:
321321
match = self._UNIT_NUMBER_PATTERN.search(code)
322322
if match and int(match.group(1)) != numero_esperado:
@@ -328,7 +328,7 @@ def _verifica_unit_number(
328328

329329
_WRITEFILE_LINEA = re.compile(r"^\s*%%writefile\s+(\S+)", re.MULTILINE)
330330

331-
def _extrae_writefiles_reales(self, python_blocks: List[str]) -> List[tuple]:
331+
def _extrae_writefiles_reales(self, python_blocks: list[str]) -> list[tuple]:
332332
"""Extrae líneas reales de %%writefile (primera línea no en blanco
333333
de una celda), ignorando menciones en prosa o dentro de f-strings.
334334
@@ -345,7 +345,7 @@ def _extrae_writefiles_reales(self, python_blocks: List[str]) -> List[tuple]:
345345
return resultado
346346

347347
def _tiene_definicion_previa(
348-
self, python_blocks: List[str], indice_bloque: int
348+
self, python_blocks: list[str], indice_bloque: int
349349
) -> bool:
350350
"""Verifica si algún bloque anterior a indice_bloque define una
351351
función o clase (ast.parse + ast.walk), tolerando bloques con
@@ -372,8 +372,8 @@ def _tiene_definicion_previa(
372372
return False
373373

374374
def _verifica_writefiles_con_definicion(
375-
self, python_blocks: List[str]
376-
) -> List[str]:
375+
self, python_blocks: list[str]
376+
) -> list[str]:
377377
"""Verifica que todo %%writefile real tenga una definición de
378378
función/clase en algún bloque anterior del mismo MD.
379379
@@ -384,7 +384,7 @@ def _verifica_writefiles_con_definicion(
384384
Lista con un hallazgo por cada %%writefile sin definición
385385
previa; vacía si todos tienen definición o no hay ninguno.
386386
"""
387-
hallazgos: List[str] = []
387+
hallazgos: list[str] = []
388388
for nombre_archivo, indice_bloque in self._extrae_writefiles_reales(
389389
python_blocks
390390
):
@@ -396,7 +396,7 @@ def _verifica_writefiles_con_definicion(
396396
)
397397
return hallazgos
398398

399-
def _verifica_fences_balanceados(self, content: str) -> List[str]:
399+
def _verifica_fences_balanceados(self, content: str) -> list[str]:
400400
"""Detecta fences de apertura que extract_fenced_blocks() no pudo
401401
cerrar correctamente — señal indirecta: el último bloque extraído
402402
consume texto hasta el final del documento, aunque el .md real
@@ -435,14 +435,16 @@ def _verifica_fences_balanceados(self, content: str) -> List[str]:
435435
)
436436
if not cierre_encontrado:
437437
return [
438-
f"Posible fence de código sin cerrar: un bloque abierto con "
439-
f"'{ultimo_fence}' no encuentra su línea de cierre exacta en "
440-
"el resto del documento — extract_fenced_blocks() pudo haber "
441-
"consumido contenido no destinado a ser código."
438+
(
439+
f"Posible fence de código sin cerrar: un bloque abierto con "
440+
f"'{ultimo_fence}' no encuentra su línea de cierre exacta en "
441+
"el resto del documento — extract_fenced_blocks() pudo haber "
442+
"consumido contenido no destinado a ser código."
443+
)
442444
]
443445
return []
444446

445-
def _verifica_celda_setup(self, content: str) -> List[str]:
447+
def _verifica_celda_setup(self, content: str) -> list[str]:
446448
"""Verifica que la celda estándar de setup (git clone + os.chdir)
447449
esté presente en el MD.
448450
@@ -454,8 +456,10 @@ def _verifica_celda_setup(self, content: str) -> List[str]:
454456
"""
455457
if "git clone" not in content or "os.chdir" not in content:
456458
return [
457-
"No se encontró la celda de setup estándar (git clone + "
458-
"os.chdir) en esta unidad."
459+
(
460+
"No se encontró la celda de setup estándar (git clone + "
461+
"os.chdir) en esta unidad."
462+
)
459463
]
460464
return []
461465

@@ -464,7 +468,7 @@ def _verifica_celda_setup(self, content: str) -> List[str]:
464468
r"^- La secuencia de la unidad anterior"
465469
)
466470

467-
def _verifica_seccion_prerequisitos(self, content: str) -> List[str]:
471+
def _verifica_seccion_prerequisitos(self, content: str) -> list[str]:
468472
"""Verifica que la sección de prerequisitos, si existe, tenga al
469473
menos una línea con el formato parseable esperado por
470474
CurriculumMapAgent.render_dag() (relación estructurada o la línea
@@ -499,16 +503,18 @@ def _verifica_seccion_prerequisitos(self, content: str) -> List[str]:
499503
return []
500504

501505
return [
502-
"La sección '## 📚 Prerequisitos de esta unidad' existe pero "
503-
"ninguna de sus líneas sigue el formato esperado "
504-
"('- **término** (Unidad N)' o la línea alternativa de "
505-
"'sin conceptos adicionales') — CurriculumMapAgent.render_dag() "
506-
"no podrá extraer ninguna relación de esta unidad."
506+
(
507+
"La sección '## 📚 Prerequisitos de esta unidad' existe pero "
508+
"ninguna de sus líneas sigue el formato esperado "
509+
"('- **término** (Unidad N)' o la línea alternativa de "
510+
"'sin conceptos adicionales') — CurriculumMapAgent.render_dag() "
511+
"no podrá extraer ninguna relación de esta unidad."
512+
)
507513
]
508514

509515
def _audit_invariantes_estructurales(
510-
self, python_blocks: List[str], content: str, md_path: Path
511-
) -> List[str]:
516+
self, python_blocks: list[str], content: str, md_path: Path
517+
) -> list[str]:
512518
"""Audita invariantes estructurales: consistencia de la celda de
513519
auto-evaluación (donde ya existe), consistencia de la sección de
514520
prerequisitos (donde ya existe), e invariantes generales del
@@ -522,15 +528,15 @@ def _audit_invariantes_estructurales(
522528
Returns:
523529
Lista concatenada de hallazgos de las 5 verificaciones.
524530
"""
525-
hallazgos: List[str] = []
531+
hallazgos: list[str] = []
526532
hallazgos.extend(self._verifica_unit_number(python_blocks, md_path))
527533
hallazgos.extend(self._verifica_writefiles_con_definicion(python_blocks))
528534
hallazgos.extend(self._verifica_fences_balanceados(content))
529535
hallazgos.extend(self._verifica_celda_setup(content))
530536
hallazgos.extend(self._verifica_seccion_prerequisitos(content))
531537
return hallazgos
532538

533-
def audit_unit(self, md_path: Path) -> Dict[str, Any]:
539+
def audit_unit(self, md_path: Path) -> dict[str, Any]:
534540
"""Audita una unidad del curso contra las 5 dimensiones de calidad.
535541
536542
Args:

src/multiagent_core/curriculum_map_agent.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
import re
1212
from pathlib import Path
13-
from typing import Dict, List
13+
from typing import ClassVar
1414

1515
from .notebook_compiler_agent import extract_fenced_blocks
1616

@@ -66,7 +66,7 @@
6666
class CurriculumMapAgent:
6767
"""Agente que sugiere y renderiza el mapa de dependencias entre unidades."""
6868

69-
_NOMBRES_UNIDAD = {
69+
_NOMBRES_UNIDAD: ClassVar[dict[int, str]] = {
7070
0: "Entorno de Trabajo",
7171
1: "Pensamiento Computacional",
7272
2: "Metodología y Pruebas",
@@ -83,7 +83,7 @@ def _numero_de_unidad(self, md_path: Path) -> int:
8383
match = _UNIT_NUMBER_FROM_FILENAME.match(md_path.name)
8484
return int(match.group(1)) if match else -1
8585

86-
def _leer_unidades_ordenadas(self, course_dir: Path) -> List[tuple]:
86+
def _leer_unidades_ordenadas(self, course_dir: Path) -> list[tuple]:
8787
"""Lee todas las UNIDAD_*.md y las retorna ordenadas por número.
8888
8989
Returns:
@@ -107,7 +107,7 @@ def _buscar_evidencia_prosa(self, termino: str, content: str) -> str:
107107
return linea.strip()
108108
return ""
109109

110-
def suggest_prerequisites(self, course_dir: Path) -> Dict[int, List[dict]]:
110+
def suggest_prerequisites(self, course_dir: Path) -> dict[int, list[dict]]:
111111
"""Sugiere candidatos de relación de concepto entre unidades.
112112
113113
Para cada unidad, busca en unidades ANTERIORES apariciones de los
@@ -123,10 +123,10 @@ def suggest_prerequisites(self, course_dir: Path) -> Dict[int, List[dict]]:
123123
es {"unidad_origen": int, "termino": str, "evidencia": str}.
124124
"""
125125
unidades = self._leer_unidades_ordenadas(course_dir)
126-
resultado: Dict[int, List[dict]] = {}
126+
resultado: dict[int, list[dict]] = {}
127127

128128
for i, (numero_actual, content_actual, blocks_actual) in enumerate(unidades):
129-
candidatos: List[dict] = []
129+
candidatos: list[dict] = []
130130
unidades_anteriores = unidades[:i]
131131

132132
for termino in TERMINOS_TECNICOS_CURADOS:
@@ -162,7 +162,7 @@ def suggest_prerequisites(self, course_dir: Path) -> Dict[int, List[dict]]:
162162

163163
return resultado
164164

165-
def _extrae_relaciones_de_seccion(self, content: str) -> List[dict]:
165+
def _extrae_relaciones_de_seccion(self, content: str) -> list[dict]:
166166
"""Extrae relaciones (término, unidad_origen) de la sección de
167167
prerequisitos ya escrita, ignorando líneas que no matcheen el
168168
formato exacto.

0 commit comments

Comments
 (0)