diff --git a/docs/bsn-coding-standards.html b/docs/bsn-coding-standards.html new file mode 100644 index 0000000..1fb0f34 --- /dev/null +++ b/docs/bsn-coding-standards.html @@ -0,0 +1,798 @@ + + + + + +BSN Coding Standards β€” Verbindliches Regelwerk v1.0 (Review-fixed) + + + + +
+ πŸ”’ VERBINDLICH Β· v1.0βœ… EXTERN GEPRÜFT +

BSN Coding Standards

+

+ Das verbindliche Regelwerk fΓΌr sΓ€mtlichen Python-Code im + Brettspiel-News-Community-Projekt. Basierend auf PEP 8, Google Python Style Guide + und dem modernen Tooling-Konsens 2026. Extern reviewed und korrigiert. +

+
+ πŸ“… 21. Juni 2026 + 🐍 Python 3.13+ + πŸ”§ Ruff 0.15 + mypy 2.1 + πŸ‡©πŸ‡ͺ Deutsche Docstrings +
+
+ +
+ + + + + + +
+

1 Allgemeine Prinzipien

+
+

β€žReadability counts." β€” Tim Peters, Zen of Python

+

Code wird 10Γ— ΓΆfter gelesen als geschrieben. Jede Zeile muss + fΓΌr einen anderen Entwickler (oder dein zukΓΌnftiges Ich) sofort + verstΓ€ndlich sein.

+
+
+

πŸ“– Lesbarkeit

Code ist Kommunikation. Unklares wird umgeschrieben β€” nicht mit Kommentar β€žerklΓ€rt".

+

🎯 Explizit

Kein Magic. Typen, Parameter, RΓΌckgabewerte β€” alles sichtbar.

+

🧩 Einfach

YAGNI β€” keine Abstraktionen fΓΌr hypothetische Zukunft.

+

πŸ›‘οΈ Sichtbare Fehler

Exceptions nie still schlucken. Spezifisch fangen + loggen.

+
+
+ ⚠️ Konsistenz-Regel (aus PEP 8):
+ Konsistenz mit diesem Styleguide ist wichtig. Konsistenz innerhalb des Projekts ist wichtiger. + Konsistenz innerhalb eines Moduls ist am wichtigsten.

+ Regelbruch? Dokumentiere WARUM im Kommentar. +
+
+ + + + +
+

2 Code-Stil (PEP 8+)

+

Durchsetzung: ruff check + ruff format AUTO

+ +

2.1 ZeilenlΓ€nge

+

Maximal 100 Zeichen. 88 (Black-Default) ist zu knapp fΓΌr Flask-Routen und SQLAlchemy-Queries.

+ +

2.2 EinrΓΌckung

+ +
# βœ… Richtig β€” vertikale Ausrichtung
+result = some_function(
+    argument_one,
+    argument_two,
+    argument_three,
+)
+
+# βœ… Richtig β€” doppelte EinrΓΌckung bei Bedingungen
+if (some_very_long_condition
+        and another_long_condition):
+    do_something()
+ +

2.3 Quotes

+ +
def get_games() -> list[dict[str, object]]:
+    """Holt alle Spiele aus der Datenbank."""
+    ...
+ +

2.4 Imports

+

Reihenfolge (automatisch): 1. Standard-Library 2. Drittanbieter 3. Projekt-intern

+
🚫 Verboten: from module import * · ZirkulÀre Imports
+ +

2.5 Trailing Commas

+

Immer bei mehrzeiligen Collections.

+
items = ["Brettspiel", "Kartenspiel", "WΓΌrfelspiel",]
+
+ + + + +
+

3 Typ-Annotationen

+

Durchsetzung: mypy --strict AUTO

+ +
+

Grundregel

+

Jede Funktion und Methode MUSS vollstΓ€ndige Typ-Annotationen haben. + Keine Ausnahmen.

+
+ +
+ πŸ”§ Review-Fix #1: Die ursprΓΌngliche Optional[str]-Regel wurde umgedreht. + Bei requires-python = ">=3.13" ist X | None (PEP 604) + die idiomatische, empfohlene Form β€” und Ruff (UP045) schreibt Optional[X] + automatisch zu X | None um. Regel und Linter sind jetzt im Einklang. +
+ +
# βœ… Richtig β€” X | None (PEP 604, idiomatisch fΓΌr 3.13+)
+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.1 Spezifische Typ-Regeln

