0%

Abschlussprojekt: Ein Werkzeug für Übungsberichte · Übung

Über die Seite berichten und das Ergebnis testen

Dein Werkzeug kann jetzt eine Seite aus drei verschiedenen Quellen finden. Was es noch nicht kann, ist etwas darüber sagen. Diese Lektion macht aus einer validierten Seite den Text, den eine Person tatsächlich liest.

Die Arbeit teilt sich in zwei Teile. Sie getrennt zu halten macht den größten Teil dieser Lektion aus.

Zählen ist nicht Ausgeben

summarize_page(page_document) beantwortet Fragen zu den Daten. Wie viele Artikel gibt es? Welche Summe ergeben ihre Werte? Wie sieht das nach Kategorien aufgeschlüsselt aus?

Es gibt Daten zurück, keinen Text:

{
    "item_count": 3,
    "value_total": 60,
    "categories": [
        {"category": "color", "item_count": 2, "value_total": 30},
        {"category": "shape", "item_count": 1, "value_total": 30},
    ],
}

render_report(...) nimmt eine Seite entgegen und erzeugt den fertigen String. Es rechnet nicht selbst, sondern fragt summarize_page und stellt die Antwort dar.

Warum die Trennung? Weil sich beides aus unterschiedlichen Gründen ändert. Du wirst die Formulierungen des Berichts viel häufiger anpassen wollen als die Bedeutung von „Gesamtwert“. Eine Funktion, die beides erledigt, macht jede Textänderung zu einer Gelegenheit, die Berechnung zu beschädigen. Außerdem kannst du so das Zählen mit einfachen Dictionary-Vergleichen testen, statt Zahlen aus einem Absatz herauszusuchen.

Sortiere die Kategorien immer

Kategorien stammen aus einem Dictionary, das du beim Durchlaufen der Artikel aufgebaut hast. Ihre Reihenfolge entspricht deshalb der Reihenfolge ihres ersten Auftretens. Zwei Läufe über dieselben Daten in anderer Reihenfolge erzeugen Berichte, die sich nur in der Reihenfolge ihrer Zeilen unterscheiden.

Das ist ungünstig für etwas, das Menschen vergleichen. Sortiere nach Kategorienamen, und der Bericht wird stabil genug für einen Vergleich mit dem von gestern.

Die Artikel selbst behalten die Reihenfolge der API bei, denn diese Reihenfolge gehört zu den Daten. Die Kategorien sind deine eigene Gruppierung; über ihre Reihenfolge entscheidest du.

Mache die Namen eindeutig

Eine Kategorie ist Text aus einem Dienst. Text kann ein Komma, ein Anführungszeichen oder einen Zeilenumbruch enthalten. All das kann aus einer ordentlichen Zeile eine verwirrende machen.

name = json.dumps(category["category"], ensure_ascii=False)

json.dumps gibt einen String in Anführungszeichen und mit maskierten Sonderzeichen zurück: "color" bleibt lesbar, und ein Name mit einem Zeilenumbruch erscheint als ein einziges eindeutiges Token, statt deinen Bericht still auf zwei Zeilen aufzuteilen. ensure_ascii=False hält echte Nicht-ASCII-Zeichen lesbar, statt café in caf\u00e9 zu verwandeln.

Das kostet einen Funktionsaufruf und beseitigt eine ganze Gruppe verwirrender Fehler.

Das genaue Format

Practice API report
source: network
fetched_at: 1754000000
page: 1
page_size: 3
dataset_total: 5
item_count: 3
value_total: 60
categories:
- "color": 2 item(s), 30 value
- "shape": 1 item(s), 30 value

source ist einer der Werte network, fresh-cache, stale-cache oder offline. So zeigt der Bericht, woher seine Daten kommen. Das ist wichtig: Wer ihn liest, sollte wissen, ob die Antwort frisch abgerufen wurde oder bereits eine Stunde alt ist.

