Saltar a contenido

Guía de Uso

Esta guía cubre todo lo que necesitas saber para usar complexipy de manera efectiva en tus proyectos de Python.

Instalación

pip install complexipy
uv add complexipy
poetry add complexipy

Uso en Línea de Comandos

Análisis Básico

Analiza tu proyecto completo:

complexipy .

Analiza archivos o directorios específicos:

complexipy src/
complexipy src/main.py
complexipy src/ tests/

Establecer Umbral de Complejidad

El umbral predeterminado es 15. Las funciones que superen este valor serán resaltadas:

complexipy . --max-complexity-allowed 10

Filtrar Resultados

Mostrar solo las funciones que superen el umbral:

complexipy . --failed

Mostrar solo las N funciones más complejas entre todos los archivos analizados:

complexipy . --top 10

Mostrar sugerencias deterministas de refactorización para funciones fallidas:

complexipy . --failed --suggest-refactors

Cuando se usa --top, los resultados se reordenan globalmente por complejidad descendente antes de truncarse.

Suprimir la salida del análisis (útil para pipelines de CI):

complexipy . --quiet

Ordenar Resultados

Ordenar por puntuación de complejidad:

complexipy . --sort asc   # Ascendente (predeterminado)
complexipy . --sort desc  # Descendente
complexipy . --sort file_name  # Alfabéticamente por nombre de archivo

Excluir Archivos y Directorios

Excluir rutas específicas del análisis:

# Excluir un directorio recursivamente
complexipy . --exclude "tests/**"

# Excluir múltiples directorios recursivamente
complexipy . --exclude "tests/**" --exclude "migrations/**" --exclude "build/**"

# Excluir archivos específicos
complexipy . --exclude "src/legacy/old_code.py"

