688 lines
19 KiB
Markdown
688 lines
19 KiB
Markdown
# 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
|