Files
BSN-Chatsystem/bsn-coding-standards.html
T

801 lines
43 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>BSN Coding Standards — Verbindliches Regelwerk v1.0 (Review-fixed)</title>
<style>
:root {
--bsn-blue: #20228a;
--bsn-blue-light: #2d30b5;
--bg: #ffffff; --bg-card: #f8f9ff; --bg-code: #1a1a2e;
--text: #1a1a2e; --text-secondary: #555; --border: #e0e0e8;
--success: #28a745; --warning: #ffc107; --danger: #dc3545;
--radius: 8px;
}
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
body{font-family:system-ui,-apple-system,'Segoe UI',Roboto,sans-serif;line-height:1.6;color:var(--text);background:#f5f5fa}
.container{max-width:1100px;margin:0 auto;padding:0 20px}
.header{background:linear-gradient(135deg,var(--bsn-blue),#181a6e);color:#fff;padding:60px 20px 50px;text-align:center;position:relative;overflow:hidden}
.header::after{content:'';position:absolute;bottom:0;left:0;right:0;height:4px;background:linear-gradient(90deg,#ff6b6b,#ffd93d,#6bcb77,#4d96ff)}
.header h1{font-size:clamp(1.8rem,4vw,2.8rem);font-weight:800;letter-spacing:-0.5px;margin-bottom:10px}
.header .subtitle{font-size:1.1rem;opacity:.85;max-width:650px;margin:0 auto;line-height:1.5}
.header .meta{margin-top:20px;font-size:.85rem;opacity:.7;display:flex;gap:20px;justify-content:center;flex-wrap:wrap}
.status-badge{display:inline-block;background:rgba(255,255,255,.15);padding:4px 14px;border-radius:20px;font-size:.8rem;font-weight:600;letter-spacing:.5px;text-transform:uppercase;margin-bottom:15px}
.review-badge{display:inline-block;background:#d4edda;color:#155724;padding:4px 14px;border-radius:20px;font-size:.8rem;font-weight:600;margin-bottom:15px;margin-left:8px}
.toc{background:white;border:2px solid var(--border);border-radius:var(--radius);padding:30px;margin:-30px auto 40px;position:relative;box-shadow:0 4px 20px rgba(0,0,0,.06)}
.toc h2{color:var(--bsn-blue);font-size:1.3rem;margin-bottom:15px;padding-bottom:10px;border-bottom:2px solid var(--bsn-blue)}
.toc ol{columns:2;column-gap:40px;list-style-position:inside}
@media(max-width:600px){.toc ol{columns:1}}
.toc li{margin-bottom:6px}
.toc a{color:var(--bsn-blue);text-decoration:none;font-weight:500}
.toc a:hover{color:var(--bsn-blue-light);text-decoration:underline}
section{background:white;border:1px solid var(--border);border-radius:var(--radius);padding:35px 30px;margin-bottom:25px;box-shadow:0 2px 8px rgba(0,0,0,.03)}
h2{font-size:1.5rem;color:var(--bsn-blue);margin-bottom:20px;padding-bottom:8px;border-bottom:2px solid var(--bsn-blue);display:flex;align-items:center;gap:10px}
h2 .num{background:var(--bsn-blue);color:white;width:32px;height:32px;border-radius:6px;display:inline-flex;align-items:center;justify-content:center;font-size:.9rem;font-weight:700;flex-shrink:0}
h3{font-size:1.15rem;color:#181a6e;margin:25px 0 12px}
h4{font-size:1rem;color:#333;margin:16px 0 8px}
p{margin-bottom:12px}p:last-child{margin-bottom:0}
code{background:#eef0ff;color:#181a6e;padding:2px 6px;border-radius:3px;font-family:'JetBrains Mono','Fira Code','Cascadia Code','Consolas',monospace;font-size:.9em}
pre{background:var(--bg-code);color:#e0e0e0;padding:20px;border-radius:var(--radius);overflow-x:auto;margin:15px 0;font-size:.88rem;line-height:1.5;position:relative}
pre code{background:none;color:inherit;padding:0;font-size:inherit}
.kw{color:#c792ea}.str{color:#c3e88d}.cm{color:#676e95;font-style:italic}.fn{color:#82aaff}.num{color:#f78c6c}.op{color:#89ddff}
.badge-correct{display:inline-block;background:#d4edda;color:#155724;padding:2px 8px;border-radius:3px;font-size:.75rem;font-weight:600;margin-right:5px}
.badge-wrong{display:inline-block;background:#f8d7da;color:#721c24;padding:2px 8px;border-radius:3px;font-size:.75rem;font-weight:600;margin-right:5px}
.badge-manual{display:inline-block;background:#fff3cd;color:#856404;padding:2px 8px;border-radius:3px;font-size:.75rem;font-weight:600;margin-right:5px}
.badge-auto{display:inline-block;background:#d4edda;color:#155724;padding:2px 8px;border-radius:3px;font-size:.75rem;font-weight:600;margin-right:5px}
.table-wrap{overflow-x:auto;margin:15px 0}
table{width:100%;border-collapse:collapse;font-size:.92rem}
th{background:var(--bsn-blue);color:white;padding:10px 14px;text-align:left;font-weight:600}
td{padding:10px 14px;border-bottom:1px solid var(--border)}
tr:nth-child(even) td{background:var(--bg-card)}
.card-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:16px;margin:15px 0}
.card{background:var(--bg-card);border:1px solid var(--border);border-radius:var(--radius);padding:20px}
.card h4{color:var(--bsn-blue);margin-top:0;margin-bottom:8px}
.principle-box{background:linear-gradient(135deg,#f0f0ff,#e8e8ff);border-left:4px solid var(--bsn-blue);padding:20px 24px;border-radius:0 var(--radius) var(--radius) 0;margin:20px 0}
.principle-box h4{color:var(--bsn-blue);margin-top:0}
.warning-box{background:#fff8e1;border-left:4px solid var(--warning);padding:16px 20px;border-radius:0 var(--radius) var(--radius) 0;margin:15px 0}
.danger-box{background:#fff0f0;border-left:4px solid var(--danger);padding:16px 20px;border-radius:0 var(--radius) var(--radius) 0;margin:15px 0}
.info-box{background:#e8f4fd;border-left:4px solid var(--bsn-blue-light);padding:16px 20px;border-radius:0 var(--radius) var(--radius) 0;margin:15px 0}
.fixed-box{background:#f0fff4;border-left:4px solid var(--success);padding:16px 20px;border-radius:0 var(--radius) var(--radius) 0;margin:15px 0}
.checklist{list-style:none;padding:0}
.checklist li{padding:6px 0 6px 28px;position:relative}
.checklist li::before{content:'☐';position:absolute;left:0;color:var(--bsn-blue);font-size:1.1rem}
.footer{text-align:center;padding:40px 20px;color:var(--text-secondary);font-size:.9rem}
.footer a{color:var(--bsn-blue)}
.tool-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:15px;margin:20px 0}
.tool-card{border:2px solid var(--border);border-radius:var(--radius);padding:20px;text-align:center;transition:border-color .2s,transform .2s}
.tool-card:hover{border-color:var(--bsn-blue);transform:translateY(-2px)}
.tool-card .tool-icon{font-size:2rem;margin-bottom:10px}
.tool-card h4{color:var(--bsn-blue);margin:5px 0}
.tool-card p{font-size:.85rem;color:var(--text-secondary)}
.changelog{font-size:.85rem;color:var(--text-secondary);margin-top:5px}
.changelog li{margin-bottom:3px}
.changelog .fix{color:#155724;font-weight:600}
@media print{body{background:white}.header{background:var(--bsn-blue)!important;-webkit-print-color-adjust:exact}section{break-inside:avoid;box-shadow:none}pre{background:#f5f5f5;color:#333}}
</style>
</head>
<body>
<p class="meta">21. Juni 2026</p>
<div class="header">
<span class="status-badge">🔒 VERBINDLICH · v1.0</span><span class="review-badge">✅ EXTERN GEPRÜFT</span>
<h1>BSN Coding Standards</h1>
<p class="subtitle">
Das verbindliche Regelwerk für sämtlichen Python-Code im
Brettspiel-News-Community-Projekt. Basierend auf PEP&nbsp;8, Google Python Style Guide
und dem modernen Tooling-Konsens&nbsp;2026. Extern reviewed und korrigiert.
</p>
<div class="meta">
<span>📅 21. Juni 2026</span>
<span>🐍 Python 3.13+</span>
<span>🔧 Ruff 0.15 + mypy 2.1</span>
<span>🇩🇪 Deutsche Docstrings</span>
</div>
</div>
<div class="container">
<nav class="toc">
<h2>📑 Inhaltsverzeichnis</h2>
<ol>
<li><a href="#s1">Allgemeine Prinzipien</a></li>
<li><a href="#s2">Code-Stil (PEP 8+)</a></li>
<li><a href="#s3">Typ-Annotationen</a></li>
<li><a href="#s4">Namenskonventionen</a></li>
<li><a href="#s5">Kommentare &amp; Docstrings</a></li>
<li><a href="#s6">Projektstruktur &amp; Architektur</a></li>
<li><a href="#s7">Sicherheit</a></li>
<li><a href="#s8">Testing</a></li>
<li><a href="#s9">Flask-spezifische Regeln</a></li>
<li><a href="#s10">Git &amp; Commits</a></li>
<li><a href="#s11">Toolchain &amp; Automatische Durchsetzung</a></li>
<li><a href="#s12">Review-Checkliste</a></li>
<li><a href="#s13">Konfigurationsdateien (Referenz)</a></li>
<li><a href="#s14">Änderungshistorie (Review-Fixes)</a></li>
</ol>
</nav>
<!-- ═══════════════════════════════════════════ -->
<!-- 1. ALLGEMEINE PRINZIPIEN -->
<!-- ═══════════════════════════════════════════ -->
<section id="s1">
<h2><span class="num">1</span> Allgemeine Prinzipien</h2>
<div class="principle-box">
<h4>„Readability counts." — Tim Peters, Zen of Python</h4>
<p>Code wird 10× öfter gelesen als geschrieben. Jede Zeile muss
für einen anderen Entwickler (oder dein zukünftiges Ich) sofort
verständlich sein.</p>
</div>
<div class="card-grid">
<div class="card"><h4>📖 Lesbarkeit</h4><p>Code ist Kommunikation. Unklares wird umgeschrieben — nicht mit Kommentar „erklärt".</p></div>
<div class="card"><h4>🎯 Explizit</h4><p>Kein Magic. Typen, Parameter, Rückgabewerte — alles sichtbar.</p></div>
<div class="card"><h4>🧩 Einfach</h4><p>YAGNI — keine Abstraktionen für hypothetische Zukunft.</p></div>
<div class="card"><h4>🛡️ Sichtbare Fehler</h4><p>Exceptions nie still schlucken. Spezifisch fangen + loggen.</p></div>
</div>
<div class="warning-box">
<strong>⚠️ Konsistenz-Regel (aus PEP 8):</strong><br>
Konsistenz mit diesem Styleguide ist wichtig. Konsistenz <em>innerhalb des Projekts</em> ist wichtiger.
Konsistenz <em>innerhalb eines Moduls</em> ist am wichtigsten.<br><br>
Regelbruch? Dokumentiere WARUM im Kommentar.
</div>
</section>
<!-- ═══════════════════════════════════════════ -->
<!-- 2. CODE-STIL -->
<!-- ═══════════════════════════════════════════ -->
<section id="s2">
<h2><span class="num">2</span> Code-Stil (PEP 8+)</h2>
<p><strong>Durchsetzung:</strong> <code>ruff check</code> + <code>ruff format</code> <span class="badge-auto">AUTO</span></p>
<h3>2.1 Zeilenlänge</h3>
<p><strong>Maximal 100 Zeichen.</strong> 88 (Black-Default) ist zu knapp für Flask-Routen und SQLAlchemy-Queries.</p>
<h3>2.2 Einrückung</h3>
<ul>
<li><strong>4 Leerzeichen</strong> pro Ebene — <span class="badge-wrong">NIE Tabs</span></li>
<li>Fortsetzungszeilen: doppelte Einrückung (8) oder vertikale Ausrichtung</li>
</ul>
<pre><code><span class="cm"># ✅ Richtig — vertikale Ausrichtung</span>
result = some_function(
argument_one,
argument_two,
argument_three,
)
<span class="cm"># ✅ Richtig — doppelte Einrückung bei Bedingungen</span>
<span class="kw">if</span> (some_very_long_condition
<span class="kw">and</span> another_long_condition):
do_something()</code></pre>
<h3>2.3 Quotes</h3>
<ul>
<li><strong>Doppelte Quotes</strong> (<code>"</code>) für Strings</li>
<li>Einfache nur, wenn der String <code>"</code> enthält</li>
<li>Docstrings: <strong>immer</strong> <code>"""triple double"""</code></li>
</ul>
<pre><code><span class="kw">def</span> <span class="fn">get_games</span>() -> <span class="kw">list</span>[<span class="kw">dict</span>[<span class="kw">str</span>, <span class="kw">object</span>]]:
<span class="str">"""Holt alle Spiele aus der Datenbank."""</span>
...</code></pre>
<h3>2.4 Imports</h3>
<p><strong>Reihenfolge</strong> (automatisch): 1. Standard-Library 2. Drittanbieter 3. Projekt-intern</p>
<div class="danger-box"><strong>🚫 Verboten:</strong> <code>from module import *</code> · Zirkuläre Imports</div>
<h3>2.5 Trailing Commas</h3>
<p><strong>Immer</strong> bei mehrzeiligen Collections.</p>
<pre><code>items = [<span class="str">"Brettspiel"</span>, <span class="str">"Kartenspiel"</span>, <span class="str">"Würfelspiel"</span>,]</code></pre>
</section>
<!-- ═══════════════════════════════════════════ -->
<!-- 3. TYP-ANNOTATIONEN (FIXED) -->
<!-- ═══════════════════════════════════════════ -->
<section id="s3">
<h2><span class="num">3</span> Typ-Annotationen</h2>
<p><strong>Durchsetzung:</strong> <code>mypy --strict</code> <span class="badge-auto">AUTO</span></p>
<div class="principle-box">
<h4>Grundregel</h4>
<p><strong>Jede Funktion und Methode MUSS vollständige Typ-Annotationen haben.</strong>
Keine Ausnahmen.</p>
</div>
<div class="fixed-box">
<strong>🔧 Review-Fix #1:</strong> Die ursprüngliche <code>Optional[str]</code>-Regel wurde umgedreht.
Bei <code>requires-python = "&gt;=3.13"</code> ist <strong><code>X | None</code></strong> (PEP 604)
die idiomatische, empfohlene Form — und Ruff (UP045) schreibt <code>Optional[X]</code>
automatisch zu <code>X | None</code> um. Regel und Linter sind jetzt im Einklang.
</div>
<pre><code><span class="cm"># ✅ Richtig — X | None (PEP 604, idiomatisch für 3.13+)</span>
<span class="kw">def</span> <span class="fn">get_game_by_id</span>(game_id: <span class="kw">int</span>) -> <span class="kw">dict</span>[<span class="kw">str</span>, <span class="kw">str</span> | <span class="kw">int</span>] | <span class="kw">None</span>:
<span class="str">"""Holt ein Spiel anhand seiner ID."""</span>
...
<span class="cm"># ❌ Falsch — keine Typen</span>
<span class="kw">def</span> <span class="fn">get_game_by_id</span>(game_id):
...</code></pre>
<h3>3.1 Spezifische Typ-Regeln</h3>
<div class="table-wrap"><table>
<tr><th>Regel</th><th>✅ Richtig</th><th>❌ Falsch</th></tr>
<tr><td>Nullable mit <code>X | None</code></td><td><code>str | None</code></td><td><code>Optional[str]</code></td></tr>
<tr><td>Built-in Generics (3.9+)</td><td><code>list[dict[str, object]]</code></td><td><code>List[Dict]</code></td></tr>
<tr><td>Generics parametrisieren</td><td><code>dict[str, Any]</code></td><td><code>dict</code> (implizites <code>dict[Any, Any]</code>)</td></tr>
<tr><td>Explizites <code>-> None</code></td><td><code>def log(msg: str) -> None:</code></td><td><code>def log(msg: str):</code></td></tr>
</table></div>
<div class="info-box">
<strong>💡 Warum <code>X | None</code> und nicht <code>Optional[X]</code>?</strong><br>
Seit <strong>Python 3.10 / PEP 604</strong> ist <code>X | None</code> der idiomatische Weg.
<code>from typing import Optional</code> ist ein Relikt aus Python 3.9 und früher.
Ruff (unter <code>UP045</code>) wandelt <code>Optional[X]</code> automatisch in <code>X | None</code> um —
diesen Auto-Fix nutzen wir aktiv (<code>fixable = ["ALL"]</code>).
</div>
</section>
<!-- ═══════════════════════════════════════════ -->
<!-- 4. NAMENSKONVENTIONEN -->
<!-- ═══════════════════════════════════════════ -->
<section id="s4">
<h2><span class="num">4</span> Namenskonventionen</h2>
<div class="table-wrap"><table>
<tr><th>Element</th><th>Konvention</th><th>Beispiel</th></tr>
<tr><td>Module / Packages</td><td><code>snake_case</code></td><td><code>game_service.py</code></td></tr>
<tr><td>Klassen</td><td><code>PascalCase</code></td><td><code>GameRepository</code></td></tr>
<tr><td>Funktionen / Methoden</td><td><code>snake_case</code></td><td><code>get_game_by_id()</code></td></tr>
<tr><td>Variablen</td><td><code>snake_case</code></td><td><code>game_count</code></td></tr>
<tr><td>Konstanten</td><td><code>UPPER_SNAKE_CASE</code></td><td><code>MAX_RESULTS = 50</code></td></tr>
<tr><td>Private (intern)</td><td><code>_leading_underscore</code></td><td><code>def _parse_response():</code></td></tr>
</table></div>
<h3>Zusätzlich</h3>
<ul>
<li>Boolesche: <code>is_active</code>, <code>has_permission</code>, <code>should_retry</code></li>
<li>Funktionen als Verben: <code>get_</code>, <code>create_</code>, <code>validate_</code></li>
<li><span class="badge-wrong">Keine</span> Ein-Buchstaben-Variablen (außer Comprehensions)</li>
<li><span class="badge-wrong">Keine</span> kryptischen Abkürzungen</li>
</ul>
</section>
<!-- ═══════════════════════════════════════════ -->
<!-- 5. KOMMENTARE -->
<!-- ═══════════════════════════════════════════ -->
<section id="s5">
<h2><span class="num">5</span> Kommentare &amp; Docstrings</h2>
<div class="principle-box">
<h4>🇩🇪 Alle Kommentare und Docstrings auf DEUTSCH</h4>
<p>Code-Schlüsselwörter sind Englisch. Alle menschenlesbaren Erklärungen: Deutsch.</p>
</div>
<h3>Google-Style Docstrings</h3>
<pre><code><span class="kw">def</span> <span class="fn">berechne_durchschnitt</span>(werte: <span class="kw">list</span>[<span class="kw">float</span>]) -> <span class="kw">float</span>:
<span class="str">"""Berechnet den arithmetischen Durchschnitt.
Args:
werte: Liste von Gleitkommazahlen.
Returns:
Der Durchschnittswert.
Raises:
ValueError: Wenn die Liste leer ist.
"""</span>
<span class="kw">if not</span> werte:
<span class="kw">raise</span> <span class="kw">ValueError</span>(<span class="str">"Die Werteliste darf nicht leer sein."</span>)
<span class="kw">return</span> sum(werte) / len(werte)</code></pre>
<div class="danger-box"><strong>🚫 Auskommentierter Code:</strong> Nie! Dafür gibt's Git History.</div>
</section>
<!-- ═══════════════════════════════════════════ -->
<!-- 6. PROJEKTSTRUKTUR (FIXED — Automatik-Status) -->
<!-- ═══════════════════════════════════════════ -->
<section id="s6">
<h2><span class="num">6</span> Projektstruktur &amp; Architektur</h2>
<div class="fixed-box">
<strong>🔧 Review-Fix #4:</strong> Die „max 500 Zeilen / 50 Zeilen"-Grenzen sind
jetzt <strong>teil-automatisiert</strong>: <code>C901</code> (McCabe-Komplexität) und
<code>PLR0915</code> (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.
</div>
<h3>6.1 Maximalgrößen</h3>
<div class="table-wrap"><table>
<tr><th>Einheit</th><th>Maximum</th><th>Prüfung</th></tr>
<tr><td>Modul (.py-Datei)</td><td><strong>500 Zeilen</strong></td><td><span class="badge-manual">MANUELL</span> (Review)</td></tr>
<tr><td>Funktion / Methode</td><td><strong>50 Statements</strong></td><td><span class="badge-auto">AUTO</span> (Ruff PLR0915)</td></tr>
<tr><td>Komplexität (McCabe)</td><td><strong>max 10</strong></td><td><span class="badge-auto">AUTO</span> (Ruff C901)</td></tr>
<tr><td>Klasse</td><td><strong>300 Zeilen</strong></td><td><span class="badge-manual">MANUELL</span> (Review)</td></tr>
<tr><td>Zeile</td><td><strong>100 Zeichen</strong></td><td><span class="badge-auto">AUTO</span> (Ruff E501)</td></tr>
</table></div>
<h3>6.2 Verzeichnisstruktur</h3>
<pre><code>project/
├── app.py <span class="cm"># Flask-App-Factory + Config</span>
├── routes/ <span class="cm"># NUR Request-Handling — keine Business-Logik</span>
├── services/ <span class="cm"># Business-Logik (Validierung, Berechnung, DB)</span>
├── models/ <span class="cm"># DB-Modelle — nur Schema, keine Logik</span>
├── utils/ <span class="cm"># Pure Functions — kein Zustand, keine DB</span>
├── templates/ <span class="cm"># Jinja2 — CSS IMMER inline</span>
├── static/ <span class="cm"># JS, Bilder</span>
├── tests/ <span class="cm"># Spiegelt src/-Struktur</span>
├── migrations/ <span class="cm"># Alembic</span>
├── pyproject.toml <span class="cm"># ALLE Konfiguration an einem Ort</span>
├── .pre-commit-config.yaml
└── CODING_STANDARDS.md</code></pre>
<h3>6.3 Architekturprinzipien</h3>
<div class="card-grid">
<div class="card"><h4>🚫 Keine God-Files</h4><p><code>app.py</code> ≠ 6000 Zeilen. Auslagern!</p></div>
<div class="card"><h4>🧠 Routes sind dumm</h4><p>Request → Service → Response. Nichts weiter.</p></div>
<div class="card"><h4>⚙️ Services = Logik</h4><p>Validierung, Berechnung, DB-Zugriffe.</p></div>
<div class="card"><h4>📦 Models = Schema</h4><p>Tabellen-Definition. Keine Business-Logik.</p></div>
</div>
</section>
<!-- ═══════════════════════════════════════════ -->
<!-- 7. SICHERHEIT (FIXED — B101) -->
<!-- ═══════════════════════════════════════════ -->
<section id="s7">
<h2><span class="num">7</span> Sicherheit</h2>
<p><strong>Durchsetzung:</strong> <code>ruff</code> (S-Regeln = flake8-bandit) <span class="badge-auto">AUTO</span></p>
<div class="fixed-box">
<strong>🔧 Review-Fix #2+3:</strong> Bandit als separater pre-commit-Hook wurde
<strong>entfernt</strong> — Ruffs <code>S</code>-Regeln decken dieselben Checks ab, nur schneller,
ohne separates Tool. Der <strong>B101-Skip wurde entfernt</strong>: asserts werden jetzt
global geprüft, nur in <code>tests/**/*.py</code> per <code>per-file-ignores</code> erlaubt.
Die assert-Regel aus 7.3 wird damit tatsächlich durchgesetzt.
</div>
<h3>7.1 Secrets</h3>
<div class="danger-box">
<strong>🚫 NIEMALS Secrets im Code!</strong> API-Keys, Passwörter, Tokens → immer
Environment Variables. <code>.env</code> + <code>.db</code> in <code>.gitignore</code>.
</div>
<h3>7.2 Input-Validierung</h3>
<ul>
<li><strong>Alle User-Inputs validieren</strong> vor Verarbeitung</li>
<li>SQL-Injection: Parameterisierte Queries (SQLAlchemy Default)</li>
<li>XSS: User-Input escapen (Jinja2 Default)</li>
</ul>
<h3>7.3 Weitere Regeln</h3>
<ul>
<li><span class="badge-wrong">Keine</span> <code>assert</code>-Statements in Produktionscode <span class="badge-auto">AUTO</span></li>
<li><span class="badge-wrong">Kein</span> <code>pickle</code> für ungetrustete Daten</li>
<li><code>subprocess</code> mit <code>shell=False</code> (Default)</li>
<li>Passwörter: <code>bcrypt</code> oder <code>argon2</code> — nie <code>md5</code>/<code>sha1</code></li>
<li>HTTPS überall — HSTS-Header setzen</li>
</ul>
</section>
<!-- ═══════════════════════════════════════════ -->
<!-- 8. TESTING (FIXED — TDD→Empfehlung) -->
<!-- ═══════════════════════════════════════════ -->
<section id="s8">
<h2><span class="num">8</span> Testing</h2>
<p><strong>Durchsetzung:</strong> <code>pytest --cov --cov-fail-under=80</code> <span class="badge-manual">CI</span> (nicht in pre-commit)</p>
<div class="fixed-box">
<strong>🔧 Review-Fix (#6):</strong> TDD („erst Test, dann Code") ist jetzt eine
<strong>Empfehlung</strong>, keine verbindliche Regel. Echte TDD-Pflicht wäre
inkonsequent bei 80% Coverage-Floor. Der Floor bleibt hart bei 80%.
</div>
<h3>8.1 Grundregeln</h3>
<ol>
<li><strong>80% Code Coverage</strong> als harter Floor — unterschreiten blockiert den Merge in CI</li>
<li><strong>Empfohlen:</strong> TDD (Test-Driven Development) — erst Test, dann Code</li>
<li><code>pytest</code> als einziges Test-Framework</li>
<li>Tests in <code>tests/</code>, Struktur spiegelt <code>src/</code></li>
</ol>
<h3>8.2 Test-Beispiel</h3>
<pre><code><span class="kw">class</span> <span class="fn">TestGameService</span>:
<span class="kw">def</span> <span class="fn">test_get_by_id_returns_game_when_found</span>(<span class="kw">self</span>, db_session):
<span class="cm"># Arrange</span>
service = GameService(db_session)
<span class="cm"># Act</span>
result = service.get_by_id(<span class="num">1</span>)
<span class="cm"># Assert</span>
<span class="kw">assert</span> result <span class="kw">is not None</span>
<span class="kw">assert</span> result.name == <span class="str">"Catan"</span></code></pre>
</section>
<!-- ═══════════════════════════════════════════ -->
<!-- 9. FLASK (FIXED — dict-Typ) -->
<!-- ═══════════════════════════════════════════ -->
<section id="s9">
<h2><span class="num">9</span> Flask-spezifische Regeln</h2>
<div class="fixed-box">
<strong>🔧 Review-Fix (#6):</strong> <code>def search_games() -> dict:</code> wurde
zu <code>-> dict[str, object]</code> korrigiert. <code>dict</code> ohne Parameter
ist <code>dict[Any, Any]</code> — das verstößt gegen mypy strict.
</div>
<div class="table-wrap"><table>
<tr><th>Regel</th><th>✅ Richtig</th><th>❌ Falsch</th></tr>
<tr><td><code>methods</code> explizit</td><td><code>@app.route("/api/games", methods=["GET"])</code></td><td>ohne methods</td></tr>
<tr><td>POST = <code>get_json()</code></td><td><code>data = request.get_json(silent=True)</code> ← None bei Parse-Fehler!</td><td><code>request.args</code></td></tr>
<tr><td>Response + Statuscode</td><td><code>return data, 201</code></td><td><code>return data</code></td></tr>
<tr><td>Fehler einheitlich</td><td><code>{"error": "msg"}, 400</code></td><td>plain string</td></tr>
<tr><td>Generics immer parametrisiert</td><td><code>-> tuple[dict[str, object], int]</code></td><td><code>-> tuple[dict, int]</code></td></tr>
</table></div>
<pre><code><span class="cm"># ✅ Route — korrekte Typen, methods explizit</span>
<span class="kw">@app</span>.route(<span class="str">"/api/search"</span>, methods=[<span class="str">"GET"</span>])
<span class="kw">def</span> <span class="fn">search_games</span>() -> <span class="kw">tuple</span>[<span class="kw">dict</span>[<span class="kw">str</span>, <span class="kw">object</span>], <span class="kw">int</span>]:
query = request.args.get(<span class="str">"q"</span>, <span class="str">""</span>).strip()
<span class="kw">if not</span> query <span class="kw">or</span> len(query) > <span class="num">100</span>:
<span class="kw">return</span> {<span class="str">"error"</span>: <span class="str">"Ungültiger Suchbegriff"</span>}, <span class="num">400</span>
<span class="kw">return</span> game_service.search(query), <span class="num">200</span></code></pre>
<h3>Config &amp; UI</h3>
<ul>
<li><code>app.config.from_prefixed_env()</code> — Secrets nie hartkodieren</li>
<li>CSS <strong>immer inline</strong> in Templates</li>
<li>Mobile-first, Primärfarbe <strong style="color:#20228a;">BSN-Blau #20228a</strong></li>
<li>Touch-Buttons min 64px, Overlays min 85vh</li>
</ul>
</section>
<!-- ═══════════════════════════════════════════ -->
<!-- 10. GIT -->
<!-- ═══════════════════════════════════════════ -->
<section id="s10">
<h2><span class="num">10</span> Git &amp; Commits</h2>
<h3>Conventional Commits</h3>
<pre><code>&lt;type&gt;(&lt;scope&gt;): &lt;description&gt;</code></pre>
<div class="table-wrap"><table>
<tr><th>Typ</th><th>Verwendung</th></tr>
<tr><td><code>feat</code></td><td>Neues Feature</td></tr>
<tr><td><code>fix</code></td><td>Bugfix</td></tr>
<tr><td><code>docs</code></td><td>Dokumentation</td></tr>
<tr><td><code>style</code></td><td>Formatierung (kein Code-Change)</td></tr>
<tr><td><code>refactor</code></td><td>Code-Umbau</td></tr>
<tr><td><code>test</code></td><td>Tests</td></tr>
<tr><td><code>chore</code></td><td>Build, Deps, CI</td></tr>
<tr><td><code>security</code></td><td>Sicherheitsrelevante Änderung</td></tr>
</table></div>
<h3>Commit-Hygiene</h3>
<ul>
<li>Nach <strong>jeder abgeschlossenen Aufgabe</strong> committen</li>
<li>Ein Commit = eine logische Änderung</li>
<li><strong>Vor jedem Commit:</strong> <code>pre-commit run</code> + <code>pytest</code></li>
</ul>
</section>
<!-- ═══════════════════════════════════════════ -->
<!-- 11. TOOLCHAIN (FIXED — mypy raus, Bandit raus) -->
<!-- ═══════════════════════════════════════════ -->
<section id="s11">
<h2><span class="num">11</span> Toolchain &amp; Automatische Durchsetzung</h2>
<div class="fixed-box">
<strong>🔧 Review-Fix #5:</strong> <strong>mypy wurde aus pre-commit entfernt.</strong>
mypy im isolierten pre-commit-venv driftet garantiert vom echten Projekt-Environment weg
(Stubs, Abhängigkeiten) und produziert andere Ergebnisse als <code>mypy .</code> im
Projekt-venv. Stattdessen: <strong>mypy läuft lokal</strong> (Schritt 4 im Workflow)
und <strong>in CI</strong> gegen das echte venv.<br><br>
<strong>🔧 Review-Fix #3:</strong> <strong>Bandit wurde als separater Hook gestrichen.</strong>
Ruffs <code>S</code>-Regeln (flake8-bandit) decken dieselben Sicherheits-Checks ab
— ein Tool weniger, keine widersprüchlichen Findings.
</div>
<div class="tool-grid">
<div class="tool-card"><div class="tool-icon">🦀</div><h4>Ruff 0.15</h4><p>Linting + Formatierung<br>10-100× schneller als Flake8+Black</p></div>
<div class="tool-card"><div class="tool-icon">🔍</div><h4>mypy 2.1</h4><p>Statische Typ-Prüfung<br>⚠️ Major v2 (Mai 2026) — bei Problemen 1.20</p></div>
<div class="tool-card"><div class="tool-icon">🪝</div><h4>pre-commit</h4><p>Git-Hooks<br>Blockiert Commits bei Verstößen</p></div>
<div class="tool-card"><div class="tool-icon">🧪</div><h4>pytest</h4><p>Testing + Coverage<br>80% Coverage minimum</p></div>
</div>
<div class="warning-box">
<strong>⚠️ mypy 2.1 — Breaking Changes (Mai 2026):</strong> Version 2.x ist sechs Wochen alt.
Mit <code>strict = true</code> kann Code, der unter 1.x sauber lief, neue Fehler werfen
(<code>--allow-redefinition</code>-Semantik, PEP 688 bytearray/memoryview).
Bei Projektstart bewusst entscheiden: Entweder 2.1 nehmen (stricter) oder auf
<strong>1.20</strong> bleiben (stabil) und nach dem ersten Milestone upgraden.
</div>
<h3>11.1 Täglicher Workflow</h3>
<pre><code><span class="cm"># 1. Einmalig: Tools installieren</span>
pip install ruff mypy pre-commit pytest
<span class="cm"># 2. Einmalig: Hooks aktivieren</span>
pre-commit install
<span class="cm"># 3. Beim Coden: Formatieren + Linten (auto-fix!)</span>
ruff check . --fix
ruff format .
<span class="cm"># 4. Typ-Prüfung (im Projekt-venv, NICHT in pre-commit)</span>
mypy .
<span class="cm"># 5. Tests ausführen</span>
pytest --cov --cov-fail-under=80
<span class="cm"># 6. Commit — pre-commit prüft AUTOMATISCH</span>
git commit -m <span class="str">"feat: neue Spiele-Suche"</span>
<span class="cm"># → Ruff Lint + Format laufen automatisch</span>
<span class="cm"># → Basic Checks (Merge-Konflikte, Whitespace, Debug-Statements)</span></code></pre>
<h3>11.2 Was wird automatisch geprüft?</h3>
<div class="table-wrap"><table>
<tr><th>Hook</th><th>Prüft</th><th>Blockiert bei</th></tr>
<tr><td>Ruff (Lint)</td><td>Code-Stil, Fehler, Komplexität, Sicherheit</td><td>PEP-8-Verstößen, ungenutzten Imports, S-Regeln</td></tr>
<tr><td>Ruff (Format)</td><td>Einrückung, Quotes, Zeilenlänge</td><td>Abweichender Formatierung</td></tr>
<tr><td>Check-Merge-Conflict</td><td>Merge-Konflikte</td><td><code>&lt;&lt;&lt;&lt;&lt;&lt;&lt;</code> im Code</td></tr>
<tr><td>Trailing-Whitespace</td><td>Whitespace am Zeilenende</td><td>Leerzeichen/Tabs am Zeilenende</td></tr>
<tr><td>End-of-File-Fixer</td><td>Fehlende Newline am Dateiende</td><td>Dateien ohne abschließende Newline</td></tr>
<tr><td>Debug-Statements</td><td><code>pdb</code>, <code>breakpoint()</code></td><td>Debug-Code im Commit</td></tr>
<tr><td>Large Files</td><td>Dateigröße &gt; 5 MB</td><td>Versehentliche Binaries</td></tr>
</table></div>
<h3>11.3 Was wird NICHT automatisch geprüft?</h3>
<p>Diese Checks sind <strong>manuell im Review</strong> durchzuführen oder laufen in CI:</p>
<ul>
<li><strong>mypy</strong> — läuft lokal + in CI gegen das echte venv (siehe Fix #5)</li>
<li><strong>Dateigrößen-Limits</strong> (500 Zeilen/Modul, 300 Zeilen/Klasse) — manuelles Review</li>
<li><strong>Coverage 80%</strong><code>pytest --cov</code> muss manuell ausgeführt werden (oder in CI)</li>
</ul>
<h3>11.4 CI-Setup (später)</h3>
<pre><code><span class="cm"># .gitea/workflows/quality.yml (Gitea Actions — GitHub-kompatibel)</span>
- <span class="kw">name</span>: Lint + Security
run: ruff check .
- <span class="kw">name</span>: Type Check (im echten venv!)
run: mypy .
- <span class="kw">name</span>: Tests + Coverage
run: pytest --cov --cov-fail-under=80</code></pre>
</section>
<!-- ═══════════════════════════════════════════ -->
<!-- 12. CHECKLISTE (FIXED — Auto/Manuell markiert) -->
<!-- ═══════════════════════════════════════════ -->
<section id="s12">
<h2><span class="num">12</span> Code-Review-Checkliste</h2>
<p>Bei <strong>jedem Code-Review</strong> durchgehen. <span class="badge-auto">AUTO</span> = wird von pre-commit erzwungen.</p>
<ul class="checklist">
<li><span class="badge-auto">AUTO</span> PEP 8 eingehalten? (Ruff-Check bestanden)</li>
<li><span class="badge-auto">AUTO</span> Keine <code>from module import *</code>?</li>
<li><span class="badge-auto">AUTO</span> Keine Debug-Statements (<code>pdb</code>, <code>breakpoint</code>)?</li>
<li><span class="badge-auto">AUTO</span> Keine Merge-Konflikte im Code?</li>
<li>Alle Funktionen haben vollständige Typ-Annotationen? (mypy bestanden)</li>
<li>Docstrings auf Deutsch für alle öffentlichen Funktionen?</li>
<li>Keine Secrets im Code?</li>
<li>User-Input wird validiert?</li>
<li>Tests geschrieben und grün? Coverage ≥ 80%?</li>
<li>Keine God-Files (&gt;500 Zeilen)? <span class="badge-manual">MANUELL</span></li>
<li>Funktionen max 50 Statements, McCabe ≤ 10? <span class="badge-auto">AUTO</span></li>
<li>Route enthält keine Business-Logik?</li>
<li>Commit folgt Conventional Commits?</li>
<li>Keine zirkulären Imports?</li>
</ul>
</section>
<!-- ═══════════════════════════════════════════ -->
<!-- 13. KONFIGURATIONSDATEIEN (FIXED) -->
<!-- ═══════════════════════════════════════════ -->
<section id="s13">
<h2><span class="num">13</span> Konfigurationsdateien (Referenz)</h2>
<p>Diese Dateien werden im Projekt-Root abgelegt. Alle Review-Fixes sind eingearbeitet.</p>
<h3>13.1 pyproject.toml</h3>
<pre><code>[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "bsn-community"
version = "0.1.0"
description = "Brettspiel-News Community-Projekt"
requires-python = "&gt;=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",
]</code></pre>
<h3>13.2 .pre-commit-config.yaml</h3>
<pre><code>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)"</code></pre>
</section>
<!-- ═══════════════════════════════════════════ -->
<!-- 14. ÄNDERUNGSHISTORIE -->
<!-- ═══════════════════════════════════════════ -->
<section id="s14">
<h2><span class="num">14</span> Änderungshistorie (Review-Fixes)</h2>
<p>Diese Version enthält alle Korrekturen aus dem externen Code-Review vom 21. Juni 2026.</p>
<div class="table-wrap"><table>
<tr><th>#</th><th>Problem</th><th>Fix</th><th>Betroffene Sektionen</th></tr>
<tr>
<td>1</td>
<td><strong>Optional[X] vs X | None</strong> — Regel widersprach sich selbst und kämpfte gegen RUFF (UP045)</td>
<td><code>X | None</code> als Standard (PEP 604, idiomatisch für 3.13). <code>Optional</code> aus allen Beispielen entfernt. Warnkasten durch Info-Box mit Begründung ersetzt.</td>
<td>§3, alle Code-Beispiele</td>
</tr>
<tr>
<td>2</td>
<td><strong>B101 global deaktiviert</strong> — „kein assert in Produktion" wurde nie durchgesetzt</td>
<td>Globalen B101-Skip entfernt. Asserts nur in <code>tests/**/*.py</code> per <code>per-file-ignores</code> erlaubt.</td>
<td>§7, §13 (pyproject.toml)</td>
</tr>
<tr>
<td>3</td>
<td><strong>Bandit doppelt</strong> — separater Hook + Ruffs S-Regeln = Redundanz</td>
<td>Bandit-Hook komplett gestrichen. Ruffs <code>S</code>-Regeln decken alles ab.</td>
<td>§11, §13 (.pre-commit-config.yaml)</td>
</tr>
<tr>
<td>4</td>
<td><strong>God-File/Function</strong> als „automatisch" behauptet, aber keine Regeln konfiguriert</td>
<td><code>C90</code> + <code>PL</code> zu Ruff-Select hinzugefügt. McCabe max 10, Statements max 50. Dateigrößen-Limits klar als <span class="badge-manual">MANUELL</span> markiert.</td>
<td>§6, §12, §13</td>
</tr>
<tr>
<td>5</td>
<td><strong>mypy in pre-commit</strong> — isoliertes venv driftet vom Projekt-Environment</td>
<td>mypy aus .pre-commit-config.yaml entfernt. Läuft stattdessen lokal (<code>mypy .</code>) und in CI.</td>
<td>§11, §13</td>
</tr>
<tr>
<td>6</td>
<td><strong>Kleinkram:</strong> TDD-Pflicht inkonsequent, <code>-> dict:</code> verstößt gegen mypy strict, Versions-Pins veraltet</td>
<td>TDD → Empfehlung. <code>dict</code><code>dict[str, object]</code>. Ruff 0.11.3 → 0.15.18, mypy 1.15.0 → 2.1.0.</td>
<td>§8, §9, §13</td>
</tr>
<tr>
<td colspan="4" style="background:var(--bsn-blue);color:white;text-align:center;font-weight:600">Zweite Review-Runde — 21. Juni 2026</td>
</tr>
<tr>
<td>7</td>
<td><strong>UP007 vs UP045</strong> — falsche Regel-Nummer in der Info-Box</td>
<td><code>UP007</code><code>UP045</code> korrigiert. Seit Ruff 0.12: UP007 = typing.Union, UP045 = typing.Optional.</td>
<td>§3, §14</td>
</tr>
<tr>
<td>8</td>
<td><strong>Coverage als AUTO gelabelt</strong> — pre-commit führt kein pytest aus</td>
<td>„blockiert den Commit" → „blockiert den Merge in CI". Coverage-Label ehrlich als CI-Check (nicht pre-commit).</td>
<td>§8</td>
</tr>
<tr>
<td>9</td>
<td><strong>mypy 2.1 Bleeding-Edge</strong> — Major v2 (Mai 2026) mit Breaking Changes</td>
<td>Warnbox in §11 ergänzt. Empfehlung: Entweder 2.1 bewusst nehmen, oder auf 1.20 bleiben.</td>
<td>§11</td>
</tr>
<tr>
<td>10</td>
<td><strong>CI-Pfad GitHub-flavored</strong> und <code>silent=True</code> None-Falle</td>
<td><code>.github/</code><code>.gitea/</code>. <code>get_json(silent=True)</code> mit None-Hinweis versehen.</td>
<td>§9, §11</td>
</tr>
</table></div>
</section>
<div class="footer">
<p>
<strong>BSN Coding Standards v1.0</strong> — Extern reviewed ✅<br>
Erstellt von Cody, korrigiert nach externem Review, für Daniel Krause ·
<a href="https://brettspiel-news.de">brettspiel-news.de</a><br>
Nächste Überprüfung: Bei Projektstart oder Dezember 2026
</p>
</div>
</div>
</body>
</html>