Contribution à BESSER
Ce skill couvre les workflows procéduraux pour contribuer à la base de code BESSER.
Pour les détails d'architecture et les conventions de code, consultez également
le fichier CLAUDE.md du projet qui est chargé automatiquement dans le contexte.
Configuration de développement
git clone https://github.com/<votre-utilisateur>/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 # pour construire la doc
pip install -e . # installation modifiable
Vérification : python tests/BUML/metamodel/structural/library/library.py
Python 3.10+ est requis (dépendance Django 5.x).
Ajout d'un nouveau générateur
C'est le type de contribution le plus courant. Pour éviter le code standard, exécutez le squelette fourni :
python scripts/scaffold_generator.py <nom> <chemin/vers/dépôt/BESSER>
# ex : python scripts/scaffold_generator.py graphql ~/code/BESSER
Cela crée un package squelette fonctionnel (classe générateur + template Jinja + pytest), exécutable immédiatement. Vous devez encore implémenter la logique réelle, enregistrer le générateur dans la configuration de l'éditeur web et ajouter la documentation — couvert dans les étapes ci-dessous.
Si vous préférez le faire à la main, suivez ces 6 étapes :
Étape 1 : Créer le package du générateur
besser/generators/my_generator/
__init__.py # export : from .my_generator import MyGenerator
my_generator.py # la classe du générateur
templates/ # templates Jinja2
output_template.py.j2
Copiez le squelette d'un générateur simple existant (par exemple, python_classes/).
Étape 2 : Implémenter GeneratorInterface
# besser/generators/my_generator/my_generator.py
import os
from jinja2 import Environment, FileSystemLoader
from besser.generators import GeneratorInterface
class MyGenerator(GeneratorInterface):
def __init__(self, model, output_dir: str = None):
super().__init__(model, output_dir)
def generate(self):
"""Génère la sortie à partir du modèle 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("output_template.py.j2")
output = template.render(
classes=self.model.get_classes(),
associations=self.model.associations,
)
file_path = self.build_generation_path("my_output.py")
with open(file_path, "w", encoding="utf-8") as f:
f.write(output)
Exigences clés :
- Appelez
super().__init__(model, output_dir)— cela configureself.modeletself.output_dir. - Utilisez
self.build_generation_path(filename)pour obtenir le chemin de fichier de sortie correct. - Utilisez
self.build_generation_dir()si vous avez seulement besoin du chemin du répertoire. - Les templates Jinja2 doivent être dans un sous-répertoire
templates/à côté du générateur. - La sortie doit être déterministe — le même modèle produit la même sortie à chaque fois (pas d'horodatages, valeurs aléatoires, etc.).
- Les aides réutilisables vont dans
besser/utilities/, pas dans le générateur.
Étape 3 : Écrire les templates Jinja2
{# besser/generators/my_generator/templates/output_template.py.j2 #}
# Généré par BESSER MyGenerator
{% for cls in classes %}
class {{ cls.name }}:
{% for attr in cls.attributes %}
{{ attr.name }}: {{ attr.type.name }}
{% endfor %}
{% endfor %}
Si vous générez du JSX/React, utilisez des délimiteurs non standard pour éviter les conflits :
env = Environment(
loader=FileSystemLoader(templates_path),
variable_start_string="[[",
variable_end_string="]]",
)
Étape 4 : Enregistrer dans le backend de l'éditeur web
Ajoutez votre générateur à besser/utilities/web_modeling_editor/backend/config/generators.py :
from besser.generators.my_generator import MyGenerator
GENERATORS = {
# ... générateurs existants ...
"my_generator": GeneratorInfo(
generator_class=MyGenerator,
output_type="file", # "file" pour un fichier unique, "zip" pour plusieurs fichiers
file_extension=".py", # extension de fichier de la sortie
category="my_category", # étiquette de regroupement
requires_class_diagram=True, # True s'il a besoin d'un DomainModel
),
}
Mettez également à jour get_filename_for_generator() dans le même fichier si le générateur
produit un nom de fichier spécifique.
Étape 5 : Écrire les tests
Créez tests/generators/my_generator/test_my_generator.py :
import os
import shutil
import pytest
from besser.BUML.metamodel.structural import (
DomainModel, Class, Property, BinaryAssociation,
Multiplicity, StringType, IntegerType,
)
from besser.generators.my_generator import MyGenerator
@pytest.fixture
def domain_model():
"""Construit un petit modèle de test."""
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 = MyGenerator(model=domain_model, output_dir=output_dir)
gen.generate()
assert os.path.exists(os.path.join(output_dir, "my_output.py"))
def test_generate_contains_class_name(domain_model, output_dir):
gen = MyGenerator(model=domain_model, output_dir=output_dir)
gen.generate()
with open(os.path.join(output_dir, "my_output.py")) as f:
content = f.read()
assert "Person" in content
assert "name" in content
assert "age" in content
Modèles de test utilisés dans toute la base de code :
- Utilisez la fixture
tmp_pathoutmpdirpour les répertoires de sortie (nettoyés automatiquement). - Construisez de petits modèles en ligne comme fixtures — ne dépendez pas de fichiers externes.
- Testez à la fois la structure (fichiers existent, noms de classe présents) et le contenu (logique métier).
- Pour les générateurs qui produisent du Python importable, utilisez
importlibpour importer dynamiquement et tester le code généré. - Nettoyez avec
shutil.rmtree()si vous n'utilisez pas les fixtures tmp de pytest.
Étape 6 : Documenter
Ajoutez la documentation dans docs/source/generators/my_generator.rst :
MyGenerator
===========
Le MyGenerator produit ... à partir d'un modèle de domaine B-UML.
Prérequis
---------
- BESSER installé
- ...
Utilisation
----------
.. code-block:: python
from besser.generators.my_generator import MyGenerator
gen = MyGenerator(model=mon_modèle, output_dir="./sortie")
gen.generate()
Sortie
------
Le générateur produit un seul fichier ``my_output.py`` contenant ...
Ajoutez-le à l'index des générateurs dans docs/source/generators.rst.
Ajout d'un nouveau métamodèle / sous-DSL
Étape 1 : Concevoir le métamodèle
Créez de nouvelles classes dans besser/BUML/metamodel/your_dsl/ :
besser/BUML/metamodel/your_dsl/
__init__.py
your_dsl.py # classes du métamodèle
Suivez les schémas existants :
- Étendez
NamedElementouElementcomme classes de base. - Utilisez des propriétés privées avec validation getter/setter.
- Gardez la nomenclature cohérente avec les packages de métamodèles existants.
Étape 2 : Ajouter des convertisseurs (si intégration dans l'éditeur web nécessaire)
Les convertisseurs traduisent entre le JSON du frontend et les objets B-UML :
besser/utilities/web_modeling_editor/backend/services/converters/
json_to_buml/process_your_dsl.py # JSON → BUML
buml_to_json/your_dsl_to_json.py # BUML → JSON
Les convertisseurs doivent être symétriques : toute fonctionnalité prise en charge dans un sens doit
fonctionner dans l'autre. Testez les allers-retours : JSON → BUML → JSON doit produire l'original.
Étape 3 : Ajouter la validation
Si le métamodèle a des contraintes au-delà de ce que les setters appliquent :
def validate(self, raise_exception=True):
errors = []
warnings = []
# Vérifier les règles structurelles
if une_mauvaise_condition:
errors.append("Message d'erreur descriptif")
result = {"success": len(errors) == 0, "errors": errors, "warnings": warnings}
if raise_exception and errors:
raise ValueError("\n".join(errors))
return result
Étape 4 : Câbler dans le backend
- Ajoutez des points d'accès dans
besser/utilities/web_modeling_editor/backend/backend.py. - Ajoutez des modèles de requête/réponse Pydantic dans
backend/models/. - Ajoutez une route de validation sous
/validate-diagramsi applicable.
Étape 5 : Écrire les tests
Placez les tests dans tests/BUML/metamodel/your_dsl/ :
- Tests de construction de modèle valide.
- Tests de modèle invalide (attendez
ValueError,TypeError). - Tests de conversion aller-retour si applicable.
Étape 6 : Documenter
- Documentation du métamodèle :
docs/source/buml_language/your_dsl.rst - Ajoutez à l'index
docs/source/buml_language.rst.
Tests
Exécuter tous les tests
python -m pytest
Exécuter des tests spécifiques
# Par répertoire
python -m pytest tests/generators/sqlalchemy/
# Par mot-clé
python -m pytest -k "test_django"
# Avec sortie verbose
python -m pytest -v tests/BUML/metamodel/structural/
# Arrêt à la première erreur
python -m pytest -x
Conventions de test
- Fichiers de test :
test_*.py - Fonctions de test :
test_* - Utilisez
@pytest.fixturepour les modèles et répertoires réutilisables. - Utilisez
tmp_pathpour les répertoires de sortie (nettoyés automatiquement par pytest). - Affirmez un contenu spécifique, pas seulement l'existence du fichier.
- Testez les conditions d'erreur avec
pytest.raises(ValueError).
Que tester
Pour les modifications du métamodèle : validation de construction, contraintes des setters,
erreurs attendues pour une entrée invalide, résultats de validate().
Pour les générateurs : existence du fichier de sortie, contenu clé dans la sortie (noms de classe, mappings de relations), cas limites (modèle vide, héritage profond, plusieurs-à-plusieurs).
Pour les convertisseurs : fidélité aller-retour (JSON → BUML → JSON doit être équivalent).
Construction de la documentation
cd docs
# Windows :
make.bat html
# macOS/Linux :
make html
# Visualisation :
# Ouvrez docs/build/html/index.html dans votre navigateur
La documentation utilise Sphinx avec le thème Furo. Les fichiers source sont en reStructuredText
(.rst) dans docs/source/.
Structure de la documentation
| Chemin | Contenu |
|---|---|
docs/source/buml_language/ | Documentation du métamodèle |
docs/source/generators/ | Documentation du générateur |
docs/source/web_editor.rst | Documentation de l'API de l'éditeur web |
docs/source/utilities/ | Documentation des utilitaires |
docs/source/contributing/ | Guides du contributeur |
docs/source/releases/ | Notes de version |
docs/source/_static/ | Images et ressources partagées |
docs/source/img/ | Diagrammes et captures d'écran |
Références croisées
Utilisez les rôles Sphinx pour les liens croisés :
:doc:\generators/django`` — lien vers une autre page de documentation:mod:\besser.generators.django`` — lien vers l'API du module:class:\besser.BUML.metamodel.structural.Class`` — lien vers l'API de la classe:ref:\label-name`` — lien vers une section étiquetée
Style de code
- PEP 8 avec indentation de 4 espaces.
- Limite de ligne de 120 caractères (configurée dans
pyproject.tomlsous[tool.pylint.'FORMAT']). - Indications de type pour les API publiques.
- Docstrings PEP 257.
- Ordre des importations : bibliothèque standard, tiers, modules locaux.
snake_casepour fonctions/variables,PascalCasepour classes,UPPER_CASEpour constantes.- Pas d'importations génériques, pas de réexportations implicites.
- Linting : pylint (pas de hooks pre-commit ruff/black/pre-commit actuellement configurés).
Conventions de commit et PR
Messages de commit
Utilisez les commits conventionnels :
feat : ajouter un générateur Terraform pour AWS EKS
fix : corriger l'analyse de multiplicité pour les associations 0..*
refactor : extraire les aides de template partagés vers besser.utilities
docs : documenter l'utilisation de QiskitGenerator
test : ajouter des tests aller-retour pour le convertisseur de modèle GUI
Workflow de Pull Request
- Créez une branche de sujet :
git checkout -b feature/ajouter-mon-generateur. - Faites des commits ciblés et logiquement groupés.
- Exécutez les tests localement :
python -m pytest. - Construisez la doc :
cd docs && make html. - Rebasculez sur le dernier
master:git fetch origin && git rebase origin/master. - Poussez et ouvrez la PR contre
master. - Remplissez le modèle de PR : description, tests exécutés, contexte supplémentaire.
- Deux approbations de responsables sont requises pour la fusion (une approbation suffit après 14 jours).
Changements cross-dépôts (BESSER + Frontend)
Lorsque les changements affectent à la fois BESSER et le frontend de l'éditeur web :
- Implémentez et validez les changements WME dans le dépôt WME.
- Implémentez les changements BESSER dans ce dépôt.
- Mettez à jour le pointeur de sous-module :
cd besser/utilities/web_modeling_editor/frontend git fetch && git checkout <commit> cd ../../../.. git add besser/utilities/web_modeling_editor/frontend - Liez les deux PR afin que les examinateurs les fusionnent dans le bon ordre.
CI / Release
- CI : Il n'y a pas de workflow de test automatisé — les tests sont exécutés localement avant les PR.
- Release : Déclenché par la création d'une version GitHub. Le workflow
python-publish.ymlconstruit et publie sur PyPI en utilisantPYPI_API_TOKEN. - Version : Définie dans
setup.cfgsous[metadata] version. - Notes de version : Ajoutées en fichiers RST dans
docs/source/releases/.
Pièges courants de contribution
- Ne dupliquez pas la logique — les aides partagés vont dans
besser/utilities/, pas dans des générateurs individuels. - Maintenez le déterminisme — les générateurs doivent produire une sortie identique pour une entrée identique.
- Gardez les convertisseurs symétriques — si JSON → BUML prend en charge une fonctionnalité, BUML → JSON doit aussi.
- Mettez à jour la documentation — tout changement dans le backend nécessite probablement des mises à jour dans
docs/source/. - Ne touchez pas au sous-module frontend sauf si explicitement requis. Les changements UI vont dans le dépôt WME amont.
- Nettoyez les ressources — utilisez toujours
try/finallypour les répertoires temporaires et les descripteurs de fichiers. - Testez les allers-retours — en particulier pour les convertisseurs (JSON → BUML → JSON doit être l'identité).