Files
BSN-Chatsystem/CODING_STANDARDS.md

694 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# BSN Coding Standards — Das verbindliche Regelwerk
> **Version:** 1.0 — 21. Juni 2026
> **Autor:** Cody (Coding-Agent für Daniel Krause, brettspiel-news.de)
> **Verbindlichkeit:** Diese Regeln werden durch automatisierte Tools (pre-commit, Ruff, mypy) durchgesetzt. Kein Merge ohne bestandene Checks.
---
## Inhaltsverzeichnis
1. [Allgemeine Prinzipien](#1-allgemeine-prinzipien)
2. [Code-Stil](#2-code-stil)
3. [Typ-Annotationen](#3-typ-annotationen)
4. [Namenskonventionen](#4-namenskonventionen)
5. [Kommentare & Docstrings](#5-kommentare--docstrings)
6. [Projektstruktur & Architektur](#6-projektstruktur--architektur)
7. [Sicherheit](#7-sicherheit)
8. [Testing](#8-testing)
9. [Flask-spezifische Regeln](#9-flask-spezifische-regeln)
10. [Git & Commits](#10-git--commits)
11. [Toolchain & Durchsetzung](#11-toolchain--durchsetzung)
---
## 1. Allgemeine Prinzipien
### 1.1 Das Zen of Python (PEP 20)
```python
import this
```
Die wichtigsten Prinzipien für dieses Projekt:
| Prinzip | Bedeutung |
|---|---|
| **Readability counts.** | Code wird 10× öfter gelesen als geschrieben. Schreibe für den Leser. |
| **Explicit is better than implicit.** | Kein Magic. Kein `**kwargs`-Missbrauch. Typen immer sichtbar. |
| **Simple is better than complex.** | YAGNI — You Ain't Gonna Need It. Keine überflüssigen Abstraktionen. |
| **Errors should never pass silently.** | Immer `try/except` mit spezifischen Exceptions + Logging. |
| **There should be one obvious way to do it.** | Einheitlicher Stil im gesamten Projekt. |
### 1.2 Konsistenz-Regel (PEP 8)
> **Konsistenz mit diesem Styleguide ist wichtig. Konsistenz innerhalb des Projekts ist wichtiger. Konsistenz innerhalb eines Moduls ist am wichtigsten.**
Wenn du eine Regel brechen musst, dokumentiere WARUM in einem Kommentar.
---
## 2. Code-Stil
**Durchsetzung:** Ruff (Linter + Formatter) in `pyproject.toml`
**Basis:** PEP 8 mit projektspezifischen Anpassungen
### 2.1 Zeilenlänge
```
Maximal 100 Zeichen pro Zeile.
```
**Begründung:** 88 (Black-Default) ist zu knapp für Flask-Routen und SQLAlchemy-Queries. 100 Zeichen erlauben lesbare Code-Blöcke ohne horizontales Scrollen.
### 2.2 Einrückung
- **4 Leerzeichen** pro Ebene — **NIE Tabs**
- Fortsetzungszeilen: 8 Leerzeichen (doppelte Einrückung) oder vertikale Ausrichtung
```python
# ✅ Richtig — vertikale Ausrichtung
result = some_function(
argument_one,
argument_two,
argument_three,
)
# ✅ Richtig — doppelte Einrückung bei langen Bedingungen
if (some_very_long_condition
and another_long_condition):
do_something()
# ❌ Falsch — nur 4 Leerzeichen (nicht unterscheidbar)
if (some_very_long_condition
and another_long_condition):
do_something()
```
### 2.3 Quotes
- **Doppelte Anführungszeichen** (`"`) für Strings — Black/Ruff-Standard
- Einfache Anführungszeichen nur, wenn der String selbst `"` enthält
- Docstrings: **immer** `"""triple double quotes"""` (PEP 257)
```python
# ✅ Richtig
name = "Brettspiel-News"
message = f"Willkommen bei {name}!"
# ✅ Richtig — enthält doppelte Quotes
quote = 'Er sagte: "Das ist ein gutes Spiel!"'
# ✅ Richtig
def get_games() -> list[dict]:
"""Holt alle Spiele aus der Datenbank."""
...
```
### 2.4 Imports
**Reihenfolge** (automatisch durch Ruff/isort):
1. Standard-Library (`os`, `json`, `datetime`)
2. Drittanbieter (`flask`, `sqlalchemy`, `requests`)
3. Projekt-interne (`from .models import ...`)
```python
# ✅ Richtig
import os
import json
from datetime import datetime, timedelta
from flask import Flask, request, jsonify
from sqlalchemy import create_engine
from .models import Game, User
from .utils import validate_email
```
**Verboten:**
- `from module import *` — verschmutzt den Namespace
- Zirkuläre Imports — deuten auf Architekturprobleme hin
### 2.5 Leerzeichen
| Regel | Beispiel |
|---|---|
| **Nach Komma** ein Leerzeichen | `[1, 2, 3]` nicht `[1,2,3]` |
| **Um Operatoren** ein Leerzeichen | `x = 1 + 2` nicht `x=1+2` |
| **Kein Leerzeichen** vor `(` bei Funktion | `def foo():` nicht `def foo ():` |
| **Kein Leerzeichen** vor `:` bei Slice | `list[1:3]` nicht `list[1 : 3]` |
| **Kein Leerzeichen** vor `=` bei Keyword-Arg | `foo(x=1)` nicht `foo(x = 1)` |
### 2.6 Leerzeilen
- 2 Leerzeilen vor Top-Level-Funktionen und -Klassen
- 1 Leerzeile vor Methoden innerhalb einer Klasse
- 1 Leerzeile zwischen logischen Blöcken innerhalb einer Funktion
### 2.7 Trailing Commas
- **Immer** trailing comma bei mehrzeiligen Listen/Dicts/Tupeln
- Erleichtert Diffs und verhindert Fehler beim Hinzufügen von Elementen
```python
# ✅ Richtig
items = [
"Brettspiel",
"Kartenspiel",
"Würfelspiel",
]
config = {
"host": "localhost",
"port": 5432,
"debug": False,
}
```
---
## 3. Typ-Annotationen
**Durchsetzung:** mypy in `strict`-Mode
### 3.1 Grundregel
**Jede Funktion und Methode MUSS vollständige Typ-Annotationen haben.**
```python
# ✅ Richtig
def get_game_by_id(game_id: int) -> dict[str, str | int] | None:
"""Holt ein Spiel anhand seiner ID."""
...
# ❌ Falsch — keine Typen
def get_game_by_id(game_id):
...
```
### 3.2 Spezifische Regeln
| Regel | Richtig | Falsch |
|---|---|---|
| `X \| None` (PEP 604, idiomatisch für 3.13+) | `str \| None` | `Optional[str]` |
| `list`/`dict` statt `List`/`Dict` (Python 3.9+) | `list[str]` | `List[str]` |
| Generics immer parametrisieren | `dict[str, Any]` | `dict` (implizites `dict[Any, Any]`) |
| `-> None` bei Funktionen ohne Return | `def log(msg: str) -> None:` | `def log(msg: str):` |
```python
# ✅ Richtig
def find_user(email: str) -> dict[str, object] | None:
"""Sucht einen User per E-Mail. Gibt None zurück, wenn nicht gefunden."""
...
# ✅ Richtig — Flask-Route
@app.route("/api/games/<int:game_id>")
def get_game(game_id: int) -> tuple[dict[str, object], int]:
"""Gibt ein einzelnes Spiel zurück."""
...
```
---
## 4. Namenskonventionen
### 4.1 Übersicht
| Element | Konvention | Beispiel |
|---|---|---|
| **Module/Packages** | `snake_case` | `game_service.py`, `user_models.py` |
| **Klassen** | `PascalCase` | `GameRepository`, `UserService` |
| **Funktionen/Methoden** | `snake_case` | `get_game_by_id()`, `calculate_score()` |
| **Variablen** | `snake_case` | `game_count`, `user_email` |
| **Konstanten** | `UPPER_SNAKE_CASE` | `MAX_RESULTS = 50`, `DEFAULT_TIMEOUT = 30` |
| **Private (intern)** | `_leading_underscore` | `def _parse_response():`, `self._cache` |
| **"Private" (Name Mangling)** | `__double_underscore` | `self.__db_connection` (nur bei Vererbungskonflikten) |
### 4.2 Spezifische Regeln
- **Boolesche Variablen** mit `is_`, `has_`, `should_`-Präfix: `is_active`, `has_permission`, `should_retry`
- **Funktionen** als Verben: `get_`, `create_`, `update_`, `delete_`, `calculate_`, `validate_`
- **Keine Ein-Buchstaben-Variablen** außer in trivialen Comprehensions (`[x for x in items]`)
- **Keine kryptischen Abkürzungen**: `user_count` statt `uc`, `response` statt `resp`
---
## 5. Kommentare & Docstrings
### 5.1 Sprache
**Alle Kommentare und Docstrings werden auf DEUTSCH geschrieben.**
```python
# ✅ Richtig
def berechne_durchschnitt(werteliste: list[float]) -> float:
"""Berechnet den arithmetischen Durchschnitt einer Werteliste.
Args:
werteliste: Liste von Gleitkommazahlen.
Returns:
Der Durchschnittswert.
Raises:
ValueError: Wenn die Liste leer ist.
"""
if not werteliste:
raise ValueError("Die Werteliste darf nicht leer sein.")
return sum(werteliste) / len(werteliste)
```
### 5.2 Docstring-Format
**Google-Style Docstrings** (maschinenlesbar, von den meisten Tools unterstützt):
```python
def komplexe_funktion(
name: str,
werte: list[int],
optional: str | None = None,
) -> dict[str, object]:
"""Kurzbeschreibung (eine Zeile).
Ausführliche Beschreibung. Kann mehrere Sätze umfassen.
Beschreibt das WARUM, nicht das WAS.
Args:
name: Beschreibung des Parameters.
werte: Liste von Werten, die verarbeitet werden.
optional: Optionaler Zusatzparameter.
Returns:
Ein Dictionary mit den verarbeiteten Ergebnissen.
Raises:
ValueError: Wenn name leer ist.
TypeError: Wenn werte keine Liste von ints ist.
Example:
>>> komplexe_funktion("Test", [1, 2, 3])
{"name": "Test", "summe": 6}
"""
```
### 5.3 Wann kommentieren?
- **Immer:** Docstrings für alle öffentlichen Funktionen/Klassen
- **Oft:** `# TODO:`, `# FIXME:`, `# HACK:` mit Erklärung
- **Manchmal:** Erklärung von nicht-offensichtlichem Code (Algorithmus, Workaround)
- **Nie:** Code auskommentieren — bei Bedarf durch Git-History wiederherstellen
```python
# ✅ Richtig — erklärt WARUM der Workaround
# Workaround: SQLAlchemy 2.0 deprecated `query` — bis Migration nutzen wir `session.execute()`
result = session.execute(select(Game).where(Game.id == game_id))
# ❌ Falsch — erklärt WAS (offensichtlich)
# Holt das Spiel mit der ID
result = session.execute(select(Game).where(Game.id == game_id))
```
---
## 6. Projektstruktur & Architektur
### 6.1 Maximalgrößen
| Einheit | Maximal |
|---|---|
| **Modul (.py-Datei)** | 500 Zeilen |
| **Funktion/Methode** | 50 Zeilen |
| **Klasse** | 300 Zeilen |
| **Zeile** | 100 Zeichen |
**Begründung:** Kleine, fokussierte Einheiten sind leichter zu testen, zu verstehen und zu warten.
### 6.2 Separation of Concerns
```
project/
├── app.py # Flask-App-Factory + Config
├── routes/ # Nur Request-Handling, keine Business-Logik
│ ├── __init__.py
│ ├── games.py # /api/games/*
│ └── users.py # /api/users/*
├── services/ # Business-Logik
│ ├── __init__.py
│ └── game_service.py
├── models/ # Datenbank-Modelle
│ ├── __init__.py
│ └── game.py
├── utils/ # Hilfsfunktionen
│ ├── __init__.py
│ └── validators.py
├── templates/ # Jinja2-Templates
├── static/ # CSS, JS, Bilder
├── tests/ # Tests (spiegelt src/-Struktur)
│ ├── test_routes/
│ ├── test_services/
│ └── conftest.py
├── migrations/ # DB-Migrationen (Alembic)
├── pyproject.toml # ALLE Projekt-Konfiguration
├── .pre-commit-config.yaml
├── README.md
└── CODING_STANDARDS.md
```
### 6.3 Prinzipien
- **Keine God-Files:** `app.py` darf nicht 6000 Zeilen haben. Auslagern!
- **Routes sind dumm:** Nur Request-Parsing → Service aufrufen → Response formatieren
- **Services enthalten Logik:** Validierung, Berechnung, DB-Zugriffe
- **Models nur Schema:** Tabellen-Definition, keine Business-Logik
- **Utils sind pure functions:** Kein Zustand, keine Seiteneffekte, keine DB-Zugriffe
```python
# ✅ Richtig — Route delegiert an Service
@app.route("/api/games/<int:game_id>")
def get_game(game_id: int) -> tuple[dict, int]:
"""Gibt ein einzelnes Spiel zurück."""
game = game_service.get_by_id(game_id)
if not game:
return {"error": "Spiel nicht gefunden"}, 404
return game.to_dict(), 200
# ❌ Falsch — Route enthält Business-Logik
@app.route("/api/games/<int:game_id>")
def get_game(game_id):
game = db.session.execute(
select(Game).where(Game.id == game_id)
).scalar_one_or_none()
if not game:
return {"error": "Not found"}, 404
# 50 Zeilen Berechnungen hier...
```
---
## 7. Sicherheit
**Durchsetzung:** Ruff (Sicherheits-Regeln) + Bandit
### 7.1 Secrets
**NIEMALS Secrets im Code!** Immer über Environment Variables oder `.env`-Datei.
```python
# ✅ Richtig
import os
API_KEY = os.environ["BSN_API_KEY"]
# ❌ Falsch
API_KEY = "sk-abc123def456" # SOFORT LÖSCHEN + Key rotieren!
```
**`.env` und `.db`-Dateien MÜSSEN in `.gitignore` stehen.**
### 7.2 Input-Validierung
- **Alle User-Inputs validieren**, bevor sie verarbeitet werden
- Flask-Werkzeuge nutzen: `request.args.get()`, `request.json.get()` mit Defaults
- SQL-Injection verhindern: Parameterisierte Queries (SQLAlchemy macht das automatisch)
```python
# ✅ Richtig — validiert und sanitized
from flask import request, abort
@app.route("/api/search")
def search_games() -> dict:
query = request.args.get("q", "").strip()
if not query or len(query) > 100:
abort(400, description="Ungültiger Suchbegriff")
return game_service.search(query)
# ❌ Falsch — keine Validierung, roher Input in Query
@app.route("/api/search")
def search_games():
query = request.args.get("q")
db.session.execute(f"SELECT * FROM games WHERE name LIKE '%{query}%'")
```
### 7.3 Weitere Regeln
- **Keine assert-Statements** in Produktionscode (werden mit `-O` deaktiviert)
- **Keine `pickle`** für ungetrustete Daten
- **`subprocess` mit `shell=False`** (Default)
- **Hash-Funktionen für Passwörter:** `bcrypt` oder `argon2`, nie `md5`/`sha1`
---
## 8. Testing
**Durchsetzung:** pytest mit Coverage-Check
### 8.1 Grundregeln
1. **Jede neue Funktion → Test** (Empfehlung: TDD — erst Test, dann Code)
2. **80% Code Coverage** als harter Floor — unterschreiten blockiert den Commit
3. **pytest** als einziges Test-Framework
4. **Tests in `tests/`**, spiegelt die Projektstruktur
### 8.2 Test-Struktur
```python
# tests/test_services/test_game_service.py
import pytest
from services.game_service import GameService
class TestGameService:
"""Tests für den GameService."""
def test_get_by_id_returns_game_when_found(self, db_session):
"""Gibt ein Spiel zurück, wenn die ID existiert."""
# Arrange
service = GameService(db_session)
# Act
result = service.get_by_id(1)
# Assert
assert result is not None
assert result.name == "Catan"
def test_get_by_id_returns_none_when_missing(self, db_session):
"""Gibt None zurück, wenn die ID nicht existiert."""
service = GameService(db_session)
result = service.get_by_id(999999)
assert result is None
def test_get_by_id_raises_on_negative_id(self, db_session):
"""Wirft ValueError bei negativer ID."""
service = GameService(db_session)
with pytest.raises(ValueError, match="ID darf nicht negativ sein"):
service.get_by_id(-1)
```
### 8.3 Test-Namen
- **`test_<was>_<erwartetes_ergebnis>`**
- Deutsch oder Englisch — aber **einheitlich** im gesamten Projekt
---
## 9. Flask-spezifische Regeln
### 9.1 Routes
```python
# ✅ Immer `methods` explizit angeben
@app.route("/api/games", methods=["GET"])
def list_games() -> tuple[dict, int]:
...
# ✅ POST-Daten aus request.get_json(), nicht request.args
@app.route("/api/games", methods=["POST"])
def create_game() -> tuple[dict, int]:
data = request.get_json(silent=True)
if not data:
return {"error": "Kein JSON-Body"}, 400
...
# ❌ Falsch — GET-Parameter in POST lesen
data = request.args.get("name") # NIEMALS für POST-Daten
```
### 9.2 Response-Format
- Immer **JSON** zurückgeben: `flask.jsonify()` oder `dict` mit Statuscode
- Statuscodes **explizit** setzen
- Fehler-Responses einheitlich: `{"error": "Nachricht"}` mit passendem Statuscode
```python
# ✅ Richtig
@app.route("/api/games/<int:game_id>")
def get_game(game_id: int) -> tuple[dict, int]:
game = game_service.get_by_id(game_id)
if not game:
return {"error": "Spiel nicht gefunden"}, 404
return game.to_dict(), 200
```
### 9.3 Config
- Flask-Config über `app.config.from_prefixed_env()` (Flask 3.1+)
- Secrets nie in `app.config` hartkodieren
```python
# ✅ Richtig — Config aus Environment
app = Flask(__name__)
app.config.from_prefixed_env()
# Zugriff: os.environ["FLASK_SECRET_KEY"], os.environ["FLASK_DATABASE_URL"]
```
### 9.4 Templates
- CSS **immer inline** in JCE-Templates (self-contained, keine separaten CSS-Dateien)
- Mobile-first Design
- BSN-Blau `#20228a` als Primärfarbe
- Touch-Buttons min 64px, Overlays min 85vh
---
## 10. Git & Commits
### 10.1 Conventional Commits
Jeder Commit folgt dem **Conventional Commits**-Standard:
```
<type>(<scope>): <description>
[optional body]
[optional footer]
```
| Typ | Verwendung |
|---|---|
| `feat` | Neues Feature |
| `fix` | Bugfix |
| `docs` | Dokumentation |
| `style` | Formatierung (kein Code-Change) |
| `refactor` | Code-Umbau (kein Feature, kein Fix) |
| `test` | Tests hinzugefügt/geändert |
| `chore` | Build, Dependencies, CI |
| `perf` | Performance-Verbesserung |
| `security` | Sicherheitsrelevante Änderung |
**Beispiele:**
```
feat(games): Spiele-Suche mit Volltext-Filter
fix(auth): Login bricht bei leeren Feldern nicht mehr ab
docs: CODING_STANDARDS.md hinzugefügt
chore(deps): Ruff auf 0.15.18 aktualisiert
```
### 10.2 Commit-Frequenz
- **Nach jeder abgeschlossenen Aufgabe** committen
- Keine Mega-Commits mit 50 geänderten Dateien
- Ein Commit = eine logische Änderung
### 10.3 Vor jedem Commit
```bash
pre-commit run # Linting + Formatierung + Typ-Check
pytest # Tests müssen grün sein
```
---
## 11. Toolchain & Durchsetzung
### 11.1 Übersicht
| Tool | Zweck | Konfiguration |
|---|---|---|
| **[Ruff](https://docs.astral.sh/ruff/)** | Linting + Formatierung + Sicherheit | `pyproject.toml``[tool.ruff]` |
| **[mypy](https://mypy-lang.org/)** | Statische Typ-Prüfung | `pyproject.toml``[tool.mypy]` |
| **[pre-commit](https://pre-commit.com/)** | Git-Hooks zur Durchsetzung | `.pre-commit-config.yaml` |
| **[pytest](https://pytest.org/)** | Testing + Coverage | `pyproject.toml``[tool.pytest.ini_options]` |
### 11.2 Quick-Start (neues Projekt)
```bash
# 1. Tools installieren
pip install ruff mypy pre-commit pytest
# 2. Pre-commit-Hooks aktivieren
pre-commit install
# 3. Erstmalig alles prüfen
ruff check . --fix
ruff format .
mypy .
pytest --cov --cov-fail-under=80
# 4. Vor jedem Commit automatisch (Ruff Lint + Format + Basic Checks)
git commit -m "feat: ..."
# → Ruff prüft + formatiert automatisch
# → Merge-Konflikte, Whitespace, Debug-Statements werden geprüft
# → mypy läuft nicht in pre-commit (isoliertes venv-Drift) — separat ausführen!
```
### 11.3 CI-Setup (später)
```yaml
# .github/workflows/quality.yml
- name: Lint + Security
run: ruff check .
- name: Type Check (im echten venv!)
run: mypy .
- name: Tests + Coverage
run: pytest --cov --cov-fail-under=80
```
---
## Anhang A: Checkliste für Code Reviews
Bei jedem Code-Review durchgehen. 🤖 = automatisch von pre-commit erzwungen.
- [ ] 🤖 PEP 8 eingehalten? (Ruff-Check bestanden)
- [ ] Alle Funktionen haben Typ-Annotationen? (mypy bestanden)
- [ ] Docstrings auf Deutsch für alle öffentlichen Funktionen?
- [ ] Keine Secrets im Code?
- [ ] User-Input validiert?
- [ ] Tests geschrieben und grün? Coverage ≥ 80%?
- [ ] Keine God-Files (>500 Zeilen)? (manuell)
- [ ] Keine God-Funktionen (>50 Statements)? (🤖 Ruff PLR0915)
- [ ] McCabe-Komplexität ≤ 10? (🤖 Ruff C901)
- [ ] Route enthält keine Business-Logik?
- [ ] Commit folgt Conventional Commits?
---
## Anhang B: Warum diese Regeln?
> *"Die Basis muss stimmen — wie bei einem Haus, das Fundament ist das wichtigste."* — Daniel Krause
Diese Regeln sind nicht willkürlich. Sie basieren auf:
1. **PEP 8** — dem offiziellen Python-Styleguide (seit 2001)
2. **Google Python Style Guide** — bewährt in tausenden Projekten
3. **The Zen of Python** — Guido van Rossums Design-Prinzipien
4. **Erfahrungen aus dem BSN-Chatbot-Projekt** — was funktioniert hat, was nicht
5. **Moderner Tooling-Konsens 2026** — Ruff + mypy + pre-commit sind Industriestandard
Jede Regel ist **automatisiert durchsetzbar**. Das bedeutet: Du musst sie nicht im Kopf behalten — die Tools sagen dir, wenn etwas falsch ist. Und sie verhindern, dass fehlerhafter Code überhaupt ins Repository kommt.
---
> **Letzte Aktualisierung:** 21. Juni 2026 (extern reviewed ✅ — 6 Fixes eingearbeitet)
> **Nächste Überprüfung:** Bei Projektstart oder Dezember 2026
>
> **Review-Fixes v1.0:**
> 1. Optional[X] → X | None (PEP 604, kein Ruff-Konflikt mehr)
> 2. B101 global entfernt — asserts nur in tests/ erlaubt
> 3. Bandit-Hook gestrichen — Ruffs S-Regeln reichen
> 4. C901 + PLR-Regeln aktiviert — God-Funktionen jetzt automatisch geprüft
> 5. mypy aus pre-commit entfernt (isoliertes venv-Drift)
> 6. TDD → Empfehlung, Typ-Fixes, Ruff 0.15.18, mypy 2.1.0