Contribuir a BESSER
Este skill cubre los flujos de trabajo procedurales para contribuir al código base de BESSER. Para detalles de arquitectura y convenciones de código, consulta también el archivo CLAUDE.md del proyecto, que se carga automáticamente en el contexto.
Configuración de Desarrollo
git clone https://github.com/<tu-usuario>/BESSER.git
cd BESSER
python -m venv venv
# Windows: venv\Scripts\activate
# macOS/Linux: source venv/bin/activate
pip install -r requirements.txt
pip install -r docs/requirements.txt # para construir la documentación
pip install -e . # instalación editable
Verificar: python tests/BUML/metamodel/structural/library/library.py
Se requiere Python 3.10+ (dependencia de Django 5.x).
Añadir un Nuevo Generador
Este es el tipo de contribución más común. Para omitir el código repetitivo, ejecuta el andamio incluido:
python scripts/scaffold_generator.py <nombre> <ruta/al/repositorio/BESSER>
# ej.: python scripts/scaffold_generator.py graphql ~/code/BESSER
Esto escribe un paquete básico funcional (clase generadora + plantilla Jinja + pytest), ejecutable inmediatamente. Aún necesitas implementar la lógica real, registrar el generador en la configuración del editor web y añadir documentación, cubierto en los pasos siguientes.
Si prefieres hacerlo manualmente, sigue estos 6 pasos:
Paso 1: Crear el Paquete del Generador
besser/generators/mi_generador/
__init__.py # exportar: from .mi_generador import MiGenerador
mi_generador.py # la clase generadora
templates/ # plantillas Jinja2
plantilla_salida.py.j2
Copia el andamio de un generador simple existente (ej., python_classes/).
Paso 2: Implementar GeneratorInterface
# besser/generators/mi_generador/mi_generador.py
import os
from jinja2 import Environment, FileSystemLoader
from besser.generators import GeneratorInterface
class MiGenerador(GeneratorInterface):
def __init__(self, model, output_dir: str = None):
super().__init__(model, output_dir)
def generate(self):
"""Generar salida a partir del modelo B-UML."""
templates_path = os.path.join(os.path.dirname(__file__), "templates")
env = Environment(
loader=FileSystemLoader(templates_path),
trim_blocks=True,
lstrip_blocks=True,
)
template = env.get_template("plantilla_salida.py.j2")
output = template.render(
classes=self.model.get_classes(),
associations=self.model.associations,
)
file_path = self.build_generation_path("mi_salida.py")
with open(file_path, "w", encoding="utf-8") as f:
f.write(output)
Requisitos clave:
- Llamar a
super().__init__(model, output_dir)— esto configuraself.modelyself.output_dir. - Usar
self.build_generation_path(filename)para obtener la ruta correcta del archivo de salida. - Usar
self.build_generation_dir()si solo necesitas la ruta del directorio. - Las plantillas Jinja2 deben estar en un subdirectorio
templates/junto al generador. - La salida debe ser determinista — el mismo modelo produce la misma salida cada vez (sin marcas de tiempo, valores aleatorios, etc.).
- Los ayudantes reutilizables van en
besser/utilities/, no en el generador.
Paso 3: Escribir Plantillas Jinja2
{# besser/generators/mi_generador/templates/plantilla_salida.py.j2 #}
# Generado por BESSER MiGenerador
{% for cls in classes %}
class {{ cls.name }}:
{% for attr in cls.attributes %}
{{ attr.name }}: {{ attr.type.name }}
{% endfor %}
{% endfor %}
Si se genera JSX/React, usa delimitadores no estándar para evitar conflictos:
env = Environment(
loader=FileSystemLoader(templates_path),
variable_start_string="[[",
variable_end_string="]]",
)
Paso 4: Registrar en el Backend del Editor Web
Añade tu generador a besser/utilities/web_modeling_editor/backend/config/generators.py:
from besser.generators.mi_generador import MiGenerador
GENERATORS = {
# ... generadores existentes ...
"mi_generador": GeneratorInfo(
generator_class=MiGenerador,
output_type="file", # "file" para un solo archivo, "zip" para múltiples archivos
file_extension=".py", # extensión del archivo de salida
category="mi_categoria", # etiqueta de agrupación
requires_class_diagram=True, # True si necesita un DomainModel
),
}
También actualiza get_filename_for_generator() en el mismo archivo si el generador produce un nombre de archivo específico.
Paso 5: Escribir Pruebas
Crea tests/generators/mi_generador/test_mi_generador.py:
import os
import shutil
import pytest
from besser.BUML.metamodel.structural import (
DomainModel, Class, Property, BinaryAssociation,
Multiplicity, StringType, IntegerType,
)
from besser.generators.mi_generador import MiGenerador
@pytest.fixture
def domain_model():
"""Construir un modelo de prueba pequeño."""
name_prop = Property(name="name", type=StringType)
age_prop = Property(name="age", type=IntegerType)
person = Class(name="Person", attributes={name_prop, age_prop})
model = DomainModel(name="TestModel", types={person})
return model
@pytest.fixture
def output_dir(tmp_path):
return str(tmp_path)
def test_generate_creates_output_file(domain_model, output_dir):
gen = MiGenerador(model=domain_model, output_dir=output_dir)
gen.generate()
assert os.path.exists(os.path.join(output_dir, "mi_salida.py"))
def test_generate_contains_class_name(domain_model, output_dir):
gen = MiGenerador(model=domain_model, output_dir=output_dir)
gen.generate()
with open(os.path.join(output_dir, "mi_salida.py")) as f:
content = f.read()
assert "Person" in content
assert "name" in content
assert "age" in content
Patrones de prueba utilizados en todo el código base:
- Usa la fixture
tmp_pathotmpdirpara directorios de salida (se limpian automáticamente). - Construye modelos pequeños en línea como fixtures — no dependas de archivos externos.
- Prueba tanto la estructura (archivos existen, nombres de clases presentes) como el contenido (lógica de negocio).
- Para generadores que producen Python importable, usa
importlibpara importar dinámicamente y probar el código generado. - Limpia con
shutil.rmtree()si no usas fixtures tmp de pytest.
Paso 6: Documentar
Añade documentación en docs/source/generators/mi_generador.rst:
MiGenerador
===========
El MiGenerador produce ... a partir de un modelo de dominio B-UML.
Prerrequisitos
--------------
- BESSER instalado
- ...
Uso
----
.. code-block:: python
from besser.generators.mi_generador import MiGenerador
gen = MiGenerador(model=mi_modelo, output_dir="./output")
gen.generate()
Salida
------
El generador produce un solo archivo ``mi_salida.py`` que contiene ...
Añádelo al índice de generadores en docs/source/generators.rst.
Añadir un Nuevo Meta-Modelo / Sub-DSL
Paso 1: Diseñar el Meta-Modelo
Crea nuevas clases en besser/BUML/metamodel/tu_dsl/:
besser/BUML/metamodel/tu_dsl/
__init__.py
tu_dsl.py # clases del metamodelo
Sigue los patrones existentes:
- Extiende
NamedElementoElementcomo clases base. - Usa propiedades privadas con validación de getter/setter.
- Mantén la nomenclatura coherente con los paquetes de metamodelo existentes.
Paso 2: Añadir Convertidores (si se necesita integración con el editor web)
Los convertidores traducen entre el JSON del frontend y los objetos B-UML:
besser/utilities/web_modeling_editor/backend/services/converters/
json_to_buml/process_tu_dsl.py # JSON → BUML
buml_to_json/tu_dsl_to_json.py # BUML → JSON
Los convertidores deben ser simétricos: cualquier característica soportada en una dirección debe funcionar en la otra. Prueba viajes de ida y vuelta: JSON → BUML → JSON debe producir el original.
Paso 3: Añadir Validación
Si el metamodelo tiene restricciones más allá de lo que aplican los setters:
def validate(self, raise_exception=True):
errors = []
warnings = []
# Verificar reglas estructurales
if alguna_mala_condicion:
errors.append("Mensaje de error descriptivo")
result = {"success": len(errors) == 0, "errors": errors, "warnings": warnings}
if raise_exception and errors:
raise ValueError("\n".join(errors))
return result
Paso 4: Conectar al Backend
- Añade endpoint(s) en
besser/utilities/web_modeling_editor/backend/backend.py. - Añade modelos Pydantic de solicitud/respuesta en
backend/models/. - Añade una ruta de validación bajo
/validate-diagramsi corresponde.
Paso 5: Escribir Pruebas
Coloca las pruebas en tests/BUML/metamodel/tu_dsl/:
- Pruebas de construcción de modelos válidos.
- Pruebas de modelos inválidos (esperar
ValueError,TypeError). - Pruebas de ida y vuelta para convertidores si corresponde.
Paso 6: Documentar
- Documentación del metamodelo:
docs/source/buml_language/tu_dsl.rst - Añadir al índice
docs/source/buml_language.rst.
Pruebas
Ejecutar Todas las Pruebas
python -m pytest
Ejecutar Pruebas Específicas
# Por directorio
python -m pytest tests/generators/sqlalchemy/
# Por palabra clave
python -m pytest -k "test_django"
# Con salida verbosa
python -m pytest -v tests/BUML/metamodel/structural/
# Detener en el primer fallo
python -m pytest -x
Convenciones de Pruebas
- Archivos de prueba:
test_*.py - Funciones de prueba:
test_* - Usa
@pytest.fixturepara modelos y directorios reutilizables. - Usa
tmp_pathpara directorios de salida (se limpian automáticamente por pytest). - Afirma contenido específico, no solo la existencia del archivo.
- Prueba condiciones de error con
pytest.raises(ValueError).
Qué Probar
Para cambios en el metamodelo: validación de construcción, restricciones de setters, errores esperados para entrada inválida, resultados de validate().
Para generadores: existencia de archivo de salida, contenido clave en la salida (nombres de clases, mapeos de relaciones), casos límite (modelo vacío, herencia profunda, muchos a muchos).
Para convertidores: fidelidad de ida y vuelta (JSON → BUML → JSON debe ser equivalente).
Construir Documentación
cd docs
# Windows:
make.bat html
# macOS/Linux:
make html
# Ver:
# Abre docs/build/html/index.html en tu navegador
La documentación usa Sphinx con el tema Furo. Los archivos fuente son reStructuredText (.rst) en docs/source/.
Estructura de la Documentación
| Ruta | Contenido |
|---|---|
docs/source/buml_language/ | Documentación del metamodelo |
docs/source/generators/ | Documentación del generador |
docs/source/web_editor.rst | Documentación de la API del editor web |
docs/source/utilities/ | Documentación de utilidades |
docs/source/contributing/ | Guías para contribuidores |
docs/source/releases/ | Notas de versión |
docs/source/_static/ | Imágenes y activos compartidos |
docs/source/img/ | Diagramas y capturas de pantalla |
Referencias Cruzadas
Usa roles de Sphinx para enlaces cruzados:
:doc:\generators/django`` — enlace a otra página de documentación:mod:\besser.generators.django`` — enlace a la API del módulo:class:\besser.BUML.metamodel.structural.Class`` — enlace a la API de la clase:ref:\nombre-etiqueta`` — enlace a una sección etiquetada
Estilo de Código
- PEP 8 con sangría de 4 espacios.
- Límite de línea de 120 caracteres (configurado en
pyproject.tomlbajo[tool.pylint.'FORMAT']). - Anotaciones de tipo para APIs públicas.
- Docstrings PEP 257.
- Orden de importaciones: biblioteca estándar, terceros, módulos locales.
snake_casepara funciones/variables,PascalCasepara clases,UPPER_CASEpara constantes.- Sin importaciones comodín, sin reexportaciones implícitas.
- Linting: pylint (sin hooks de pre-commit de ruff/black actualmente configurados).
Convenciones de Commits y PR
Mensajes de Commit
Usa Conventional Commits:
feat: añadir generador Terraform para AWS EKS
fix: corregir análisis de multiplicidad para asociaciones 0..*
refactor: extraer ayudantes de plantilla compartidos a besser.utilities
docs: documentar uso de QiskitGenerator
test: añadir pruebas de ida y vuelta para el convertidor de modelo GUI
Flujo de Trabajo de Pull Request
- Crea una rama temática:
git checkout -b feature/añadir-mi-generador. - Realiza commits enfocados y agrupados lógicamente.
- Ejecuta pruebas localmente:
python -m pytest. - Construye docs:
cd docs && make html. - Rebase sobre el último
master:git fetch origin && git rebase origin/master. - Sube y abre PR contra
master. - Completa la plantilla del PR: descripción, pruebas ejecutadas, contexto adicional.
- Se requieren dos aprobaciones de mantenedores para fusionar (una aprobación es suficiente después de 14 días).
Cambios Cruzados entre Repositorios (BESSER + Frontend)
Cuando los cambios afectan tanto a BESSER como al frontend del editor web:
- Implementa y confirma los cambios de WME en el repositorio WME.
- Implementa los cambios de BESSER en este repositorio.
- Actualiza el puntero del submódulo:
cd besser/utilities/web_modeling_editor/frontend git fetch && git checkout <commit> cd ../../../.. git add besser/utilities/web_modeling_editor/frontend - Vincula los dos PRs para que los revisores fusionen en el orden correcto.
CI / Lanzamiento
- CI: No hay un flujo de trabajo de pruebas automatizado — las pruebas se ejecutan localmente antes de los PRs.
- Lanzamiento: Se activa creando un lanzamiento de GitHub. El flujo de trabajo
python-publish.ymlconstruye y publica en PyPI usandoPYPI_API_TOKEN. - Versión: Definida en
setup.cfgbajo[metadata] version. - Notas de versión: Añadidas como archivos RST en
docs/source/releases/.
Errores Comunes al Contribuir
- No duplicar lógica — los ayudantes compartidos van en
besser/utilities/, no en generadores individuales. - Mantener determinismo — los generadores deben producir salida idéntica para entrada idéntica.
- Mantener convertidores simétricos — si JSON → BUML soporta una característica, BUML → JSON también debe.
- Actualizar docs — cualquier cambio en el backend probablemente necesita actualizaciones en
docs/source/. - No tocar el submódulo del frontend a menos que sea explícitamente necesario. Los cambios de UI van al repositorio WME ascendente.
- Limpiar recursos — usa siempre
try/finallypara directorios temporales y manejadores de archivos. - Probar ida y vuelta — especialmente para convertidores (JSON → BUML → JSON debe ser identidad).