docs: Coding Standards Fundament — Regelwerk + Implementierungsplan

This commit is contained in:
Hermes Agent
2026-06-21 21:36:17 +02:00
parent 20a7f5987a
commit b1a74b7d02
2 changed files with 767 additions and 0 deletions
+687
View File
@@ -0,0 +1,687 @@
# 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 |
|---|---|---|
| `Optional` statt `X \| None` (Konsistenz) | `Optional[str]` | `str \| None` |
| `list`/`dict` statt `List`/`Dict` (Python 3.9+) | `list[str]` | `List[str]` |
| `Any` nur in Ausnahmefällen | `dict[str, Any]` (API-Response) | `Any` als Rückgabetyp |
| `-> None` bei Funktionen ohne Return | `def log(msg: str) -> None:` | `def log(msg: str):` |
```python
from typing import Optional, Any
# ✅ Richtig
def find_user(email: str) -> Optional[dict[str, Any]]:
"""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, 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: Optional[str] = None,
) -> dict[str, Any]:
"""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** (TDD: erst Test, dann Code)
2. **80% Code Coverage** als Minimum
3. **pytest** als 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.11.3 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 | `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 | `pyproject.toml``[tool.pytest.ini_options]` |
| **[Bandit](https://bandit.readthedocs.io/)** | Sicherheits-Scans | `pyproject.toml``[tool.bandit]` |
### 11.2 Quick-Start (neues Projekt)
```bash
# 1. Tools installieren
pip install ruff mypy pre-commit pytest bandit
# 2. Pre-commit-Hooks aktivieren
pre-commit install
# 3. Erstmalig alles prüfen
ruff check . --fix
ruff format .
mypy .
pytest
# 4. Vor jedem Commit automatisch
git commit -m "feat: ..."
# → Ruff prüft + formatiert automatisch
# → mypy prüft die Typen
# → Bandit scannt auf Sicherheitslücken
```
### 11.3 CI-Setup (später)
```yaml
# .github/workflows/quality.yml
- name: Lint
run: ruff check .
- name: Type Check
run: mypy .
- name: Security Scan
run: bandit -r src/ -c pyproject.toml
- name: Tests
run: pytest --cov --cov-fail-under=80
```
---
## Anhang A: Checkliste für Code Reviews
- [ ] 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?
- [ ] Keine God-Files (>500 Zeilen)?
- [ ] Keine God-Funktionen (>50 Zeilen)?
- [ ] 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
> **Nächste Überprüfung:** Bei Projektstart oder nach 6 Monaten