+
+ + + + + +
Regelβœ… Richtig❌ Falsch
Nullable mit X | Nonestr | NoneOptional[str]
Built-in Generics (3.9+)list[dict[str, object]]List[Dict]
Generics parametrisierendict[str, Any]dict (implizites dict[Any, Any])
Explizites -> Nonedef log(msg: str) -> None:def log(msg: str):
+ +
+ πŸ’‘ Warum X | None und nicht Optional[X]?
+ Seit Python 3.10 / PEP 604 ist X | None der idiomatische Weg. + from typing import Optional ist ein Relikt aus Python 3.9 und frΓΌher. + Ruff (unter UP045) wandelt Optional[X] automatisch in X | None um β€” + diesen Auto-Fix nutzen wir aktiv (fixable = ["ALL"]). +
+
+ + + + +
+

4 Namenskonventionen

+
+ + + + + + + +
ElementKonventionBeispiel
Module / Packagessnake_casegame_service.py
KlassenPascalCaseGameRepository
Funktionen / Methodensnake_caseget_game_by_id()
Variablensnake_casegame_count
KonstantenUPPER_SNAKE_CASEMAX_RESULTS = 50
Private (intern)_leading_underscoredef _parse_response():
+

ZusΓ€tzlich

+ +
+ + + + +
+

5 Kommentare & Docstrings

+
+

πŸ‡©πŸ‡ͺ Alle Kommentare und Docstrings auf DEUTSCH

+

Code-SchlΓΌsselwΓΆrter sind Englisch. Alle menschenlesbaren ErklΓ€rungen: Deutsch.

+
+

Google-Style Docstrings

+
def berechne_durchschnitt(werte: list[float]) -> float:
+    """Berechnet den arithmetischen Durchschnitt.
+
+    Args:
+        werte: Liste von Gleitkommazahlen.
+
+    Returns:
+        Der Durchschnittswert.
+
+    Raises:
+        ValueError: Wenn die Liste leer ist.
+    """
+    if not werte:
+        raise ValueError("Die Werteliste darf nicht leer sein.")
+    return sum(werte) / len(werte)
+
🚫 Auskommentierter Code: Nie! Dafür gibt's Git History.
+
+ + + + +
+

6 Projektstruktur & Architektur

+ +
+ πŸ”§ Review-Fix #4: Die β€žmax 500 Zeilen / 50 Zeilen"-Grenzen sind + jetzt teil-automatisiert: C901 (McCabe-KomplexitΓ€t) und + PLR0915 (too-many-statements, Default 50) wurden in Ruff aktiviert. + Dateigrâßen-Limits bleiben manuell β€” Ruff hat keinen β€žlines per file"-Checker. + Die Tabelle unten zeigt klar, was automatisch und was manuell geprΓΌft wird. +
+ +

6.1 Maximalgrâßen

+
+ + + + + + +
EinheitMaximumPrΓΌfung
Modul (.py-Datei)500 ZeilenMANUELL (Review)
Funktion / Methode50 StatementsAUTO (Ruff PLR0915)
KomplexitΓ€t (McCabe)max 10AUTO (Ruff C901)
Klasse300 ZeilenMANUELL (Review)
Zeile100 ZeichenAUTO (Ruff E501)
+ +

6.2 Verzeichnisstruktur

+
project/
+β”œβ”€β”€ app.py              # Flask-App-Factory + Config
+β”œβ”€β”€ routes/             # NUR Request-Handling β€” keine Business-Logik
+β”œβ”€β”€ services/           # Business-Logik (Validierung, Berechnung, DB)
+β”œβ”€β”€ models/             # DB-Modelle β€” nur Schema, keine Logik
+β”œβ”€β”€ utils/              # Pure Functions β€” kein Zustand, keine DB
+β”œβ”€β”€ templates/          # Jinja2 β€” CSS IMMER inline
+β”œβ”€β”€ static/             # JS, Bilder
+β”œβ”€β”€ tests/              # Spiegelt src/-Struktur
+β”œβ”€β”€ migrations/         # Alembic
+β”œβ”€β”€ pyproject.toml      # ALLE Konfiguration an einem Ort
+β”œβ”€β”€ .pre-commit-config.yaml
+└── CODING_STANDARDS.md
+ +

