Files
BSN-Chatsystem/CODING_STANDARDS.md

20 KiB
Raw Permalink Blame History

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
  2. Code-Stil
  3. Typ-Annotationen
  4. Namenskonventionen
  5. Kommentare & Docstrings
  6. Projektstruktur & Architektur
  7. Sicherheit
  8. Testing
  9. Flask-spezifische Regeln
  10. Git & Commits
  11. Toolchain & Durchsetzung

1. Allgemeine Prinzipien

1.1 Das Zen of Python (PEP 20)

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
# ✅ 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)
# ✅ 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 ...)
# ✅ 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
# ✅ 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.

# ✅ 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):
# ✅ 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.

# ✅ 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):

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
# ✅ 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
# ✅ 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.

# ✅ 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)
# ✅ 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

# 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

# ✅ 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
# ✅ 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
# ✅ 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

pre-commit run          # Linting + Formatierung + Typ-Check
pytest                  # Tests müssen grün sein

11. Toolchain & Durchsetzung

11.1 Übersicht

Tool Zweck Konfiguration
Ruff Linting + Formatierung + Sicherheit pyproject.toml[tool.ruff]
mypy Statische Typ-Prüfung pyproject.toml[tool.mypy]
pre-commit Git-Hooks zur Durchsetzung .pre-commit-config.yaml
pytest Testing + Coverage pyproject.toml[tool.pytest.ini_options]

11.2 Quick-Start (neues Projekt)

# 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)

# .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