HTTP is tekst op een socket · eindproject
Eindproject: Project: een kale fetch()
In vier lessen bouwde je fetch(method, target, *, connect) uit vier onderdelen: make_request schrijft het verzoek, read_head leest tot aan de lege regel, parse_head maakt van de kop een status en headers, en read_body leest precies de body, of geen na HEAD. Elk onderdeel werd los gecontroleerd, en elke live uitvoering liet één antwoord van practice-api zien. Dit project bewijst de hele
Wat fetch belooft
Als je klaar bent, bevat wire.py een fetch die dit allemaal doet:
geeft een
terug met "version","status","reason","headers"en"body"voor een antwoord met elke status, 404 net zo goed als 200;werpt
op voor een misvormde kop of lengte, en voor een body die het nog niet kan afmeten: een antwoord zonder Content-Length, of metTransfer-Encoding, dat hoofdstuk 9 afhandelt;werpt
ConnectionErrorop als de server sluit voordat de kop of de body compleet is;leest nooit voorbij het einde van de body, en leest geen body na
HEAD;laat de verbinding op elk pad gesloten achter, wat het
with-blok voor je doet, ook als er daarbinnen een exception wordt opgeworpen.
Als je les 3 tot en met 6 hebt gevolgd, doet je fetch dit allemaal al. Vindt Check my work een gat, dan noemt het dat, en je herstelt het in wire.py. Het nieuwe werk in deze les zijn de tests.
De hulpmiddelen in fakes.py
Open fakes.py. Het is gewone Python, geschreven voor deze cursus, en het bevat twee functies die je tests gebruiken.
scripted_socket(chunks) geeft een vervangende recv(size) geeft het volgende stuk van het script, nooit meer dan size bytes tegelijk, en bewaart de rest van een lang stuk voor de volgende aanroep. Na het laatste stuk geeft recv b"" terug, zoals een echte socket doet zodra de server heeft gesloten. Een None in het script staat voor een server die stil is geworden maar de verbinding heeft opengehouden: daar werpt recv TimeoutError op, en dat is wat een echte socket met een time-out doet. Elke aanroep van sendall wordt vastgelegd in de sock.sent.
fake_connect(sock, calls) geeft een functie terug die je als connect aan fetch meegeeft. Als fetch hem aanroept, voegt hij het adres en de time-out toe aan je lijst calls en geeft hij sock terug. Hij verpakt sock eerst in contextlib.nullcontext(sock), omdat fetch with connect(...) as sock: schrijft en de vervangende socket zelf geen with-blok kan beginnen: nullcontext geeft sock aan het blok en doet niets als het blok eindigt. Je prikbordtests in Networks with Python gebruikten dezelfde truc.
tests/test_wire.py begint al met import wire, importeert beide functies, definieert EXAMPLE_REPLY, een verzonnen antwoord in de vorm van practice-api met een
Mijlpalen
Schrijf de tests in deze volgorde. Voer na elke test python -m pytest -q uit.
Het hele antwoord in één stuk. Script
EXAMPLE_REPLYals één stuk, roepwire.fetch("GET", "/v1/items/1", connect=fake_connect(sock, calls))aan, en controleer met asserts wat er terugkwam: de connect-aanroep (("practice-api", 8080)en2.0), de exacte bytes insock.sent, de status, de headercontent-lengthen de body.Elke knippositie. Een
over elke positie ivan 1 tot de lengte van het antwoord bouwt een script van twee stukken,[EXAMPLE_REPLY[:i], EXAMPLE_REPLY[i:]], en controleert elke keer de body. Dat dekt een knip binnen de statusregel, binnen elke, binnen de lege regel en binnen de body. HEAD. Script alleen de kop van het antwoord, tot en met de lege regel, gevolgd door
None, en roepfetchaan met"HEAD". Controleer dat de bodyb""is en datcontent-lengther nog staat. DeNonedoet ertoe: het is een server die de verbinding openhoudt, dus eenfetchdie op een body wacht, of op het sluiten door de server, werptTimeoutErrorop en de test mislukt.Elke schrijfwijze. Script een antwoord waarvan de lengteheader als
cOnTeNt-LeNgThis geschreven, en controleer dat de body terugkomt.
Hier is de vorm van mijlpaal 2, zonder de asserts:
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, []))
Drie regels houden je tests controleerbaar. Schrijf elke test als een gewone test_-functie zonder wire.fetch(...) binnen elke test via de from wire import fetch of fetch = wire.fetch: Check my work zet via de module andere versies van fetch in om te zien of je tests dat merken, en een naam die uit de module is gekopieerd, houdt het origineel vast. En laat een test nooit het netwerk bereiken: elke aanroep van fetch in je tests geeft connect= mee.
De tests uitvoeren
Vanuit het tabblad Shell:
python -m pytest -q
Elke test zou ruim binnen een seconde moeten slagen, omdat geen enkele test op een netwerk wacht. Blijft er een hangen, dan bereikt een lus in je test of in wire.py recv niet: een script kan niet wachten, omdat het eindigt op b"" of bij None een fout opwerpt.
Als je wilt, voer python wire.py dan nog eens uit tegen practice-api. Het is hetzelfde antwoord als in les 5, nu van een functie die je hebt getest tegen elke manier waarop het antwoord geknipt kon worden. Die live uitvoering is om naar te kijken; Check my work gebruikt hem niet, en je tests zijn sterker bewijs dan welke uitvoering ook, omdat ze knippen dekken die een netwerk misschien nooit toevallig maakt terwijl jij kijkt.
Druk daarna op Check my work. fetch kan precies één soort verzoek versturen: een Host-regel en geen andere header. Hoofdstuk 2 bouwt verzoeken voor elke
Opdracht
Maak fetch in wire.py af als er iets uit de tests/test_wire.py, waarbij je de test die er al staat houdt:
een antwoord in één stuk: controleer de connect-aanroep, de verstuurde bytes, de status, de header
content-lengthen de body;een antwoord dat op elke positie in tweeën is geknipt, waarbij je elke keer de body controleert;
een
HEAD-verzoek waarvan het script alleen de kop bevat, gevolgd doorNone, waarbij je controleert dat de bodyb""is en datcontent-lengther nog staat;een antwoord waarvan de lengteheader in een ongebruikelijke schrijfwijze is geschreven, waarbij je de body controleert.
Schrijf elke test als een gewone test_-scripted_socket en fake_connect uit fakes.py, en roep wire.fetch(...) binnen elke test via de connect=. Schrijf geen from wire import en sla wire.fetch niet op onder een eigen naam.
Voer in de python -m pytest -q uit.
Druk daarna op Check my work. De controle roept eerst je fetch aan met een vervangende connect op antwoorden die je niet hebt gezien: antwoorden met elke status, headernamen in ongebruikelijke schrijfwijzen, een herhaalde header, een body met tekens buiten ASCII, een antwoord gevolgd door het begin van een volgend, en een antwoord op HEAD, elk in zijn geheel geleverd, één byte tegelijk, op elke positie in tweeën geknipt en in stukken van 7 bytes, met een server die sluit en met een die de verbinding openhoudt; daarna misvormde statusregels, Content-Length, een met Transfer-Encoding, een kop die nooit eindigt, een lengte ver boven de limiet, en servers die te vroeg sluiten. Elke keer controleert ze dat de verbinding gesloten werd. Ze controleert ook de bytes die make_request schrijft. Daarna voert ze je tests uit met pytest, eerst tegen je eigen wire.py, waar ze allemaal moeten slagen zonder netwerk, en daarna tegen vier kapotte versies van fetch: een die recv maar één keer aanroept, een die leest totdat de server sluit, een die na HEAD een body leest, en een die Content-Length alleen in precies die schrijfwijze opzoekt. Je tests moeten tegen elke kapotte versie mislukken. Ze leest je terminal niet en neemt geen contact op met practice-api.