6.3 Architekturprinzipien

+
+

🚫 Keine God-Files

app.py β‰  6000 Zeilen. Auslagern!

+

🧠 Routes sind dumm

Request β†’ Service β†’ Response. Nichts weiter.

+

βš™οΈ Services = Logik

Validierung, Berechnung, DB-Zugriffe.

+

πŸ“¦ Models = Schema

Tabellen-Definition. Keine Business-Logik.

+
+
+ + + + +
+

7 Sicherheit

+

Durchsetzung: ruff (S-Regeln = flake8-bandit) AUTO

+ +
+ πŸ”§ Review-Fix #2+3: Bandit als separater pre-commit-Hook wurde + entfernt β€” Ruffs S-Regeln decken dieselben Checks ab, nur schneller, + ohne separates Tool. Der B101-Skip wurde entfernt: asserts werden jetzt + global geprΓΌft, nur in tests/**/*.py per per-file-ignores erlaubt. + Die assert-Regel aus 7.3 wird damit tatsΓ€chlich durchgesetzt. +
+ +

7.1 Secrets

+
+ 🚫 NIEMALS Secrets im Code! API-Keys, PasswΓΆrter, Tokens β†’ immer + Environment Variables. .env + .db in .gitignore. +
+ +

7.2 Input-Validierung

+ + +

7.3 Weitere Regeln

+ +
+ + + + +
+

8 Testing

+

Durchsetzung: pytest --cov --cov-fail-under=80 AUTO

