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

GrafoNodosAristas
Módulopkg.modA importa B
Callablepkg.mod::Func, pkg.mod::Class.methodA 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.