BESSER에 기여하기
이 스킬은 BESSER 코드베이스에 기여하기 위한 절차별 워크플로우를 다룹니다. 아키텍처 세부 사항과 코드 규칙에 대해서는 컨텍스트에 자동으로 로드되는 프로젝트의 CLAUDE.md 파일도 함께 참조하세요.
개발 환경 설정
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 # 문서 빌드용
pip install -e . # 편집 가능한 설치
확인: python tests/BUML/metamodel/structural/library/library.py
Python 3.10+가 필요합니다 (Django 5.x 의존성 때문).
새 생성기 추가하기
이것은 가장 일반적인 기여 유형입니다. 보일러플레이트를 생략하려면 번들로 제공되는 스캐폴드를 실행하세요:
python scripts/scaffold_generator.py <name> <path/to/BESSER/repo>
# 예: python scripts/scaffold_generator.py graphql ~/code/BESSER
이 명령은 즉시 실행 가능한 작업 스텁 패키지(생성기 클래스 + Jinja 템플릿 + pytest)를 생성합니다. 실제 로직을 구현하고, 웹 편집기 설정에 생성기를 등록하고, 문서를 추가해야 합니다 — 아래 단계에서 다룹니다.
수동으로 수행하려면 다음 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단계: 웹 편집기 백엔드에 등록하기
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로 빌드하세요 — 외부 파일에 의존하지 마세요.
- 구조(파일 존재, 클래스 이름 포함)와 내용(비즈니스 로직)을 모두 테스트하세요.
- import 가능한 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단계: 컨버터 추가하기 (웹 편집기 통합이 필요한 경우)
컨버터는 프론트엔드 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은 원본과 동일해야 합니다.
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 | 웹 편집기 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 훅은 현재 구성되지 않음).
커밋 및 PR 규칙
커밋 메시지
Conventional Commits를 사용하세요:
feat: AWS EKS용 Terraform 생성기 추가
fix: 0..* 연관에 대한 다중성 구문 분석 수정
refactor: 공유 템플릿 헬퍼를 besser.utilities로 추출
docs: QiskitGenerator 사용법 문서화
test: GUI 모델 컨버터에 대한 라운드-트립 테스트 추가
Pull Request 워크플로우
- 토픽 브랜치를 만드세요:
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 템플릿을 작성하세요: 설명, 실행된 테스트, 추가 컨텍스트.
- 병합을 위해 메인테이너 승인 2개가 필요합니다 (14일 후에는 승인 1개로 충분).
크로스-레포 변경 (BESSER + 프론트엔드)
변경 사항이 BESSER와 웹 편집기 프론트엔드 모두에 영향을 미치는 경우:
- 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 릴리스를 생성하여 트리거됩니다.
python-publish.yml워크플로우가PYPI_API_TOKEN을 사용하여 PyPI에 빌드하고 게시합니다. - 버전:
setup.cfg의[metadata] version에 정의됩니다. - 릴리스 노트:
docs/source/releases/디렉토리에 RST 파일로 추가됩니다.
일반적인 기여 함정
- 로직을 중복하지 마세요 — 공유 헬퍼는 개별 생성기가 아닌
besser/utilities/에 위치해야 합니다. - 결정성을 유지하세요 — 생성기는 동일한 입력에 대해 동일한 출력을 생성해야 합니다.
- 컨버터를 대칭적으로 유지하세요 — JSON → BUML이 기능을 지원하면 BUML → JSON도 지원해야 합니다.
- 문서를 업데이트하세요 — 백엔드 변경은 대개
docs/source/업데이트가 필요합니다. - 명시적으로 필요하지 않는 한 프론트엔드 서브모듈을 건드리지 마세요 — UI 변경은 업스트림 WME 저장소로 이동합니다.
- 리소스를 정리하세요 — 임시 디렉토리와 파일 핸들에는 항상
try/finally를 사용하세요. - 라운드-트립을 테스트하세요 — 특히 컨버터의 경우 (JSON → BUML → JSON은 항등적이어야 함).