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¶
Uso en Línea de Comandos¶
Análisis Básico¶
Analiza tu proyecto completo:
Analiza archivos o directorios específicos:
Establecer Umbral de Complejidad¶
El umbral predeterminado es 15. Las funciones que superen este valor serán resaltadas:
Filtrar Resultados¶
Mostrar solo las funciones que superen el umbral:
Mostrar solo las N funciones más complejas entre todos los archivos analizados:
Mostrar sugerencias deterministas de refactorización para funciones fallidas:
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):
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:
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:
Cada línea se emite con este formato:
--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>:
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:
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):
- Argumentos de línea de comandos
complexipy.toml.complexipy.tomlpyproject.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
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:
branchestablece la referencia por defecto para--diffy--diff-only. Uncomplexipy .simple entonces se comporta comocomplexipy . --diff main, incluida la aplicación del umbral. Pasa--diff <ref>o--diff-only <ref>para sobrescribir la referencia en una ejecución concreta.stagedhabilita la comparación staged por defecto, como pasar--stageden 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
branchconfigurada no existe en el clon local (por ejemplo un clon recién hecho o shallow), cada función se reporta comoNEWy 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¶
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:
Ú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:
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:
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:
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:
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:
Problemas de rendimiento en bases de código grandes¶
Excluir directorios innecesarios:
Resultados diferentes a los esperados¶
Verifica la precedencia de los archivos de configuración. Usa --help para ver la configuración activa:
Próximos Pasos¶
- Lee Entendiendo las Puntuaciones de Complejidad para interpretar los resultados
- Consulta Comparación con Ruff para herramientas complementarias
- Visita Acerca de complexipy para obtener más información sobre el proyecto