name: besser-dev
description: >
Contributor guide for developing BESSER itself
(https://github.com/BESSER-PEARL/BESSER). Use this skill whenever the user
is working inside the BESSER source tree — adding a new generator (the
most common contribution), adding a new metamodel or sub-DSL under
besser/BUML/metamodel/, writing pytest tests for generators or
metamodels, writing JSON↔BUML converters for the web editor, building
Sphinx documentation under docs/source/, registering a generator in
SUPPORTED_GENERATORS, or preparing a pull request to BESSER. Trigger on
phrases like "add a new generator", "register in the web editor",
"GeneratorInterface", "json_to_buml", "buml_to_json", "write tests for my
generator", "build the docs", "open a PR to BESSER", or any work that
touches besser/generators/, besser/BUML/metamodel/,
besser/utilities/web_modeling_editor/, or tests/. Prefer this skill
over besser-user when the user is contributing to BESSER rather than
using BESSER to build something else.
license: Apache-2.0
compatibility:
- claude-code
- cursor
- cline
- windsurf
- copilot metadata: author: BESSER-PEARL version: "0.2.0" repository: https://github.com/BESSER-PEARL/BESSER-Skills
Contributing to BESSER
This skill covers the procedural workflows for contributing to the BESSER
codebase. For architecture details and code conventions, also consult the
project's CLAUDE.md file which is loaded into context automatically.
Development Setup
git clone https://github.com/<your-username>/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 # for building docs
pip install -e . # editable install
Verify: python tests/BUML/metamodel/structural/library/library.py
Python 3.10+ is required (Django 5.x dependency).
Adding a New Generator
This is the most common type of contribution. To skip the boilerplate, run the bundled scaffold:
python scripts/scaffold_generator.py <name> <path/to/BESSER/repo>
# e.g.: python scripts/scaffold_generator.py graphql ~/code/BESSER
That writes a working stub package (generator class + Jinja template + pytest), runnable immediately. You still need to implement real logic, register the generator in the web-editor config, and add docs — covered in the steps below.
If you prefer to do it by hand, follow these 6 steps:
Step 1: Create the Generator Package
besser/generators/my_generator/
__init__.py # export: from .my_generator import MyGenerator
my_generator.py # the generator class
templates/ # Jinja2 templates
output_template.py.j2
Copy the scaffold from an existing simple generator (e.g., python_classes/).
Step 2: Implement 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):
"""Generate output from the B-UML model."""
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)
Key requirements:
- Call
super().__init__(model, output_dir)— this sets upself.modelandself.output_dir. - Use
self.build_generation_path(filename)to get the correct output file path. - Use
self.build_generation_dir()if you need just the directory path. - Jinja2 templates should be in a
templates/subdirectory alongside the generator. - Output must be deterministic — same model produces same output every time (no timestamps, random values, etc.).
- Reusable helpers go in
besser/utilities/, not in the generator.
Step 3: Write Jinja2 Templates
{# besser/generators/my_generator/templates/output_template.py.j2 #}
# Generated by BESSER MyGenerator
{% for cls in classes %}
class {{ cls.name }}:
{% for attr in cls.attributes %}
{{ attr.name }}: {{ attr.type.name }}
{% endfor %}
{% endfor %}
If generating JSX/React, use non-standard delimiters to avoid conflicts:
env = Environment(
loader=FileSystemLoader(templates_path),
variable_start_string="[[",
variable_end_string="]]",
)
Step 4: Register in the Web Editor Backend
Add your generator to besser/utilities/web_modeling_editor/backend/config/generators.py:
from besser.generators.my_generator import MyGenerator
GENERATORS = {
# ... existing generators ...
"my_generator": GeneratorInfo(
generator_class=MyGenerator,
output_type="file", # "file" for single file, "zip" for multi-file
file_extension=".py", # file extension of the output
category="my_category", # grouping label
requires_class_diagram=True, # True if it needs a DomainModel
),
}
Also update get_filename_for_generator() in the same file if the generator
produces a specific filename.
Step 5: Write Tests
Create 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():
"""Build a small test model."""
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
Test patterns used throughout the codebase:
- Use
tmp_pathortmpdirfixture for output directories (auto-cleaned). - Build small inline models as fixtures — don't depend on external files.
- Test both structure (files exist, class names present) and content (business logic).
- For generators that produce importable Python, use
importlibto dynamically import and test the generated code. - Clean up with
shutil.rmtree()if not using pytest tmp fixtures.
Step 6: Document
Add documentation in docs/source/generators/my_generator.rst:
MyGenerator
===========
The MyGenerator produces ... from a B-UML domain model.
Prerequisites
-------------
- BESSER installed
- ...
Usage
-----
.. code-block:: python
from besser.generators.my_generator import MyGenerator
gen = MyGenerator(model=my_model, output_dir="./output")
gen.generate()
Output
------
The generator produces a single file ``my_output.py`` containing ...
Add it to the generators index in docs/source/generators.rst.
Adding a New Metamodel / Sub-DSL
Step 1: Design the Metamodel
Create new classes in besser/BUML/metamodel/your_dsl/:
besser/BUML/metamodel/your_dsl/
__init__.py
your_dsl.py # metamodel classes
Follow existing patterns:
- Extend
NamedElementorElementas base classes. - Use private properties with getter/setter validation.
- Keep naming consistent with existing metamodel packages.
Step 2: Add Converters (if web editor integration needed)
Converters translate between frontend JSON and B-UML objects:
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
Converters must be symmetric: any feature supported in one direction must work in
the other. Test round-trips: JSON → BUML → JSON should produce the original.
Step 3: Add Validation
If the metamodel has constraints beyond what setters enforce:
def validate(self, raise_exception=True):
errors = []
warnings = []
# Check structural rules
if some_bad_condition:
errors.append("Descriptive error message")
result = {"success": len(errors) == 0, "errors": errors, "warnings": warnings}
if raise_exception and errors:
raise ValueError("\n".join(errors))
return result
Step 4: Wire into Backend
- Add endpoint(s) in
besser/utilities/web_modeling_editor/backend/backend.py. - Add Pydantic request/response models in
backend/models/. - Add validation route under
/validate-diagramif applicable.
Step 5: Write Tests
Place tests in tests/BUML/metamodel/your_dsl/:
- Valid model construction tests.
- Invalid model tests (expect
ValueError,TypeError). - Round-trip converter tests if applicable.
Step 6: Document
- Metamodel docs:
docs/source/buml_language/your_dsl.rst - Add to
docs/source/buml_language.rstindex.
Testing
Run All Tests
python -m pytest
Run Specific Tests
# By directory
python -m pytest tests/generators/sqlalchemy/
# By keyword
python -m pytest -k "test_django"
# With verbose output
python -m pytest -v tests/BUML/metamodel/structural/
# Stop on first failure
python -m pytest -x
Test Conventions
- Test files:
test_*.py - Test functions:
test_* - Use
@pytest.fixturefor reusable models and directories. - Use
tmp_pathfor output directories (auto-cleaned by pytest). - Assert specific content, not just file existence.
- Test error conditions with
pytest.raises(ValueError).
What to Test
For metamodel changes: construction validation, setter constraints, expected
errors for invalid input, validate() results.
For generators: output file existence, key content in output (class names, relationship mappings), edge cases (empty model, deep inheritance, many-to-many).
For converters: round-trip fidelity (JSON → BUML → JSON should be equivalent).
Building Documentation
cd docs
# Windows:
make.bat html
# macOS/Linux:
make html
# View:
# Open docs/build/html/index.html in your browser
Documentation uses Sphinx with the Furo theme. Source files are reStructuredText
(.rst) in docs/source/.
Doc Structure
| Path | Contains |
|---|---|
docs/source/buml_language/ | Metamodel documentation |
docs/source/generators/ | Generator documentation |
docs/source/web_editor.rst | Web editor API docs |
docs/source/utilities/ | Utility documentation |
docs/source/contributing/ | Contributor guides |
docs/source/releases/ | Release notes |
docs/source/_static/ | Shared images and assets |
docs/source/img/ | Diagrams and screenshots |
Cross-References
Use Sphinx roles for cross-linking:
:doc:\generators/django`` — link to another doc page:mod:\besser.generators.django`` — link to module API:class:\besser.BUML.metamodel.structural.Class`` — link to class API:ref:\label-name`` — link to a labeled section
Code Style
- PEP 8 with 4-space indentation.
- 120-character line limit (configured in
pyproject.tomlunder[tool.pylint.'FORMAT']). - Type hints for public APIs.
- PEP 257 docstrings.
- Import order: standard library, third-party, local modules.
snake_casefunctions/variables,PascalCaseclasses,UPPER_CASEconstants.- No wildcard imports, no implicit re-exports.
- Linting: pylint (no ruff/black/pre-commit hooks currently configured).
Commit and PR Conventions
Commit Messages
Use Conventional Commits:
feat: add Terraform generator for AWS EKS
fix: correct multiplicity parsing for 0..* associations
refactor: extract shared template helpers to besser.utilities
docs: document QiskitGenerator usage
test: add round-trip tests for GUI model converter
Pull Request Workflow
- Create a topic branch:
git checkout -b feature/add-my-generator. - Make focused, logically grouped commits.
- Run tests locally:
python -m pytest. - Build docs:
cd docs && make html. - Rebase on latest
master:git fetch origin && git rebase origin/master. - Push and open PR against
master. - Fill in the PR template: description, tests executed, extra context.
- Two maintainer approvals required for merge (one approval enough after 14 days).
Cross-Repo Changes (BESSER + Frontend)
When changes affect both BESSER and the web editor frontend:
- Implement and commit WME changes in the WME repo.
- Implement BESSER changes in this repo.
- Update the submodule pointer:
cd besser/utilities/web_modeling_editor/frontend git fetch && git checkout <commit> cd ../../../.. git add besser/utilities/web_modeling_editor/frontend - Link the two PRs so reviewers merge in correct order.
CI / Release
- CI: There is no automated test workflow — tests run locally before PRs.
- Release: Triggered by creating a GitHub Release. The
python-publish.ymlworkflow builds and publishes to PyPI usingPYPI_API_TOKEN. - Version: Defined in
setup.cfgunder[metadata] version. - Release notes: Added as RST files in
docs/source/releases/.
Common Contribution Pitfalls
- Don't duplicate logic — shared helpers go in
besser/utilities/, not in individual generators. - Maintain determinism — generators must produce identical output for identical input.
- Keep converters symmetric — if JSON → BUML supports a feature, BUML → JSON must too.
- Update docs — any backend change likely needs
docs/source/updates. - Don't touch the frontend submodule unless explicitly required. UI changes go to the upstream WME repo.
- Clean up resources — always use
try/finallyfor temp directories and file handles. - Test round-trips — especially for converters (JSON → BUML → JSON should be identity).