为BESSER做贡献
本技能涵盖了为BESSER代码库做贡献的程序化工作流程。有关架构细节和代码约定,请同时参考项目自动加载到上下文中的 CLAUDE.md 文件。
开发环境搭建
git clone https://github.com/<你的用户名>/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 # 用于构建文档
pip install -e . # 可编辑安装
验证:python tests/BUML/metamodel/structural/library/library.py
需要 Python 3.10+(Django 5.x 依赖)。
添加新的生成器
这是最常见的贡献类型。要跳过样板代码,请运行内置脚手架:
python scripts/scaffold_generator.py <名称> <BESSER仓库路径>
# 例如:python scripts/scaffold_generator.py graphql ~/code/BESSER
这将写入一个可立即运行的工作存根包(生成器类 + Jinja 模板 + pytest)。您仍然需要实现真正的逻辑、在 Web 编辑器配置中注册生成器并添加文档——这些内容将在下面的步骤中介绍。
如果您更喜欢手动操作,请遵循以下 6 个步骤:
第 1 步:创建生成器包
besser/generators/my_generator/
__init__.py # 导出:from .my_generator import MyGenerator
my_generator.py # 生成器类
templates/ # Jinja2 模板
output_template.py.j2
从现有的简单生成器(例如 python_classes/)复制脚手架。
第 2 步:实现 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):
"""从 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)
关键要求:
- 调用
super().__init__(model, output_dir)—— 这设置了self.model和self.output_dir。 - 使用
self.build_generation_path(filename)获取正确的输出文件路径。 - 如果只需要目录路径,请使用
self.build_generation_dir()。 - Jinja2 模板应位于生成器旁边的
templates/子目录中。 - 输出必须是确定性的 —— 相同的模型每次都产生相同的输出(没有时间戳、随机值等)。
- 可重用的辅助函数放在
besser/utilities/中,而不是生成器中。
第 3 步:编写 Jinja2 模板
{# besser/generators/my_generator/templates/output_template.py.j2 #}
# 由 BESSER MyGenerator 生成
{% for cls in classes %}
class {{ cls.name }}:
{% for attr in cls.attributes %}
{{ attr.name }}: {{ attr.type.name }}
{% endfor %}
{% endfor %}
如果生成 JSX/React,请使用非标准分隔符以避免冲突:
env = Environment(
loader=FileSystemLoader(templates_path),
variable_start_string="[[",
variable_end_string="]]",
)
第 4 步:在 Web 编辑器后端注册
将您的生成器添加到 besser/utilities/web_modeling_editor/backend/config/generators.py:
from besser.generators.my_generator import MyGenerator
GENERATORS = {
# ... 现有生成器 ...
"my_generator": GeneratorInfo(
generator_class=MyGenerator,
output_type="file", # "file" 表示单个文件,"zip" 表示多个文件
file_extension=".py", # 输出的文件扩展名
category="my_category", # 分组标签
requires_class_diagram=True, # 如果需要 DomainModel 则为 True
),
}
如果生成器产生特定的文件名,也需更新同一文件中的 get_filename_for_generator()。
第 5 步:编写测试
创建 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():
"""构建一个小型测试模型。"""
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
整个代码库中使用的测试模式:
- 使用
tmp_path或tmpdirfixture 作为输出目录(自动清理)。 - 构建小型内联模型作为fixture —— 不要依赖外部文件。
- 测试结构和内容(文件存在,类名存在)和业务逻辑。
- 对于生成可导入 Python 代码的生成器,使用
importlib动态导入并测试生成的代码。 - 如果不使用 pytest 的 tmp fixture,使用
shutil.rmtree()进行清理。
第 6 步:文档
在 docs/source/generators/my_generator.rst 中添加文档:
MyGenerator
===========
MyGenerator 从 B-UML 领域模型生成……
前提条件
------------
- 已安装 BESSER
- …
用法
-----
.. code-block:: python
from besser.generators.my_generator import MyGenerator
gen = MyGenerator(model=my_model, output_dir="./output")
gen.generate()
输出
------
生成器生成一个包含……的单个文件 ``my_output.py``。
将其添加到 docs/source/generators.rst 的生成器索引中。
添加新的元模型 / 子 DSL
第 1 步:设计元模型
在 besser/BUML/metamodel/your_dsl/ 中创建新类:
besser/BUML/metamodel/your_dsl/
__init__.py
your_dsl.py # 元模型类
遵循现有模式:
- 扩展
NamedElement或Element作为基类。 - 使用带有 getter/setter 验证的私有属性。
- 保持命名与现有元模型包一致。
第 2 步:添加转换器(如果需要 Web 编辑器集成)
转换器在前端 JSON 和 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
转换器必须是对称的:在一个方向支持的任何功能都必须在另一个方向正常工作。测试往返:JSON → BUML → JSON 应产生原始 JSON。
第 3 步:添加验证
如果元模型具有超出 setter 强制执行的约束:
def validate(self, raise_exception=True):
errors = []
warnings = []
# 检查结构规则
if some_bad_condition:
errors.append("描述性错误消息")
result = {"success": len(errors) == 0, "errors": errors, "warnings": warnings}
if raise_exception and errors:
raise ValueError("\n".join(errors))
return result
第 4 步:接入后端
- 在
besser/utilities/web_modeling_editor/backend/backend.py中添加端点。 - 在
backend/models/中添加 Pydantic 请求/响应模型。 - 如果适用,在
/validate-diagram下添加验证路由。
第 5 步:编写测试
将测试放在 tests/BUML/metamodel/your_dsl/ 中:
- 有效模型构建测试。
- 无效模型测试(期望
ValueError、TypeError)。 - 如果适用,进行往返转换器测试。
第 6 步:文档
- 元模型文档:
docs/source/buml_language/your_dsl.rst - 添加到
docs/source/buml_language.rst索引。
测试
运行所有测试
python -m pytest
运行特定测试
# 按目录
python -m pytest tests/generators/sqlalchemy/
# 按关键字
python -m pytest -k "test_django"
# 带详细输出
python -m pytest -v tests/BUML/metamodel/structural/
# 在第一次失败时停止
python -m pytest -x
测试约定
- 测试文件:
test_*.py - 测试函数:
test_* - 使用
@pytest.fixture处理可重用的模型和目录。 - 使用
tmp_path作为输出目录(由 pytest 自动清理)。 - 断言特定内容,而不仅仅是文件存在。
- 使用
pytest.raises(ValueError)测试错误条件。
测试什么
对于元模型更改:构造验证、setter 约束、对无效输入的预期错误、validate() 结果。
对于生成器:输出文件存在、输出中的关键内容(类名、关系映射)、边界情况(空模型、深度继承、多对多)。
对于转换器:往返保真度(JSON → BUML → JSON 应等效)。
构建文档
cd docs
# Windows:
make.bat html
# macOS/Linux:
make html
# 查看:
# 在浏览器中打开 docs/build/html/index.html
文档使用带有 Furo 主题的 Sphinx。源文件是位于 docs/source/ 中的 reStructuredText (.rst) 文件。
文档结构
| 路径 | 内容 |
|---|---|
docs/source/buml_language/ | 元模型文档 |
docs/source/generators/ | 生成器文档 |
docs/source/web_editor.rst | Web 编辑器 API 文档 |
docs/source/utilities/ | 实用程序文档 |
docs/source/contributing/ | 贡献者指南 |
docs/source/releases/ | 发布说明 |
docs/source/_static/ | 共享图片和资源 |
docs/source/img/ | 图表和截图 |
交叉引用
使用 Sphinx 角色进行交叉链接:
:doc:\generators/django`` — 链接到另一个文档页面:mod:\besser.generators.django`` — 链接到模块 API:class:\besser.BUML.metamodel.structural.Class`` — 链接到类 API:ref:\label-name`` — 链接到带标签的部分
代码风格
- PEP 8,4 空格缩进。
- 120 字符行限制(在
pyproject.toml的[tool.pylint.'FORMAT']下配置)。 - 公共 API 的类型提示。
- PEP 257 文档字符串。
- 导入顺序:标准库,第三方,本地模块。
snake_case函数/变量,PascalCase类,UPPER_CASE常量。- 不允许通配符导入,不允许隐式重新导出。
- 代码检查:pylint(当前未配置 ruff/black/pre-commit hooks)。
提交和 PR 约定
提交消息
使用常规提交:
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
拉取请求工作流
- 创建主题分支:
git checkout -b feature/add-my-generator。 - 进行重点明确、逻辑分组的提交。
- 在本地运行测试:
python -m pytest。 - 构建文档:
cd docs && make html。 - 变基到最新的
master:git fetch origin && git rebase origin/master。 - 推送并针对
master打开 PR。 - 填写 PR 模板:描述、执行的测试、额外情况。
- 合并需要两位维护者批准(14 天后一个批准即可)。
跨仓库更改(BESSER + 前端)
当更改同时影响 BESSER 和 Web 编辑器前端时:
- 在 WME 仓库 中实现并提交 WME 更改。
- 在此仓库中实现 BESSER 更改。
- 更新子模块指针:
cd besser/utilities/web_modeling_editor/frontend git fetch && git checkout <commit> cd ../../../.. git add besser/utilities/web_modeling_editor/frontend - 链接两个 PR,以便审阅者按正确顺序合并。
CI / 发布
- CI:没有自动化测试工作流 —— 测试在 PR 之前本地运行。
- 发布:通过创建 GitHub Release 触发。
python-publish.yml工作流使用PYPI_API_TOKEN构建并发布到 PyPI。 - 版本:在
setup.cfg的[metadata] version下定义。 - 发布说明:作为 RST 文件添加到
docs/source/releases/。
常见贡献陷阱
- 不要重复逻辑 —— 共享辅助函数放在
besser/utilities/中,而不是单个生成器中。 - 保持确定性 —— 生成器必须为相同输入生成相同输出。
- 保持转换器对称 —— 如果 JSON → BUML 支持某个功能,BUML → JSON 也必须支持。
- 更新文档 —— 任何后端更改可能都需要更新
docs/source/。 - 除非明确要求,否则不要接触前端子模块。UI 更改应转到上游 WME 仓库。
- 清理资源 —— 始终对临时目录和文件句柄使用
try/finally。 - 测试往返 —— 特别是对于转换器(JSON → BUML → JSON 应为恒等)。