貢獻 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 #}
# Generated by 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 fixtures,請使用
shutil.rmtree()進行清理。
步驟 6:撰寫文件
在 docs/source/generators/my_generator.rst 中新增文件:
MyGenerator
===========
The 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
文件使用 Sphinx 搭配 Furo 主題。原始檔案是 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的常數。 - 無萬用字元匯入,無隱含的重新匯出。
- Linting:pylint(目前未設定 ruff/black/pre-commit hooks)。
提交和 PR 慣例
提交訊息
使用 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 工作流程
- 建立主題分支:
git checkout -b feature/add-my-generator。 - 進行專注、邏輯分組的提交。
- 在本機執行測試:
python -m pytest。 - 建置文件:
cd docs && make html。 - 基於最新的
master進行 rebase: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 應為恆等)。