fetched_at ist der Zeitstempel für jede Quelle außer offline. Diese hat keinen und gibt none aus. Ein Offline-Bericht hat keinen Abrufzeitpunkt; einen zu erfinden wäre eine falsche Angabe in einer Datei, die Menschen aufbewahren.

Wie immer genau ein abschließender Zeilenumbruch.

Halte die Funktion frei von Nebenwirkungen

render_report nimmt Werte entgegen und gibt einen String zurück. Es öffnet keine Dateien, liest nicht die Uhr und sendet keine Anfragen.

Dadurch kann die CLI aus Kapitel 13 den ganzen Bericht erzeugen, bevor sie die Zieldatei öffnet. Deshalb bleibt bei einem Fehler während der Texterzeugung dein bisheriger Bericht erhalten. Die Regel „Erledige die Arbeit, die fehlschlagen kann, vor dem Schritt, der etwas zerstört“ aus Lektion 1 ergibt sich jetzt aus deiner Entwurfsentscheidung an dieser Stelle.

Jetzt bist du dran

Zwei Aufgaben, die zusammengehören.

Ergänze zuerst summarize_page(page_document) und render_report(page_document, *, source, fetched_at) in practice_report.py nach dem oben gezeigten Format. Beide verwenden die berichtsspezifische Seitenvalidierung und lassen die Eingabe unverändert. Übergib source und fetched_at beim Aufruf der Berichtsfunktion mit Namen.

Die Berichtsfunktion weist eine unbekannte Quelle mit ValueError zurück. Verlange bei source="offline" den Wert fetched_at=None und gib das Wort none aus. Verlange für jede andere Quelle einen echten nicht negativen ganzzahligen Zeitstempel; weise Wahrheitswerte und andere ungültige Werte mit ValueError zurück. Eine leere Seite hat null Artikel, Gesamtwert null und eine leere Kategorienliste. Ihr Bericht endet deshalb direkt nach categories: und dem letzten Zeilenumbruch.

Schreibe anschließend tests/test_practice_report.py. Decke darin nicht nur den Bericht ab, sondern alles, was dieses Kapitel bisher aufgebaut hat. Auf dieses Modul stützt sich das Abschlussprojekt; es verdient also mehr als einen oberflächlichen Erfolgsfall:

  • Cache-Validierung: Ein gutes Dokument übersteht Schreiben und Lesen; ein Boolean als fetched_at wird abgelehnt; ein Cache für Seite 2 wird bei einer Anfrage nach Seite 1 abgelehnt.

  • Aktualität: Ein Alter gleich max_age ist aktuell, eine Sekunde mehr ist veraltet, ein zukünftiger Zeitstempel löst eine Ausnahme aus.

  • Quellenauswahl: Ein aktueller Cache sendet keine Anfrage; ein fehlender Cache ruft einmal ab; --refresh greift nicht auf den Cache zurück; ein TypeError aus dem Abruf wird nicht verschluckt.

  • Bericht: Kategorien sind sortiert, eine leere Seite wird behandelt, offline gibt fetched_at: none aus, genau ein abschließender Zeilenumbruch ist vorhanden.

Verwende das Fake-Session-Muster aus Kapitel 11 für alles, was sonst das Netzwerk erreichen würde. Übergib überall now_epoch, statt die Uhr zu lesen, und halte jede von deinen Tests erzeugte Datei unter tmp_path.

Strebe Tests an, die einen realistischen Fehler bemerken würden. Frage bei jedem nicht „Besteht er?“, sondern „Was müsste kaputtgehen, damit er fehlschlägt?“ Wenn du das nicht beantworten kannst, ist der Test Dekoration.

Warum gibt summarize_page ein Dictionary statt formatiertem Text zurück?

Warum sortierst du die Kategoriezeilen nach Namen?

Warum darf render_report keine Dateien öffnen oder die Uhr lesen?

Aufgabe

Implementiere summarize_page und render_report in practice_report.py und decke beide anschließend in tests/test_practice_report.py ab: sortierte Kategorien, eine leere Seite, offline mit der Darstellung fetched_at: none, ein Kategoriename mit Komma und genau ein abschließender Zeilenumbruch.