Cómo funciona la exclusión

  • Las exclusiones son patrones glob evaluados de forma relativa a cada ruta raíz proporcionada
  • Usa directory/** para excluir un directorio recursivamente
  • Usa una ruta relativa exacta, como src/legacy/old_code.py, para excluir un archivo
  • Las reglas de gitignore se siguen respetando durante el descubrimiento de archivos

Formatos de Salida

Guarda los resultados en JSON, CSV, GitLab Code Quality o SARIF:

# Salida JSON (guardada en complexipy-results.json)
complexipy . --output-format json

# Salida CSV (guardada en complexipy-results.csv)
complexipy . --output-format csv

# Ambos
complexipy . --output-format json --output-format csv

# Destino explícito para un solo formato
complexipy . --output-format gitlab --output complexipy-code-quality.json

# Varios formatos escritos en un directorio
complexipy . --output-format json --output-format sarif --output reports/

# Añade --suggest-refactors a cualquiera de los anteriores para incluir
# también los hallazgos de las reglas de refactorización (C001, C007, ...)
# como sus propios IDs de regla SARIF/GitLab, junto a los hallazgos de
# complejidad cognitiva que siempre están presentes.
complexipy . --output-format sarif --suggest-refactors

JSON, SARIF y GitLab Code Quality siguen la misma regla: los datos de los planes de refactorización (refactor_plans en JSON, hallazgos por regla en SARIF/GitLab) solo se emiten cuando también se pasa --suggest-refactors. Sin esa flag, solo aparecen los hallazgos del umbral de complejidad cognitiva -- igual que la salida enriquecida de la CLI, que también oculta las sugerencias de refactorización a menos que se active la flag.

Diff de Complejidad

Compara los resultados actuales contra cualquier referencia de git:

complexipy . --diff HEAD~1
complexipy . --diff main
complexipy src/ --max-complexity-allowed 10 --diff HEAD~1

Por defecto --diff aplica el umbral de complejidad: la ejecución termina con código 1 solo cuando un cambio rompe el contrato definido por --max-complexity-allowed:

  • Una función nueva introducida por encima del umbral.
  • Una función modificada cuya complejidad aumentó y queda por encima del umbral (incluye funciones ya sobre el umbral que empeoran).

Las funciones que aumentan su complejidad pero se mantienen en o por debajo del umbral (por ejemplo 3 → 4 con --max-complexity-allowed 15) no son fallos.

Para ver el diff visualmente sin afectar el código de salida, usa --diff-only en su lugar:

complexipy . --diff-only HEAD~1

Para comparar el contenido staged (el índice de git) en lugar del árbol de trabajo — "¿qué complejidad estoy a punto de commitear?" — añade --staged:

# Cambios staged vs HEAD (la línea base por defecto de --staged)
complexipy . --staged

# Cambios staged vs una referencia específica
complexipy . --diff main --staged

Igual que --diff, --staged aplica el umbral contra el contenido staged y falla cuando un cambio staged empuja una función por encima de --max-complexity-allowed. Las eliminaciones staged producen entradas REMOVED y las adiciones staged entradas NEW. Usa --diff-only con --staged para una vista solo visual.

Esto requiere git y una ruta dentro de un repositorio.

En lugar de repetir la referencia en cada llamada, declara la política de comparación una sola vez en un archivo de configuración — ver Configuración de Diff.

Salida en Texto Plano

Usa la salida en texto plano cuando necesites una línea legible por máquina por función:

complexipy . --plain
complexipy . --plain --top 5
complexipy . --plain --failed -mx 10

Cada línea se emite con este formato:

<path> <function> <complexity>

--plain es solo para CLI y no se puede combinar con --quiet.

Complejidad de Script a Nivel de Módulo

Usa --check-script para incluir el código a nivel de módulo en los resultados como <module>:

complexipy path/to/script.py --check-script
complexipy path/to/script.py --check-script -mx 5

Esto es útil para scripts con flujo de control complejo en el nivel superior, fuera de las funciones.

Sugerencias de Refactorización

Usa --suggest-refactors para imprimir un conjunto pequeño y ordenado de planes deterministas de refactorización junto a los resultados enriquecidos de la CLI:

complexipy . --failed --suggest-refactors

Salida de ejemplo (abreviada -- la salida real también muestra un tramo subrayado con acentos circunflejos, el código fuente circundante y un enlace a la documentación):

      [1] C007 Merge nested if statements
          --> sample.py:4:9
          Category: ◆ Readability | Applicability: * Safe to apply
          Lines 4-6 -> Estimated reduction: -2 complexity (6 -> 4)

          Suggestion: * Safe to apply
          Merge nested conditions into `if item.active and item.ready:`

Los planes se basan solo en el análisis AST de Rust; no se usa IA y no se reescribe código automáticamente. Las reducciones estimadas son aproximadas, ordenadas y limitadas, así que trátalas como orientación, no como puntuaciones futuras exactas. --plain --suggest-refactors mantiene la salida plana sin cambios.

Estructura de Salida JSON:

[
    {
        "path": "src",
        "file_name": "main.py",
        "function_name": "process_items",
        "complexity": 6,
        "refactor_plans": [
            {
                "rule_id": "C007",
                "kind": "collapsible_if",
                "title": "Merge nested if statements",
                "line_start": 4,
                "line_end": 6,
                "column_start": 9,
                "current_complexity": 6,
                "estimated_reduction": 2,
                "estimated_complexity_after": 4,
                "category": "Readability",
                "applicability": "MachineApplicable",
                "description": "Merge nested if statements into a single if with combined conditions",
                "explanation": "Nested if statements with a single body can be merged into a single if with combined conditions using 'and'. This reduces nesting and improves readability.",
                "references": [],
                "suggestion": {
                    "replacement": "        if item.active and item.ready:\n            total += item.value",
                    "applicability": "MachineApplicable",
                    "description": "Merge nested conditions into `if item.active and item.ready:`"
                },
                "help": null,
                "doc_url": "https://rohaquinlop.github.io/complexipy/refactoring-rules/#c007-collapsible-if"
            }
        ]
    }
]

La salida JSON contiene una entrada por cada función emitida. La lista refactor_plans solo se completa cuando también se pasa --suggest-refactors -- de lo contrario es [], igual que el comportamiento de la salida enriquecida de la CLI. La salida CSV no cambia y no incluye planes. Los rangos de líneas de funciones están disponibles mediante la API de Python (line_start, line_end), pero no se incluyen en las entradas de función JSON/CSV legibles por máquina de la CLI.

Salida en Color

Controla la salida en color:

complexipy . --color auto  # Predeterminado: detecta automáticamente la compatibilidad con la terminal
complexipy . --color yes   # Forza colores
complexipy . --color no    # Deshabilita colores

Archivos de Configuración

Prioridad de Configuración

complexipy carga la configuración en este orden (de mayor a menor prioridad):

  1. Argumentos de línea de comandos
  2. complexipy.toml
  3. .complexipy.toml
  4. pyproject.toml (bajo [tool.complexipy])

Configuraciones de Ejemplo

paths = ["src", "tests"]
max-complexity-allowed = 10
exclude = ["migrations/**", "build/**"]
snapshot-create = false
snapshot-ignore = false
quiet = false
ignore-complexity = false
failed = false
color = "auto"
sort = "asc"
output-format = ["json", "gitlab"]
output = "reports/"
check-script = false
no-ignore = false
report-ignored = false
[tool.complexipy]
paths = ["src", "tests"]
max-complexity-allowed = 10
exclude = ["migrations/**", "build/**"]
failed = true
sort = "desc"
check-script = true
# Archivo de configuración oculto para ajustes específicos del equipo
max-complexity-allowed = 15
exclude = ["venv/**", ".venv/**", "node_modules/**"]

check-script está soportado en TOML. --top y --plain son flags solo de CLI.

Configuración de Diff

La política de comparación se puede declarar una sola vez en el repositorio en lugar de pasar los mismos flags en cada llamada. Añade una sección [tool.complexipy.diff] al mismo archivo de configuración:

[diff]
branch = "main"
staged = true
[tool.complexipy.diff]
branch = "main"
staged = true
  • branch establece la referencia por defecto para --diff y --diff-only. Un complexipy . simple entonces se comporta como complexipy . --diff main, incluida la aplicación del umbral. Pasa --diff <ref> o --diff-only <ref> para sobrescribir la referencia en una ejecución concreta.
  • staged habilita la comparación staged por defecto, como pasar --staged en cada llamada.
  • Los flags de CLI siempre tienen prioridad sobre los valores de la sección.
  • branch = "" deshabilita el diff para el repositorio actual (opt-out).
  • Orden de resolución: flag de CLI, luego la sección diff.
  • Si la branch configurada no existe en el clon local (por ejemplo un clon recién hecho o shallow), cada función se reporta como NEW y la aplicación del umbral sigue activa. Haz fetch de la rama o pasa --diff <ref> con una referencia existente.

API de Python

Analizar Archivos

from complexipy import file_complexity

# Analizar un archivo
result = file_complexity("src/main.py", check_script=True)

print(f"Total complexity: {result.complexity}")
print(f"File path: {result.path}")

# Iterar sobre las funciones
for func in result.functions:
    print(f"{func.name}:")
    print(f"  Complexity: {func.complexity}")
    print(f"  Lines: {func.line_start}-{func.line_end}")

# Analizar sin respetar los comentarios de ignorado en línea
result = file_complexity("src/main.py", no_ignore=True)

Analizar Cadenas de Código

from complexipy import code_complexity

# Analizar fragmento de código
code = """
def calculate_discount(price, customer):
    if customer.is_premium:
        if price > 100:
            return price * 0.8
        else:
            return price * 0.9
    return price
"""

result = code_complexity(code, check_script=True)
print(f"Complexity: {result.complexity}")

for func in result.functions:
    print(f"{func.name}: {func.complexity}")

# Analizar cadena de código sin respetar los comentarios de ignorado
result = code_complexity(code, no_ignore=True)

Comparar Contra una Referencia de Git

compute_diff compara los resultados de complejidad actuales contra una referencia de git (commit, etiqueta o rama) y devuelve objetos DiffEntry — uno por función que cambió, apareció o desapareció. has_regressions informa si alguna entrada supera un umbral de complejidad (una función REGRESSED o NEW por encima de max_complexity).

from complexipy import (
    compute_diff,
    has_regressions,
    file_complexity,
    DiffEntry,
    DiffStatus,
)

# Analizar el estado actual de los archivos de interés
current = [file_complexity(p) for p in changed_files]

# Comparar contra una referencia de git (el directorio de trabajo es el cwd por defecto)
entries = compute_diff(current, "origin/main")

# Filtrar regresiones por encima de tu umbral
regressions = [
    e
    for e in entries
    if e.status == DiffStatus.REGRESSED and e.new_complexity > 15
]

# O usar la compuerta de ratchet directamente (falla con REGRESSED/NEW sobre el umbral)
if has_regressions(entries, 15):
    raise SystemExit("Regresiones de complejidad detectadas")

DiffEntry expone file_path, func_name, old_complexity y new_complexity (cualquiera puede ser None para funciones NEW / REMOVED), más las propiedades status y delta. status es un miembro de DiffStatus — un enum basado en str, por lo que compara igual a su valor de cadena (p. ej. DiffStatus.REGRESSED == "REGRESSED").

for e in entries:
    if e.status != DiffStatus.UNCHANGED:
        print(f"{e.file_path}::{e.func_name}: {e.status} {e.delta}")

Uso Práctico de la API

Ejemplo: Hook de Pre-commit

#!/usr/bin/env python3
"""Verifica la complejidad de los archivos Python en staging."""
import sys
from pathlib import Path
from complexipy import file_complexity

MAX_COMPLEXITY = 15

def main():
    # Obtener los archivos Python en staging (integrar con git)
    staged_files = get_staged_python_files()

    violations = []
    for filepath in staged_files:
        result = file_complexity(str(filepath))

        for func in result.functions:
            if func.complexity > MAX_COMPLEXITY:
                violations.append({
                    'file': filepath,
                    'function': func.name,
                    'complexity': func.complexity,
                    'line': func.line_start
                })

    if violations:
        print("Complexity violations found:")
        for v in violations:
            print(f"  {v['file']}:{v['line']} - "
                  f"{v['function']} (complexity: {v['complexity']})")
        sys.exit(1)

    print("All functions pass complexity check!")
    sys.exit(0)

if __name__ == "__main__":
    main()

Ejemplo: Panel de Calidad de Código

from pathlib import Path
from complexipy import file_complexity
import json

def analyze_project(root_path: str):
    """Genera un informe de complejidad para todo el proyecto."""
    project = Path(root_path)
    results = []

    for py_file in project.rglob("*.py"):
        if "venv" in str(py_file) or ".venv" in str(py_file):
            continue

        try:
            result = file_complexity(str(py_file))
            results.append({
                'file': str(py_file),
                'complexity': result.complexity,
                'functions': [
                    {
                        'name': f.name,
                        'complexity': f.complexity,
                        'line_start': f.line_start,
                        'line_end': f.line_end
                    }
                    for f in result.functions
                ]
            })
        except Exception as e:
            print(f"Error analyzing {py_file}: {e}")

    # Ordenar por complejidad
    results.sort(key=lambda x: x['complexity'], reverse=True)

    # Guardar informe
    with open("complexity-report.json", "w") as f:
        json.dump(results, f, indent=2)

    # Imprimir resumen
    total_files = len(results)
    total_complexity = sum(r['complexity'] for r in results)
    avg_complexity = total_complexity / total_files if total_files else 0

    print(f"Analyzed {total_files} files")
    print(f"Total complexity: {total_complexity}")
    print(f"Average complexity: {avg_complexity:.2f}")

    # Los 10 archivos más complejos
    print("\nTop 10 most complex files:")
    for r in results[:10]:
        print(f"  {r['file']}: {r['complexity']}")

if __name__ == "__main__":
    analyze_project("./src")

Snapshots Base

Los snapshots te permiten adoptar complexipy gradualmente en bases de código grandes y existentes.

Crear un Snapshot

complexipy . --snapshot-create --max-complexity-allowed 15

Esto crea complexipy-snapshot.json en tu directorio de trabajo, registrando todas las funciones que actualmente superan el umbral.

Cómo Funcionan las snapshots

Una vez que exista un snapshot, complexipy:

  • Pasa: Funciones que ya estaban en el snapshot y no han empeorado
  • Pasa: Funciones que mejoraron (se eliminan automáticamente del snapshot)
  • Falla: Funciones nuevas que superan el umbral
  • Falla: Funciones rastreadas que se volvieron más complejas

Usar snapshots en CI

# .github/workflows/complexity.yml
name: Complexity Check

on: [push, pull_request]

jobs:
    check:
        runs-on: ubuntu-latest
        steps:
            - uses: actions/checkout@v4

            - name: Install complexipy
              run: pip install complexipy

            - name: Check complexity
              run: complexipy . --max-complexity-allowed 15

El archivo de snapshot (complexipy-snapshot.json) debería ser commiteado al control de versiones.

Ignorar snapshots

Deshabilitar temporalmente la verificación de snapshots:

complexipy . --snapshot-ignore

Úsalo al:

  • Refactorizar múltiples archivos a la vez
  • Regenerar la línea base
  • Probar diferentes umbrales

Formato del Archivo de snapshot

[
    {
        "path": "src",
        "file_name": "legacy.py",
        "functions": [
            {
                "name": "old_function",
                "complexity": 23
            }
        ]
    }
]

Los snapshots se guardan como un arreglo JSON de archivos analizados. Cada entrada contiene solo las funciones por encima del umbral cuando se escribió el snapshot. El archivo se reescribe después de verificaciones de snapshot exitosas, así que las funciones que mejoran se eliminan automáticamente. Las actualizaciones solo afectan a los archivos analizados en la ejecución — las entradas de archivos fuera del análisis se conservan, por lo que ejecutar sobre un subconjunto de archivos (por ejemplo, a través de un hook de pre-commit) nunca reduce la línea base. Es posible que los snapshots creados por versiones anteriores de complexipy deban regenerarse con --snapshot-create.

Ignorar en Línea

Suprime las advertencias de complejidad para funciones específicas usando el comentario # complexipy: ignore:

def complex_legacy_function():  # complexipy: ignore
    # Lógica compleja que no puede ser refactorizada
    pass

# O con un motivo
def another_complex_function():  # complexipy: ignore (technical debt: issue #123)
    pass

El comentario de ignorar también puede colocarse en la línea anterior a la definición de la función:

# complexipy: ignore
def complex_function():
    pass

Sintaxis Obsoleta

La sintaxis # noqa: complexipy está obsoleta y será eliminada en una versión futura. Por favor, migra a # complexipy: ignore en su lugar.

¿Por qué? Herramientas como yesqa eliminan automáticamente los comentarios # noqa que flake8 no reconoce, lo que eliminaría silenciosamente tus supresiones de complexipy. La nueva sintaxis evita este conflicto por completo.

Usar con Moderación

Los ignorados en línea deben ser temporales. Documenta por qué la complejidad es necesaria y rastrea la deuda técnica.

Deshabilitar Ignorados en Línea

Usa --no-ignore para ignorar todos los comentarios de supresión y analizar cada función:

complexipy . --no-ignore

Las funciones previamente suprimidas por # complexipy: ignore o # noqa: complexipy se analizarán normalmente y pueden superar el umbral.

Reportar Funciones Ignoradas

Usa --report-ignored para listar cada ubicación donde un comentario de ignore suprime una función:

# Listar funciones ignoradas
complexipy . --report-ignored

# Combinar con --no-ignore para reportar mientras se analiza todo
complexipy . --report-ignored --no-ignore

El formato de salida es ruta:línea # texto-del-comentario. Cuando --output-format json también está activo, las ubicaciones ignoradas se exportan a complexipy-ignored.json. El informe se imprime incluso bajo --quiet.

Ambos flags también están disponibles en la API de Python mediante no_ignore=True:

from complexipy import file_complexity, code_complexity

# Analizar sin respetar los comentarios de ignore
result = file_complexity("app.py", no_ignore=True)

Para recolectar ubicaciones ignoradas programáticamente, usa collect_all_ignored_locations():

from complexipy import collect_all_ignored_locations

locations, failed = collect_all_ignored_locations(
    paths=["src"],
    exclude=["tests/"],
)
for loc in locations:
    print(f"{loc.path}:{loc.line}  {loc.comment}")

Eliminar Comentarios de Ignore Obsoletos

Un comentario de ignore solo es necesario mientras la complejidad de la función suprimida supere --max-complexity-allowed. Cuando la complejidad de la función baje al límite o por debajo de él, el comentario queda obsoleto y puede eliminarse. complexipy detecta esto automáticamente en cada ejecución e informa las ubicaciones que puedes limpiar:

complexipy .

Ejemplo de salida:

The following ignore comment(s) are no longer necessary (complexity is within the allowed limit) and can be removed:
src/legacy.py:42  function=parse_legacy_config complexity=8  # complexipy: ignore

El informe es puramente informativo: nunca afecta al código de salida y se suprime bajo --quiet. La misma detección está disponible programáticamente vía collect_removable_ignored_locations():

from complexipy import collect_removable_ignored_locations

removable, failed = collect_removable_ignored_locations(
    paths=["src"],
    exclude=["tests/"],
    max_complexity_allowed=15,
)
for rem in removable:
    print(f"{rem.path}:{rem.line}  function={rem.function} complexity={rem.complexity}")

Integración con CI/CD

GitHub Actions

Usa la acción oficial:

- uses: rohaquinlop/complexipy-action@v2
  with:
      paths: src tests
      max_complexity_allowed: 15
      output_format: json

O ejecuta directamente:

- name: Check complexity
  run: |
      pip install complexipy
      complexipy . --max-complexity-allowed 15

Hook de Pre-commit

Agrega a .pre-commit-config.yaml:

repos:
    - repo: https://github.com/rohaquinlop/complexipy-pre-commit
      rev: v5.1.0
      hooks:
          - id: complexipy
            args: [--max-complexity-allowed=15]

GitLab CI

.complexipy_code_quality:
    image: python:3.11
    script:
        - pip install complexipy
        - complexipy . --output-format gitlab --output complexipy-code-quality.json --ignore-complexity --max-complexity-allowed 15
    artifacts:
        when: always
        reports:
            codequality: complexipy-code-quality.json

complexity:
    extends: .complexipy_code_quality
    rules:
        - if: $CI_PIPELINE_SOURCE == "merge_request_event"
        - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

Usa --ignore-complexity cuando tu objetivo principal sea publicar el reporte aunque existan violaciones. GitLab seguirá mostrando los hallazgos en el widget del merge request y en la interfaz del pipeline.

Si además quieres que el job falle cuando se supere el umbral, separa el flujo en dos jobs:

complexity_check:
    image: python:3.11
    script:
        - pip install complexipy
        - complexipy . --max-complexity-allowed 15

complexity_report:
    image: python:3.11
    script:
        - pip install complexipy
        - complexipy . --output-format gitlab --output complexipy-code-quality.json --ignore-complexity --max-complexity-allowed 15
    artifacts:
        when: always
        reports:
            codequality: complexipy-code-quality.json

Integración con VS Code

Instala la extensión de complexipy para análisis de complejidad en tiempo real:

  • Puntuaciones de complejidad en línea
  • Tooltips al pasar el cursor con detalles
  • Indicadores con código de color
  • Sugerencias de corrección rápida

Consejos y Mejores Prácticas

1. Comienza con Umbrales Altos

Al introducir complexipy en una base de código existente:

# Crear línea base
complexipy . --snapshot-create --max-complexity-allowed 25

# Reducir gradualmente el umbral con el tiempo
complexipy . --max-complexity-allowed 20
complexipy . --max-complexity-allowed 15

2. Enfócate en el Código de Alto Tráfico

No todo el código complejo necesita refactorización inmediata:

# Centrarse en los archivos que cambian con frecuencia
complexipy src/core/ --max-complexity-allowed 10
complexipy src/legacy/ --max-complexity-allowed 25

3. Usar con Revisiones de Código

# Verificar solo los archivos en la rama actual
git diff --name-only main | grep '.py$' | xargs complexipy

4. Combinar con Cobertura de Pruebas

Alta complejidad + baja cobertura = alto riesgo

# Verificar cobertura para funciones complejas
pytest --cov=src --cov-report=term-missing
complexipy src/ --failed

5. Rastrear Tendencias

# Generar datos históricos
complexipy . --output-format json
# Hacer commit de complexipy-results.json para rastrear cambios a lo largo del tiempo

Solución de Problemas

No se encontraron archivos Python

Asegúrate de estar en el directorio correcto y de que tus archivos tengan extensiones .py.

Errores de sintaxis en archivos analizados

complexipy requiere sintaxis Python válida. Corrige primero los errores de sintaxis:

python -m py_compile file.py

Problemas de rendimiento en bases de código grandes

Excluir directorios innecesarios:

complexipy . --exclude "venv/**" --exclude ".venv/**" --exclude "node_modules/**"

Resultados diferentes a los esperados

Verifica la precedencia de los archivos de configuración. Usa --help para ver la configuración activa:

complexipy --help

Próximos Pasos