Beispielarbeiten · Code & Fehler
Code-Modernisierung: veraltete Module ersetzt, Verhalten gleich
Ein altes Python-Skript mit Modulen, die in neuen Python-Versionen wegfallen, wurde modernisiert. Tests vorher und nachher, gleiche Ausgaben Byte für Byte.
Musterarbeit (Demo), kein Kundenauftrag. Gebaut als Probeaufgabe mit erfundenen Beispieldaten; die Prüfzahlen unten stammen aus dem automatischen Prüflauf und sind nicht geschönt.
Was gebaut wurde
- Zuerst Tests, die das alte Verhalten festhalten
- Dann veraltete Bausteine ersetzt: Kommandozeile, Zeitangaben, Typangaben, Formatierung
- Danach dieselben Tests grün und ohne Warnung über veraltete Module
Wie geprüft wurde
Automatischer Prüflauf vom 18.10.2026, aus dem Protokoll (Testdaten erfunden, Zahlen gemessen):
- 7/7 gruen
- Tests gruen vorher 15/15, nachher 15/15
- vorher: DeprecationWarning: 'cgi' is deprecated and slated for removal in Python 3.13
- Altlasten vorher ['%-Formatierung', 'from optparse', 'import cgi', 'io.open', 'typing.Dict', 'typing.List', 'utcfromtimestamp'], nachher []
Alle Prüffälle (7 von 7 grün)
- ✓ 1 Charakterisierungstests gegen alten Code gruen
- ✓ 2 Alter Code wirft DeprecationWarning beim Import
- ✓ 3 Dieselben Tests nach Modernisierung gruen (-W error::DeprecationWarning)
- ✓ 4 Identische Testnamen vorher/nachher
- ✓ 5 Goldene Ausgaben byte-gleich, Exitcodes gleich
- ✓ 6 Neuer Code importiert ohne DeprecationWarning
- ✓ 7 AST: keine Altlasten mehr (vorher gefunden)
Grenzen
Was diese Probe nicht abdeckt, steht hier, damit niemand mehr erwartet:
- Probe: Python-Modernisierung (optparse->argparse, cgi->email, naive->UTC-Zeit, Typen/f-Strings); Sprung zwischen Frameworks (z. B. AngularJS->React, Python 2->3 mit C-Erweiterungen) ist groesser und …
- Verhalten wird durch Charakterisierungstests + goldene Ausgaben gehalten; Fehlertexte der Kommandozeile duerfen sich im Wortlaut aendern (Exitcode bleibt)
- requests->httpx nicht Teil der Probe (requests ist in der Werkzeug-Umgebung nicht installiert)
Die Änderung (Diff)
--- zeitbericht.py (alt)
+++ zeitbericht.py (neu)
@@ -1,86 +1,80 @@
-# -*- coding: utf-8 -*-
"""Zeitbericht: Zeiterfassungs-CSV (zeitstempel;stunden;projekt) zusammenfassen."""
-import cgi
-import datetime
-import io
+from __future__ import annotations
+
+import argparse
import json
-import os
import sys
-from optparse import OptionParser
-from typing import Dict, List
+from datetime import date, datetime, timezone
+from email.message import Message
+from pathlib import Path
-def kodierung_aus(content_type):
- """'text/csv; charset=latin-1' -> 'latin-1' (Standard utf-8)."""
- _, params = cgi.parse_header(content_type or "text/csv")
- return params.get("charset", "utf-8")
+def kodierung_aus(content_type: str | None) -> str:
+ """'text/csv; charset=latin-1' -> 'latin-1' (Standard utf-8). Ersatz fuer cgi.parse_header."""
+ m = Message()
+ m["content-type"] = content_type or "text/csv"
+ return m.get_param("charset", "utf-8")
-def lade_eintraege(pfad, kodierung="utf-8"):
- # type: (str, str) -> List[Dict]
+def lade_eintraege(pfad: str | Path, kodierung: str = "utf-8") -> list[dict]:
eintraege = []
- f = io.open(pfad, "r", encoding=kodierung)
- try:
+ with Path(pfad).open(encoding=kodierung) as f:
for zeile in f:
zeile = zeile.strip()
if not zeile or zeile.startswith("#"):
continue
ts, stunden, projekt = zeile.split(";")
- eintraege.append({"zeit": datetime.datetime.utcfromtimestamp(int(ts)),
+ eintraege.append({"zeit": datetime.fromtimestamp(int(ts), timezone.utc),
"stunden": float(stunden.replace(",", ".")), "projekt": projekt.strip()})
- finally:
- f.close()
return eintraege
-def zusammenfassen(eintraege, projekt=None, ab=None):
- # type: (List[Dict], str, datetime.date) -> Dict
+def _utc_text(t: datetime) -> str:
+ return t.astimezone(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S") + "Z"
+
+
+def zusammenfassen(eintraege: list[dict], projekt: str | None = None, ab: date | None = None) -> dict:
auswahl = [e for e in eintraege
if (projekt is None or e["projekt"] == projekt) and (ab is None or e["zeit"].date() >= ab)]
- je_projekt = {} # type: Dict[str, float]
+ je_projekt: dict[str, float] = {}
for e in auswahl:
je_projekt[e["projekt"]] = je_projekt.get(e["projekt"], 0.0) + e["stunden"]
return {
"anzahl": len(auswahl),
"stunden": round(sum(e["stunden"] for e in auswahl), 2),
"je_projekt": dict(sorted((k, round(v, 2)) for k, v in je_projekt.items())),
- "erster": auswahl and min(e["zeit"] for e in auswahl).isoformat() + "Z" or None,
- "letzter": auswahl and max(e["zeit"] for e in auswahl).isoformat() + "Z" or None,
+ "erster": _utc_text(min(e["zeit"] for e in auswahl)) if auswahl else None,
+ "letzter": _utc_text(max(e["zeit"] for e in auswahl)) if auswahl else None,
}
-def als_text(z):
- zeilen = ["Eintraege: %d, Stunden: %.2f" % (z["anzahl"], z["stunden"])]
- for p, h in z["je_projekt"].items():
- zeilen.append(" %-12s %7.2f h" % (p, h))
+def als_text(z: dict) -> str:
+ zeilen = [f"Eintraege: {z['anzahl']}, Stunden: {z['stunden']:.2f}"]
+ zeilen += [f" {p:<12} {h:7.2f} h" for p, h in z["je_projekt"].items()]
return "\n".join(zeilen)
-def main(argv=None):
- parser = OptionParser(usage="%prog [optionen] DATEI")
- parser.add_option("-p", "--projekt", dest="projekt", help="nur dieses Projekt")
- parser.add_option("--ab", dest="ab", help="nur ab Datum JJJJ-MM-TT")
- parser.add_option("-f", "--format", dest="format", default="text", choices=["text", "json"],
- help="text oder json")
- parser.add_option("--content-type", dest="content_type", default="text/csv; charset=utf-8",
- help="Kodierung der Datei als Content-Type")
- optionen, args = parser.parse_args(argv)
- if len(args) != 1:
- parser.error("genau eine DATEI angeben")
- if not os.path.exists(args[0]):
- sys.stderr.write("Datei nicht gefunden: %s\n" % args[0])
+def _datum(text: str) -> date:
+ try:
+ return datetime.strptime(text, "%Y-%m-%d").date()
+ except ValueError:
+ raise argparse.ArgumentTypeError("--ab braucht JJJJ-MM-TT") from None
+
+
+def main(argv: list[str] | None = None) -> int:
+ parser = argparse.ArgumentParser(usage="%(prog)s [optionen] DATEI")
+ parser.add_argument("datei", metavar="DATEI")
+ parser.add_argument("-p", "--projekt", help="nur dieses Projekt")
+ parser.add_argument("--ab", type=_datum, help="nur ab Datum JJJJ-MM-TT")
+ parser.add_argument("-f", "--format", default="text", choices=["text", "json"], help="text oder json")
+ parser.add_argument("--content-type", default="text/csv; charset=utf-8",
+ help="Kodierung der Datei als Content-Type")
+ a = parser.parse_args(argv)
+ if not Path(a.datei).exists():
+ sys.stderr.write(f"Datei nicht gefunden: {a.datei}\n")
return 3
- ab = None
- if optionen.ab:
- try:
- ab = datetime.datetime.strptime(optionen.ab, "%Y-%m-%d").date()
- except ValueError:
- parser.error("--ab braucht JJJJ-MM-TT")
- z = zusammenfassen(lade_eintraege(args[0], kodierung_aus(optionen.content_type)), optionen.projekt, ab)
- if optionen.format == "json":
- print(json.dumps(z, sort_keys=True))
- else:
- print(als_text(z))
+ z = zusammenfassen(lade_eintraege(a.datei, kodierung_aus(a.content_type)), a.projekt, a.ab)
+ print(json.dumps(z, sort_keys=True) if a.format == "json" else als_text(z))
return 0
So etwas für deinen Betrieb?
Beschreib kurz, was du brauchst. Du bekommst einen Festpreis, bevor ich anfange; ohne dein Ja passiert nichts.