Tooling (Welle B): ruff-Linting einführen, Pre-commit-Hook härten

- ruff.toml: Config (E/F/W, E501 bewusst ignoriert wegen langer
  deutscher Daten-/Log-Strings; alembic/versions ausgeschlossen)
- requirements-dev.txt mit ruff==0.15.18
- .githooks/pre-commit: ruff-Linting vor den Tests; fragiles
  'grep -q OK' durch echte Exit-Code-Prüfung ersetzt; Testaufruf auf
  'unittest discover' vereinheitlicht
- Lints behoben: ungenutzte Imports (datetime, Optional, request u.a.),
  f-strings ohne Platzhalter, ungenutzte Test-Variable; bewusster
  alembic-Verfügbarkeits-Check mit noqa markiert

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-06-22 12:39:31 +02:00
commit 814b3f45cb
10 changed files with 55 additions and 32 deletions

View file

@ -1,46 +1,47 @@
#!/bin/bash
# Pre-commit hook für Typewriter Trainer
# Führt automatisch Tests aus vor jedem Commit
# Führt Linting (ruff) und Tests vor jedem Commit aus.
#
# Installation:
# cp .githooks/pre-commit .git/hooks/pre-commit
# chmod +x .git/hooks/pre-commit
echo "🧪 Running tests before commit..."
set -u
echo "🔎 Running pre-commit checks..."
echo ""
# Farben für Output
RED='\033[0;31m'
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
NC='\033[0m'
# Unit Tests ausführen (schnell)
echo "📋 Running Unit Tests..."
if python -m unittest tests.test_lesson_service tests.test_statistics_service tests.test_settings_service -v 2>&1 | grep -q "OK"; then
echo -e "${GREEN}✅ Unit Tests passed${NC}"
# 1. Linting mit ruff (falls installiert)
if python -m ruff --version >/dev/null 2>&1; then
echo "🧹 Running ruff..."
if ! python -m ruff check .; then
echo -e "${RED}❌ Linting failed${NC}"
echo "Bitte 'python -m ruff check .' beheben (ggf. '--fix')."
exit 1
fi
echo -e "${GREEN}✅ Linting passed${NC}"
else
echo -e "${RED}❌ Unit Tests failed${NC}"
echo ""
echo "Please fix the tests before committing."
exit 1
echo -e "${YELLOW}⚠️ ruff nicht installiert Linting übersprungen${NC}"
echo " Installation: pip install -r requirements-dev.txt"
fi
echo ""
# Integration Tests ausführen
echo "🔗 Running Integration Tests..."
if python -m unittest tests.test_integration_progress tests.test_integration_statistics -v 2>&1 | grep -q "OK"; then
echo -e "${GREEN}✅ Integration Tests passed${NC}"
else
echo -e "${RED}❌ Integration Tests failed${NC}"
echo ""
echo "Please fix the tests before committing."
# 2. Tests (Exit-Code-basiert, kein grep)
echo "🧪 Running tests..."
if ! python -m unittest discover -s tests -p "test_*.py"; then
echo -e "${RED}❌ Tests failed${NC}"
echo "Bitte die Tests vor dem Commit beheben."
exit 1
fi
echo -e "${GREEN}✅ Tests passed${NC}"
echo ""
echo -e "${GREEN}✅ All tests passed! Proceeding with commit...${NC}"
echo -e "${GREEN}✅ All checks passed! Proceeding with commit...${NC}"
echo ""
exit 0

2
app.py
View file

@ -90,7 +90,7 @@ if __name__ == '__main__':
logger.info("Tipptrainer gestartet")
logger.info(f"Version: {VERSION}")
logger.info(f"Lektionen geladen: {len(helpers.lessons)}")
logger.info(f"Debug-Info verfügbar unter: http://127.0.0.1:5000/debug")
logger.info("Debug-Info verfügbar unter: http://127.0.0.1:5000/debug")
logger.info("=" * 50)
app.run(debug=True, host='0.0.0.0', port=5000)

View file

@ -1,5 +1,4 @@
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime
db = SQLAlchemy()

4
requirements-dev.txt Normal file
View file

@ -0,0 +1,4 @@
# Entwicklungs-Abhängigkeiten (nicht für den Produktivbetrieb nötig)
-r requirements.txt
ruff==0.15.18

View file

@ -8,7 +8,7 @@ import json
import logging
import threading
import time
from flask import Blueprint, request, jsonify, current_app
from flask import Blueprint, jsonify, current_app
from services import helpers
from services.helpers import (
repair_json,

19
ruff.toml Normal file
View file

@ -0,0 +1,19 @@
# Ruff-Konfiguration https://docs.astral.sh/ruff/
target-version = "py311"
line-length = 120
# Generierte / externe Verzeichnisse nicht prüfen
extend-exclude = ["venv", "alembic/versions"]
[lint]
select = ["E", "F", "W"]
# Zeilenlänge nicht erzwingen: das Projekt enthält lange deutsche
# Daten-Strings (Lektionstexte) und Log-Meldungen, bei denen E501 nur
# Rauschen erzeugt. Logikfehler (E/F/W) bleiben aktiv.
ignore = ["E501"]
[lint.per-file-ignores]
# Tests setzen FLASK_TESTING bewusst VOR dem App-Import (E402)
"tests/*" = ["E402"]
# app.py registriert Blueprints bewusst nach App-/CSRF-Setup (E402)
"app.py" = ["E402"]

View file

@ -74,7 +74,7 @@ def get_or_create_secret_key() -> str:
# Setze Dateiberechtigungen auf 0o600 (nur User kann lesen/schreiben)
os.chmod(env_path, 0o600)
logger.info(f"Neuer Secret Key generiert und in .env gespeichert (Berechtigungen: 0o600)")
logger.info("Neuer Secret Key generiert und in .env gespeichert (Berechtigungen: 0o600)")
except Exception as e:
logger.error(f"Fehler beim Speichern der .env Datei: {e}")
logger.warning("Verwende temporären Secret Key für diese Session")

View file

@ -4,7 +4,7 @@ Handles all progress and lesson progress related business logic
"""
import logging
from typing import Dict, Any, Optional
from typing import Dict, Any
from models import db, Progress, LessonProgress
logger = logging.getLogger(__name__)

View file

@ -26,7 +26,7 @@ def main():
# Prüfe ob Alembic installiert ist
try:
import alembic
import alembic # noqa: F401 # nur Verfügbarkeits-Check
logger.info("✓ Alembic ist installiert")
except ImportError:
logger.error("✗ Alembic nicht gefunden!")
@ -36,7 +36,7 @@ def main():
# Prüfe ob instance/ Verzeichnis existiert
instance_dir = os.path.join(os.path.dirname(__file__), 'instance')
if not os.path.exists(instance_dir):
logger.info(f"Erstelle instance/ Verzeichnis...")
logger.info("Erstelle instance/ Verzeichnis...")
os.makedirs(instance_dir)
logger.info("✓ Verzeichnis erstellt")

View file

@ -5,7 +5,7 @@ import unittest
import os
os.environ['FLASK_TESTING'] = 'true' # Set before importing app
from datetime import datetime, timedelta, date
from datetime import timedelta, date
from app import app, db
from models import LessonStatistic, DailyPractice
from services.statistics_service import StatisticsService
@ -281,7 +281,7 @@ class TestStatisticsServiceIntegration(unittest.TestCase):
)
# Speichere in DB
saved_stat = StatisticsService.save_lesson_statistics(
StatisticsService.save_lesson_statistics(
lesson_index=5,
chars_per_minute=stats['typing_speed'],
error_rate=stats['error_rate'],