+ +
+ πŸ”§ Review-Fix (#6): TDD (β€žerst Test, dann Code") ist jetzt eine + Empfehlung, keine verbindliche Regel. Echte TDD-Pflicht wΓ€re + inkonsequent bei 80% Coverage-Floor. Der Floor bleibt hart bei 80%. +
+ +

8.1 Grundregeln

+
    +
  1. 80% Code Coverage als harter Floor β€” unterschreiten blockiert den Merge in CI
  2. +
  3. Empfohlen: TDD (Test-Driven Development) β€” erst Test, dann Code
  4. +
  5. pytest als einziges Test-Framework
  6. +
  7. Tests in tests/, Struktur spiegelt src/
  8. +
+ +

8.2 Test-Beispiel

+
class TestGameService:
+    def test_get_by_id_returns_game_when_found(self, db_session):
+        # Arrange
+        service = GameService(db_session)
+        # Act
+        result = service.get_by_id(1)
+        # Assert
+        assert result is not None
+        assert result.name == "Catan"
+
+ + + + +
+

9 Flask-spezifische Regeln

+ +
+ πŸ”§ Review-Fix (#6): def search_games() -> dict: wurde + zu -> dict[str, object] korrigiert. dict ohne Parameter + ist dict[Any, Any] β€” das verstâßt gegen mypy strict. +
+ +
+ + + + + + +
Regelβœ… Richtig❌ Falsch
methods explizit@app.route("/api/games", methods=["GET"])ohne methods
POST = get_json()data = request.get_json(silent=True) ← None bei Parse-Fehler!request.args
Response + Statuscodereturn data, 201return data
Fehler einheitlich{"error": "msg"}, 400plain string
Generics immer parametrisiert-> tuple[dict[str, object], int]-> tuple[dict, int]
+ +
# βœ… Route β€” korrekte Typen, methods explizit
+@app.route("/api/search", methods=["GET"])
+def search_games() -> tuple[dict[str, object], int]:
+    query = request.args.get("q", "").strip()
+    if not query or len(query) > 100:
+        return {"error": "UngΓΌltiger Suchbegriff"}, 400
+    return game_service.search(query), 200
+ +

Config & UI

+ +
+ + + + +
+

10 Git & Commits

+

Conventional Commits

+
<type>(<scope>): <description>
+
+ + + + + + + + + +
TypVerwendung
featNeues Feature
fixBugfix
docsDokumentation
styleFormatierung (kein Code-Change)
refactorCode-Umbau
testTests
choreBuild, Deps, CI
securitySicherheitsrelevante Γ„nderung
+

Commit-Hygiene

+ +
+ + + + +
+

11 Toolchain & Automatische Durchsetzung

+ +
+ πŸ”§ Review-Fix #5: mypy wurde aus pre-commit entfernt. + mypy im isolierten pre-commit-venv driftet garantiert vom echten Projekt-Environment weg + (Stubs, AbhΓ€ngigkeiten) und produziert andere Ergebnisse als mypy . im + Projekt-venv. Stattdessen: mypy lΓ€uft lokal (Schritt 4 im Workflow) + und in CI gegen das echte venv.

+ πŸ”§ Review-Fix #3: Bandit wurde als separater Hook gestrichen. + Ruffs S-Regeln (flake8-bandit) decken dieselben Sicherheits-Checks ab + β€” ein Tool weniger, keine widersprΓΌchlichen Findings. +
+ +
+
πŸ¦€

Ruff 0.15

Linting + Formatierung
10-100Γ— schneller als Flake8+Black

+
πŸ”

mypy 2.1

Statische Typ-PrΓΌfung
⚠️ Major v2 (Mai 2026) β€” bei Problemen 1.20

+
πŸͺ

pre-commit

Git-Hooks
Blockiert Commits bei Verstâßen

+
πŸ§ͺ

pytest

Testing + Coverage
80% Coverage minimum

+
+ +
+ ⚠️ mypy 2.1 β€” Breaking Changes (Mai 2026): Version 2.x ist sechs Wochen alt. + Mit strict = true kann Code, der unter 1.x sauber lief, neue Fehler werfen + (--allow-redefinition-Semantik, PEP 688 bytearray/memoryview). + Bei Projektstart bewusst entscheiden: Entweder 2.1 nehmen (stricter) oder auf + 1.20 bleiben (stabil) und nach dem ersten Milestone upgraden. +
+ +

11.1 TΓ€glicher Workflow

+
# 1. Einmalig: Tools installieren
+pip install ruff mypy pre-commit pytest
+
+# 2. Einmalig: Hooks aktivieren
+pre-commit install
+
+# 3. Beim Coden: Formatieren + Linten (auto-fix!)
+ruff check . --fix
+ruff format .
+
+# 4. Typ-PrΓΌfung (im Projekt-venv, NICHT in pre-commit)
+mypy .
+
+# 5. Tests ausfΓΌhren
+pytest --cov --cov-fail-under=80
+
+# 6. Commit β€” pre-commit prΓΌft AUTOMATISCH
+git commit -m "feat: neue Spiele-Suche"
+# β†’ Ruff Lint + Format laufen automatisch
+# β†’ Basic Checks (Merge-Konflikte, Whitespace, Debug-Statements)
+ +

11.2 Was wird automatisch geprΓΌft?

+
+ + + + + + + + +
HookPrΓΌftBlockiert bei
Ruff (Lint)Code-Stil, Fehler, KomplexitÀt, SicherheitPEP-8-Verstâßen, ungenutzten Imports, S-Regeln
Ruff (Format)EinrΓΌckung, Quotes, ZeilenlΓ€ngeAbweichender Formatierung
Check-Merge-ConflictMerge-Konflikte<<<<<<< im Code
Trailing-WhitespaceWhitespace am ZeilenendeLeerzeichen/Tabs am Zeilenende
End-of-File-FixerFehlende Newline am DateiendeDateien ohne abschließende Newline
Debug-Statementspdb, breakpoint()Debug-Code im Commit
Large FilesDateigrâße > 5 MBVersehentliche Binaries
+ +

11.3 Was wird NICHT automatisch geprΓΌft?

+

Diese Checks sind manuell im Review durchzufΓΌhren oder laufen in CI:

+ + +

11.4 CI-Setup (spΓ€ter)

+
# .gitea/workflows/quality.yml (Gitea Actions β€” GitHub-kompatibel)
+- name: Lint + Security
+  run: ruff check .
+- name: Type Check (im echten venv!)
+  run: mypy .
+- name: Tests + Coverage
+  run: pytest --cov --cov-fail-under=80
+
+ + + + +
+

12 Code-Review-Checkliste

+

Bei jedem Code-Review durchgehen. AUTO = wird von pre-commit erzwungen.

+ + +
+ + + + +
+

13 Konfigurationsdateien (Referenz)

+

Diese Dateien werden im Projekt-Root abgelegt. Alle Review-Fixes sind eingearbeitet.

+ +

13.1 pyproject.toml

+
[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project]
+name = "bsn-community"
+version = "0.1.0"
+description = "Brettspiel-News Community-Projekt"
+requires-python = ">=3.13"
+readme = "README.md"
+license = {text = "Proprietary"}
+authors = [{name = "Daniel Krause", email = "info@brettspiel-news.de"}]
+
+# ── Ruff: Linting + Formatierung + Sicherheit ──
+[tool.ruff]
+line-length = 100
+indent-width = 4
+target-version = "py313"
+
+[tool.ruff.lint]
+select = [
+    "E",     # pycodestyle errors
+    "F",     # Pyflakes
+    "W",     # pycodestyle warnings
+    "I",     # isort (Import-Ordnung)
+    "N",     # pep8-naming (Namenskonventionen)
+    "UP",    # pyupgrade (auto-upgrade auf 3.13-Syntax)
+    "B",     # flake8-bugbear (Common Bugs)
+    "C4",    # flake8-comprehensions
+    "SIM",   # flake8-simplify
+    "TCH",   # flake8-type-checking
+    "RUF",   # Ruff-spezifische Regeln
+    "S",     # flake8-bandit β€” Sicherheit (ersetzt separaten Bandit-Hook!)
+    "D",     # pydocstyle (Docstrings)
+    "C90",   # McCabe-KomplexitΓ€t
+    "PL",    # Pylint-Regeln (too-many-*)
+]
+ignore = [
+    "D100",  # Modul-Docstring optional
+    "D104",  # Package-Docstring optional
+    "D107",  # __init__ Docstring optional
+    "D203",  # Konflikt mit D211
+    "D213",  # Konflikt mit D212
+]
+fixable = ["ALL"]
+
+[tool.ruff.lint.mccabe]
+max-complexity = 10
+
+[tool.ruff.lint.pylint]
+max-statements = 50    # PLR0915 β€” entspricht β€žmax 50 Statements/Funktion"
+max-args = 7           # PLR0913
+max-branches = 15      # PLR0912
+
+[tool.ruff.lint.per-file-ignores]
+"tests/**/*.py" = ["S101", "D103"]   # asserts + fehlende Docstrings in Tests erlaubt
+"migrations/**/*.py" = ["D"]         # Keine Docstring-Pflicht in Migrationen
+
+[tool.ruff.format]
+quote-style = "double"
+indent-style = "space"
+skip-magic-trailing-comma = false
+line-ending = "auto"
+
+# ── mypy: Statische Typ-PrΓΌfung ──
+[tool.mypy]
+strict = true
+python_version = "3.13"
+exclude = ["migrations/", "tests/"]
+warn_unreachable = true
+warn_unused_ignores = true
+enable_error_code = ["ignore-without-code", "redundant-expr", "truthy-bool"]
+
+# ── pytest: Testing ──
+[tool.pytest.ini_options]
+testpaths = ["tests"]
+python_files = "test_*.py"
+python_classes = "Test*"
+python_functions = "test_*"
+addopts = [
+    "--strict-markers",
+    "--tb=short",
+    "--cov=src",
+    "--cov-report=term-missing",
+]
+markers = [
+    "slow: Langsame Tests (Datenbank, API-Calls)",
+    "integration: Integrationstests",
+]
+ +

13.2 .pre-commit-config.yaml

+
repos:
+  - repo: https://github.com/astral-sh/ruff-pre-commit
+    rev: v0.15.18
+    hooks:
+      - id: ruff
+        name: "πŸ” Ruff β€” Linting + Sicherheit"
+        args: [--fix]
+      - id: ruff-format
+        name: "🎨 Ruff β€” Formatierung"
+
+  # mypy lΓ€uft NICHT in pre-commit (isoliertes venv-Drift-Problem).
+  # Stattdessen: manuell mit `mypy .` oder in CI gegen das echte venv.
+
+  - repo: https://github.com/pre-commit/pre-commit-hooks
+    rev: v5.0.0
+    hooks:
+      - id: check-added-large-files
+        name: "πŸ“¦ Keine großen Dateien (>5 MB)"
+        args: [--maxkb=5000]
+      - id: check-merge-conflict
+        name: "⚠️ Keine Merge-Konflikte"
+      - id: trailing-whitespace
+        name: "🧹 Kein Whitespace am Zeilenende"
+      - id: end-of-file-fixer
+        name: "πŸ“„ Dateien enden mit Newline"
+      - id: check-yaml
+        name: "πŸ“‹ YAML-Syntax prΓΌfen"
+      - id: check-toml
+        name: "πŸ“‹ TOML-Syntax prΓΌfen"
+      - id: debug-statements
+        name: "πŸ› Keine Debug-Statements (pdb, breakpoint)"
+
+ + + + +
+

14 Γ„nderungshistorie (Review-Fixes)

+ +

Diese Version enthΓ€lt alle Korrekturen aus dem externen Code-Review vom 21. Juni 2026.

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
#ProblemFixBetroffene Sektionen
1Optional[X] vs X | None β€” Regel widersprach sich selbst und kΓ€mpfte gegen RUFF (UP045)X | None als Standard (PEP 604, idiomatisch fΓΌr 3.13). Optional aus allen Beispielen entfernt. Warnkasten durch Info-Box mit BegrΓΌndung ersetzt.Β§3, alle Code-Beispiele
2B101 global deaktiviert β€” β€žkein assert in Produktion" wurde nie durchgesetztGlobalen B101-Skip entfernt. Asserts nur in tests/**/*.py per per-file-ignores erlaubt.Β§7, Β§13 (pyproject.toml)
3Bandit doppelt β€” separater Hook + Ruffs S-Regeln = RedundanzBandit-Hook komplett gestrichen. Ruffs S-Regeln decken alles ab.Β§11, Β§13 (.pre-commit-config.yaml)
4God-File/Function als β€žautomatisch" behauptet, aber keine Regeln konfiguriertC90 + PL zu Ruff-Select hinzugefΓΌgt. McCabe max 10, Statements max 50. Dateigrâßen-Limits klar als MANUELL markiert.Β§6, Β§12, Β§13
5mypy in pre-commit β€” isoliertes venv driftet vom Projekt-Environmentmypy aus .pre-commit-config.yaml entfernt. LΓ€uft stattdessen lokal (mypy .) und in CI.Β§11, Β§13
6Kleinkram: TDD-Pflicht inkonsequent, -> dict: verstâßt gegen mypy strict, Versions-Pins veraltetTDD β†’ Empfehlung. dict β†’ dict[str, object]. Ruff 0.11.3 β†’ 0.15.18, mypy 1.15.0 β†’ 2.1.0.Β§8, Β§9, Β§13
Zweite Review-Runde β€” 21. Juni 2026
7UP007 vs UP045 β€” falsche Regel-Nummer in der Info-BoxUP007 β†’ UP045 korrigiert. Seit Ruff 0.12: UP007 = typing.Union, UP045 = typing.Optional.Β§3, Β§14
8Coverage als AUTO gelabelt β€” pre-commit fΓΌhrt kein pytest ausβ€žblockiert den Commit" β†’ β€žblockiert den Merge in CI". Coverage-Label ehrlich als CI-Check (nicht pre-commit).Β§8
9mypy 2.1 Bleeding-Edge β€” Major v2 (Mai 2026) mit Breaking ChangesWarnbox in Β§11 ergΓ€nzt. Empfehlung: Entweder 2.1 bewusst nehmen, oder auf 1.20 bleiben.Β§11
10CI-Pfad GitHub-flavored und silent=True None-Falle.github/ β†’ .gitea/. get_json(silent=True) mit None-Hinweis versehen.Β§9, Β§11
+
+ + + +
+ +