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 @@ + + +
+ + ++ 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. +
+ +Code wird 10Γ ΓΆfter gelesen als geschrieben. Jede Zeile muss + fΓΌr einen anderen Entwickler (oder dein zukΓΌnftiges Ich) sofort + verstΓ€ndlich sein.
+Code ist Kommunikation. Unklares wird umgeschrieben β nicht mit Kommentar βerklΓ€rt".
Kein Magic. Typen, Parameter, RΓΌckgabewerte β alles sichtbar.
YAGNI β keine Abstraktionen fΓΌr hypothetische Zukunft.
Exceptions nie still schlucken. Spezifisch fangen + loggen.
Durchsetzung: ruff check + ruff format AUTO
Maximal 100 Zeichen. 88 (Black-Default) ist zu knapp fΓΌr Flask-Routen und SQLAlchemy-Queries.
+ +# β
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()
+
+ ") fΓΌr Strings" enthΓ€lt"""triple double"""def get_games() -> list[dict[str, object]]:
+ """Holt alle Spiele aus der Datenbank."""
+ ...
+
+ Reihenfolge (automatisch): 1. Standard-Library 2. Drittanbieter 3. Projekt-intern
+from module import * Β· ZirkulΓ€re ImportsImmer bei mehrzeiligen Collections.
+items = ["Brettspiel", "Kartenspiel", "WΓΌrfelspiel",]
+Durchsetzung: mypy --strict AUTO
Jede Funktion und Methode MUSS vollstΓ€ndige Typ-Annotationen haben. + Keine Ausnahmen.
+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):
+ ...
+
+ | Regel | β Richtig | β Falsch |
|---|---|---|
Nullable mit X | None | str | None | Optional[str] |
| Built-in Generics (3.9+) | list[dict[str, object]] | List[Dict] |
| Generics parametrisieren | dict[str, Any] | dict (implizites dict[Any, Any]) |
Explizites -> None | def log(msg: str) -> None: | def log(msg: str): |
X | None und nicht Optional[X]?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"]).
+ | Element | Konvention | Beispiel |
|---|---|---|
| Module / Packages | snake_case | game_service.py |
| Klassen | PascalCase | GameRepository |
| Funktionen / Methoden | snake_case | get_game_by_id() |
| Variablen | snake_case | game_count |
| Konstanten | UPPER_SNAKE_CASE | MAX_RESULTS = 50 |
| Private (intern) | _leading_underscore | def _parse_response(): |
is_active, has_permission, should_retryget_, create_, validate_Code-SchlΓΌsselwΓΆrter sind Englisch. Alle menschenlesbaren ErklΓ€rungen: Deutsch.
+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)
+ 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.
+ | Einheit | Maximum | PrΓΌfung |
|---|---|---|
| Modul (.py-Datei) | 500 Zeilen | MANUELL (Review) |
| Funktion / Methode | 50 Statements | AUTO (Ruff PLR0915) |
| KomplexitΓ€t (McCabe) | max 10 | AUTO (Ruff C901) |
| Klasse | 300 Zeilen | MANUELL (Review) |
| Zeile | 100 Zeichen | AUTO (Ruff E501) |
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
+
+ app.py β 6000 Zeilen. Auslagern!
Request β Service β Response. Nichts weiter.
Validierung, Berechnung, DB-Zugriffe.
Tabellen-Definition. Keine Business-Logik.
Durchsetzung: ruff (S-Regeln = flake8-bandit) AUTO
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.
+ .env + .db in .gitignore.
+ assert-Statements in Produktionscode AUTOpickle fΓΌr ungetrustete Datensubprocess mit shell=False (Default)bcrypt oder argon2 β nie md5/sha1Durchsetzung: pytest --cov --cov-fail-under=80 AUTO
pytest als einziges Test-Frameworktests/, Struktur spiegelt src/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"
+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 + Statuscode | return data, 201 | return data |
| Fehler einheitlich | {"error": "msg"}, 400 | plain 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
+
+ app.config.from_prefixed_env() β Secrets nie hartkodieren<type>(<scope>): <description>
+ | Typ | Verwendung |
|---|---|
feat | Neues Feature |
fix | Bugfix |
docs | Dokumentation |
style | Formatierung (kein Code-Change) |
refactor | Code-Umbau |
test | Tests |
chore | Build, Deps, CI |
security | Sicherheitsrelevante Γnderung |
pre-commit run + pytestmypy . im
+ Projekt-venv. Stattdessen: mypy lΓ€uft lokal (Schritt 4 im Workflow)
+ und in CI gegen das echte venv.S-Regeln (flake8-bandit) decken dieselben Sicherheits-Checks ab
+ β ein Tool weniger, keine widersprΓΌchlichen Findings.
+ Linting + Formatierung
10-100Γ schneller als Flake8+Black
Statische Typ-PrΓΌfung
β οΈ Major v2 (Mai 2026) β bei Problemen 1.20
Git-Hooks
Blockiert Commits bei VerstΓΆΓen
Testing + Coverage
80% Coverage minimum
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.
+ # 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)
+
+ | Hook | PrΓΌft | Blockiert bei |
|---|---|---|
| Ruff (Lint) | Code-Stil, Fehler, KomplexitΓ€t, Sicherheit | PEP-8-VerstΓΆΓen, ungenutzten Imports, S-Regeln |
| Ruff (Format) | EinrΓΌckung, Quotes, ZeilenlΓ€nge | Abweichender Formatierung |
| Check-Merge-Conflict | Merge-Konflikte | <<<<<<< im Code |
| Trailing-Whitespace | Whitespace am Zeilenende | Leerzeichen/Tabs am Zeilenende |
| End-of-File-Fixer | Fehlende Newline am Dateiende | Dateien ohne abschlieΓende Newline |
| Debug-Statements | pdb, breakpoint() | Debug-Code im Commit |
| Large Files | DateigrΓΆΓe > 5 MB | Versehentliche Binaries |
Diese Checks sind manuell im Review durchzufΓΌhren oder laufen in CI:
+pytest --cov muss manuell ausgefΓΌhrt werden (oder in CI)# .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
+Bei jedem Code-Review durchgehen. AUTO = wird von pre-commit erzwungen.
+ +from module import *?pdb, breakpoint)?Diese Dateien werden im Projekt-Root abgelegt. Alle Review-Fixes sind eingearbeitet.
+ +[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",
+]
+
+ 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)"
+Diese Version enthΓ€lt alle Korrekturen aus dem externen Code-Review vom 21. Juni 2026.
+ +| # | Problem | Fix | Betroffene Sektionen |
|---|---|---|---|
| 1 | +Optional[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 | +
| 2 | +B101 global deaktiviert β βkein assert in Produktion" wurde nie durchgesetzt | +Globalen B101-Skip entfernt. Asserts nur in tests/**/*.py per per-file-ignores erlaubt. |
+ Β§7, Β§13 (pyproject.toml) | +
| 3 | +Bandit doppelt β separater Hook + Ruffs S-Regeln = Redundanz | +Bandit-Hook komplett gestrichen. Ruffs S-Regeln decken alles ab. |
+ Β§11, Β§13 (.pre-commit-config.yaml) | +
| 4 | +God-File/Function als βautomatisch" behauptet, aber keine Regeln konfiguriert | +C90 + PL zu Ruff-Select hinzugefΓΌgt. McCabe max 10, Statements max 50. DateigrΓΆΓen-Limits klar als MANUELL markiert. |
+ Β§6, Β§12, Β§13 | +
| 5 | +mypy in pre-commit β isoliertes venv driftet vom Projekt-Environment | +mypy aus .pre-commit-config.yaml entfernt. LΓ€uft stattdessen lokal (mypy .) und in CI. |
+ Β§11, Β§13 | +
| 6 | +Kleinkram: TDD-Pflicht inkonsequent, -> dict: verstΓΆΓt gegen mypy strict, Versions-Pins veraltet |
+ TDD β 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 | +|||
| 7 | +UP007 vs UP045 β falsche Regel-Nummer in der Info-Box | +UP007 β UP045 korrigiert. Seit Ruff 0.12: UP007 = typing.Union, UP045 = typing.Optional. |
+ Β§3, Β§14 | +
| 8 | +Coverage 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 | +
| 9 | +mypy 2.1 Bleeding-Edge β Major v2 (Mai 2026) mit Breaking Changes | +Warnbox in Β§11 ergΓ€nzt. Empfehlung: Entweder 2.1 bewusst nehmen, oder auf 1.20 bleiben. | +Β§11 | +
| 10 | +CI-Pfad GitHub-flavored und silent=True None-Falle |
+ .github/ β .gitea/. get_json(silent=True) mit None-Hinweis versehen. |
+ Β§9, Β§11 | +