HTTP ist Text auf einem Socket · Abschlussprojekt
Abschlussprojekt: Projekt: ein rohes fetch()
In vier Lektionen hast du fetch(method, target, *, connect) aus vier Teilen gebaut: make_request schreibt die Anfrage, read_head liest bis zur Leerzeile, parse_head macht aus dem Kopf einen Status und Header, und read_body liest genau den Body, oder nach HEAD keinen. Jedes Teil wurde für sich geprüft, und jeder Live-Lauf hat eine Antwort von practice-api gezeigt. Dieses Projekt prüft die ganze
Was fetch verspricht
Wenn du fertig bist, enthält wire.py ein fetch, das all das tut:
Es gibt für eine Antwort mit beliebigem Status ein
mit "version","status","reason","headers"und"body"zurück, für 404 genauso bereitwillig wie für 200;es löst
aus für einen fehlerhaften Kopf oder eine fehlerhafte Länge und für einen Body, den es noch nicht abmessen kann: eine Antwort ohne Content-Lengthoder mitTransfer-Encoding, die Kapitel 9 behandelt;es löst
ConnectionErroraus, wenn der Server schließt, bevor der Kopf oder der Body vollständig ist;es liest nie über das Ende des Bodys hinaus und liest nach
HEADkeinen Body;es hinterlässt die Verbindung auf jedem Weg geschlossen, was der
with-Block für dich erledigt, auch wenn darin eine Ausnahme ausgelöst wird.
Wenn du die Lektionen 3 bis 6 durchgearbeitet hast, tut dein fetch all das schon. Wenn Check my work eine Lücke findet, nennt es sie, und du behebst sie in wire.py. Die neue Arbeit in dieser Lektion sind die Tests.
Die Werkzeuge in fakes.py
Öffne fakes.py. Es ist gewöhnliches Python, für diesen Kurs geschrieben, und enthält zwei Funktionen, die deine Tests benutzen.
scripted_socket(chunks) gibt einen Ersatz-recv(size) gibt das nächste Stück des Skripts heraus, nie mehr als size Bytes auf einmal, und hebt den Rest eines langen Stücks für den nächsten Aufruf auf. Nach dem letzten Stück gibt recv b"" zurück, wie ein echter Socket, sobald der Server geschlossen hat. Ein None im Skript steht für einen Server, der verstummt ist, die Verbindung aber offen gehalten hat: recv löst dort TimeoutError aus, und genau das tut ein echter Socket mit einem Timeout. Jeder Aufruf von sendall wird in der sock.sent aufgezeichnet.
fake_connect(sock, calls) gibt eine Funktion zurück, die du fetch als connect übergibst. Wenn fetch sie aufruft, hängt sie die Adresse und das Timeout an deine Liste calls an und gibt sock zurück. Vorher verpackt sie sock in contextlib.nullcontext(sock), weil fetch with connect(...) as sock: schreibt und der Ersatz-Socket allein keinen with-Block beginnen kann: nullcontext übergibt sock an den Block und tut nichts, wenn der Block endet. Deine Pinnwand-Tests in Networks with Python haben denselben Trick benutzt.
tests/test_wire.py beginnt schon mit import wire, importiert beide Funktionen, definiert EXAMPLE_REPLY, eine erfundene Antwort in der Form von practice-api mit einem
Meilensteine
Schreibe die Tests in dieser Reihenfolge. Führe nach jedem python -m pytest -q aus.
Die ganze Antwort in einem Stück. Skripte
EXAMPLE_REPLYals ein Stück, rufewire.fetch("GET", "/v1/items/1", connect=fake_connect(sock, calls))auf und prüfe mit assert, was zurückkam: den Aufruf von connect (("practice-api", 8080)und2.0), die genauen Bytes insock.sent, den Status, den Headercontent-lengthund den Body.Jede Schnittposition. Eine
über jede Position ivon 1 bis zur Länge der Antwort baut ein Skript aus zwei Stücken,[EXAMPLE_REPLY[:i], EXAMPLE_REPLY[i:]], und prüft jedes Mal den Body. Das deckt einen Schnitt innerhalb der Statuszeile ab, innerhalb jeder, innerhalb der Leerzeile und innerhalb des Bodys. HEAD. Skripte nur den Kopf der Antwort, bis einschließlich der Leerzeile, gefolgt von
None, und rufefetchmit"HEAD"auf. Prüfe, dass der Bodyb""ist und dasscontent-lengthnoch da ist. DasNoneist wichtig: Es ist ein Server, der die Verbindung offen hält, also löst einfetch, das auf einen Body wartet oder darauf, dass der Server schließt,TimeoutErroraus, und der Test schlägt fehl.Beliebige Schreibweise. Skripte eine Antwort, deren Längen-Header
cOnTeNt-LeNgThgeschrieben ist, und prüfe, dass ihr Body zurückkommt.
Hier ist die Form von Meilenstein 2, ohne seine Zusicherungen:
def test_fetch_reads_a_reply_split_anywhere():
"""The reply may arrive in two pieces cut at any byte."""
for i in range(1, len(EXAMPLE_REPLY)):
sock = scripted_socket([EXAMPLE_REPLY[:i], EXAMPLE_REPLY[i:]])
response = wire.fetch("GET", "/v1/items/1", connect=fake_connect(sock, []))
Drei Regeln halten deine Tests prüfbar. Schreibe jeden Test als einfache test_-Funktion ohne wire.fetch(...) über das from wire import fetch oder fetch = wire.fetch: Check my work setzt über das Modul andere Versionen von fetch ein, um zu sehen, ob deine Tests es bemerken, und ein Name, der aus dem Modul herauskopiert wurde, behält das Original. Und lass nie einen Test das Netzwerk erreichen: Jeder Aufruf von fetch in deinen Tests übergibt connect=.
Die Tests ausführen
Im Tab Shell:
python -m pytest -q
Jeder Test sollte in deutlich unter einer Sekunde bestehen, weil kein Test auf ein Netzwerk wartet. Wenn einer hängt, erreicht eine Schleife in deinem Test oder in wire.py das recv nicht: Ein Skript kann nicht warten, weil es mit b"" endet oder bei None eine Ausnahme auslöst.
Wenn du möchtest, führe python wire.py noch einmal gegen practice-api aus. Es ist dieselbe Antwort wie in Lektion 5, jetzt von einer Funktion, die du gegen jede Art getestet hast, auf die die Antwort geschnitten werden könnte. Dieser Live-Lauf ist zum Anschauen da; Check my work benutzt ihn nicht, und deine Tests sind ein stärkerer Beleg als jeder einzelne Lauf, weil sie Schnitte abdecken, die ein Netzwerk vielleicht nie zufällig macht, während du zusiehst.
Drücke dann Check my work. fetch kann genau eine Art von Anfrage senden: eine Host und keinem anderen Header. Kapitel 2 baut Anfragen für jede
Aufgabe
Stelle fetch in wire.py fertig, falls etwas aus der tests/test_wire.py, wobei du den vorhandenen behältst:
eine Antwort in einem Stück: Prüfe den Aufruf von connect, die gesendeten Bytes, den Status, den Header
content-lengthund den Body;eine Antwort, die an jeder Position in zwei Teile geschnitten ist, wobei jedes Mal der Body geprüft wird;
eine Anfrage
HEAD, deren Skript nur den Kopf gefolgt vonNoneenthält, wobei geprüft wird, dass der Bodyb""ist undcontent-lengthnoch da ist;eine Antwort, deren Längen-Header in ungewöhnlicher Schreibweise geschrieben ist, wobei der Body geprüft wird.
Schreibe jeden Test als einfache test_-scripted_socket und fake_connect aus fakes.py und rufe in jedem Test wire.fetch(...) über das connect=. Schreibe nicht from wire import und speichere wire.fetch nicht unter einem eigenen Namen.
Führe im python -m pytest -q aus.
Drücke dann Check my work. Die Prüfung ruft zuerst dein fetch mit einem Ersatz für connect auf Antworten auf, die du nicht gesehen hast: Antworten mit beliebigem Status, Headernamen in ungewöhnlicher Schreibweise, ein wiederholter Header, ein Body mit Zeichen außerhalb von ASCII, eine Antwort, auf die der Anfang einer weiteren folgt, und eine Antwort auf HEAD, jede am Stück geliefert, ein Byte nach dem anderen, an jeder Position in zwei Teile geschnitten und in Stücken von 7 Bytes, mit einem Server, der schließt, und mit einem, der die Verbindung offen hält; danach fehlerhafte Statuszeilen, Content-Length, eine mit Transfer-Encoding, einen Kopf, der nie endet, eine Länge weit über der Grenze und Server, die zu früh schließen. Jedes Mal prüft sie, dass die Verbindung geschlossen wurde. Sie prüft außerdem die Bytes, die make_request schreibt. Dann führt sie deine Tests mit pytest aus, zuerst gegen dein eigenes wire.py, wo sie alle ohne Netzwerk bestehen müssen, und dann gegen vier kaputte Versionen von fetch: eine, die recv nur einmal aufruft, eine, die liest, bis der Server schließt, eine, die nach HEAD einen Body liest, und eine, die Content-Length nur in genau dieser Schreibweise nachschlägt. Deine Tests müssen gegen jede kaputte Version fehlschlagen. Sie liest dein Terminal nicht und nimmt keinen Kontakt mit practice-api auf.