Diseño de Software
Arquitectura
DependaMan está estructurado como un pipeline de seis fases. Cada fase tiene una única responsabilidad y un contrato de entrada/salida bien definido. Las fases son independientes — los análisis de la Fase 4 no saben nada de git; el renderizador de la Fase 6 no sabe nada del sistema de archivos.
Fase 1 — Descubrimiento de archivos
Recorre el directorio del proyecto, recolecta todos los archivos .py y
determina la raíz del paquete. Distingue módulos internos de externos — los
imports de stdlib y de terceros se ignoran en todo el pipeline.
Fase 2 — Parseo de imports
Usa ast para parsear cada archivo y extraer sentencias import y
from ... import. Resuelve imports relativos. Filtra solo los imports internos.
Sin regex, sin matching de strings — el AST da la estructura exacta del código
fuente.
Fase 3 — Construcción del grafo
Construye un grafo dirigido como estructura de adyacencia:
- Nodo = módulo interno
- Arista A → B = “el módulo A importa al módulo B”
Adjunta metadatos a cada nodo: ruta del archivo, conteo de líneas, conteo de funciones/clases.
Fase 4 — Análisis
Cuatro pasadas independientes sobre el grafo de módulos:
- Código muerto — nodos sin aristas entrantes que no son puntos de entrada
- Imports circulares — detección de ciclos basada en DFS; reporta los caminos completos del ciclo
- Hotspots — nodos rankeados por fan-in (más importados)
- Acoplamiento — nodos rankeados por fan-out (importan más)
Fase 5 — Integración con Git
Usa subprocess + git log para calcular por archivo: frecuencia de commits,
líneas añadidas/eliminadas a lo largo del tiempo (churn) y último autor. Adjunta
estos datos a los nodos del grafo. Opcional — se omite elegantemente si el
proyecto no es un repo git.
Fase 6 — Salida HTML
Genera un archivo .html autocontenido. La plantilla HTML es una string
estática embebida en Python. Solo los datos cambian entre ejecuciones — Python
serializa el grafo a JSON y lo inyecta en la plantilla:
data = json.dumps({"nodes": [...], "edges": [...]})
html = TEMPLATE.replace("__GRAPH_DATA__", data)
La plantilla contiene un bloque <script> que lee los datos inyectados y
renderiza el grafo usando la API canvas del navegador. Sin librerías JS
externas. Funciona completamente sin conexión.
Grafo de Callables (v1.1)
Una segunda capa de grafo construida sobre el grafo de módulos. Donde el grafo de módulos responde qué depende de qué, el grafo de callables responde quién llama a qué.
Dos grafos, un espacio de nombres canónico
| Grafo | Nodos | Aristas |
|---|---|---|
| Módulo | pkg.mod | A importa B |
| Callable | pkg.mod::Func, pkg.mod::Class.method | A llama a B |
Todo canónico de callable contiene el canónico de su módulo como prefijo — se
separa en :: para obtener el módulo padre.
Detección de callables muertos
Un callable está muerto cuando nada lo referencia:
dead = {
c for c in project_definitions
if callable_fanin[c] == 0 and c not in project_imports
}
Concurrencia
Estadísticas de Git (I/O-bound) — esperar llamadas a subprocesos, no trabajo
de CPU. ThreadPoolExecutor ejecuta múltiples llamadas a git log
concurrentemente.
Parseo (CPU-bound) — cómputo puro. Cuando la cantidad de módulos es
suficientemente alta, ProcessPoolExecutor elude el GIL y usa múltiples
núcleos. En una versión de Python sin GIL (3.13+ free-threaded), se usa
ThreadPoolExecutor.
Estructura del Paquete
dependaman/
__init__.py API pública: dependaman()
__main__.py punto de entrada del CLI
core.py orquestación
discovery.py Fase 1 — descubrimiento de archivos
parser.py Fase 2 — parseo de imports
graph.py Fase 3 — construcción del grafo
analysis.py Fase 4 — pasadas de análisis
git.py Fase 5 — integración con git
renderer.py Fase 6 — salida HTML
pool.py selección de executor consciente del GIL
Restricciones de Diseño
Cero dependencias externas. La herramienta completa corre sobre la
biblioteca estándar de Python (ast, pathlib, json, subprocess,
concurrent.futures).
Salida agnóstica al framework. La función de renderizado retorna un string HTML — no sabe nada de cómo será servido:
def render(graph, analysis) -> str: ...
# FastAPI
@app.get("/graph", response_class=HTMLResponse)
def dependency_graph():
return render(build_graph("."), analyze(graph))
Parseo en un solo paso. Cada módulo se lee y parsea con AST una vez. El resultado se distribuye en imports del proyecto, grafo de módulos y grafo de callables en la misma ronda del pool.