From: Tom Christie Date: Tue, 30 Apr 2019 13:58:43 +0000 (+0100) Subject: Response.content X-Git-Tag: 0.3.0~60 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=4c5511313c792cc3eee4a300b40e9ff6f98ba57f;p=thirdparty%2Fhttpx.git Response.content --- diff --git a/httpcore/__init__.py b/httpcore/__init__.py index 1aa20f65..618e41d8 100644 --- a/httpcore/__init__.py +++ b/httpcore/__init__.py @@ -14,6 +14,7 @@ from .exceptions import ( RedirectBodyUnavailable, RedirectLoop, ResponseClosed, + ResponseNotRead, StreamConsumed, Timeout, TooManyRedirects, diff --git a/httpcore/dispatch/http11.py b/httpcore/dispatch/http11.py index 62cb2fac..d8e2740a 100644 --- a/httpcore/dispatch/http11.py +++ b/httpcore/dispatch/http11.py @@ -85,7 +85,7 @@ class HTTP11Connection(Adapter): reason_phrase=reason_phrase, protocol="HTTP/1.1", headers=headers, - body=body, + content=body, on_close=self.response_closed, request=request, ) diff --git a/httpcore/dispatch/http2.py b/httpcore/dispatch/http2.py index d44c5f60..4a63f90a 100644 --- a/httpcore/dispatch/http2.py +++ b/httpcore/dispatch/http2.py @@ -73,7 +73,7 @@ class HTTP2Connection(Adapter): status_code=status_code, protocol="HTTP/2", headers=headers, - body=body, + content=body, on_close=on_close, request=request, ) diff --git a/httpcore/exceptions.py b/httpcore/exceptions.py index 0b6efeec..bae6b08d 100644 --- a/httpcore/exceptions.py +++ b/httpcore/exceptions.py @@ -78,6 +78,13 @@ class StreamConsumed(Exception): """ +class ResponseNotRead(Exception): + """ + Attempted to access response content, without having called `read()` + after a streaming response. + """ + + class ResponseClosed(Exception): """ Attempted to read or stream response content, but the request has been @@ -91,6 +98,9 @@ class DecodingError(Exception): """ +# Other cases... + + class InvalidURL(Exception): """ URL was missing a hostname, or was not one of HTTP/HTTPS. diff --git a/httpcore/models.py b/httpcore/models.py index 5325b16b..b2837a12 100644 --- a/httpcore/models.py +++ b/httpcore/models.py @@ -9,7 +9,7 @@ from .decoders import ( IdentityDecoder, MultiDecoder, ) -from .exceptions import ResponseClosed, StreamConsumed +from .exceptions import ResponseClosed, ResponseNotRead, StreamConsumed from .status_codes import codes from .utils import get_reason_phrase, normalize_header_key, normalize_header_value @@ -197,7 +197,9 @@ class Headers(typing.MutableMapping[str, str]): except KeyError: return default - def getlist(self, key: str, default: typing.Any = None, split_commas = None) -> typing.List[str]: + def getlist( + self, key: str, split_commas: bool=False + ) -> typing.List[str]: """ Return multiple header values. @@ -208,7 +210,7 @@ class Headers(typing.MutableMapping[str, str]): """ get_header_key = key.lower().encode(self.encoding) if split_commas is None: - split_commas = get_header_key != b'set-cookie' + split_commas = get_header_key != b"set-cookie" values = [ item_value.decode(self.encoding) @@ -216,9 +218,6 @@ class Headers(typing.MutableMapping[str, str]): if item_key == get_header_key ] - if not values: - return [] if default is None else default - if not split_commas: return values @@ -352,6 +351,13 @@ class Request: yield self.body def prepare(self) -> None: + """ + Adds in any default headers. When using the `Client`, this will + end up being called into by the `prepare_request()` stage. + + You can omit this behavior by calling `Client.send()` with an + explicitly built `Request` instance. + """ auto_headers = [] # type: typing.List[typing.Tuple[bytes, bytes]] has_host = "host" in self.headers @@ -383,28 +389,26 @@ class Response: reason_phrase: str = None, protocol: str = None, headers: HeaderTypes = None, - body: BodyTypes = b"", + content: BodyTypes = b"", on_close: typing.Callable = None, request: Request = None, history: typing.List["Response"] = None, ): self.status_code = status_code - if reason_phrase is None: - self.reason_phrase = get_reason_phrase(status_code) - else: - self.reason_phrase = reason_phrase + self.reason_phrase = reason_phrase or get_reason_phrase(status_code) self.protocol = protocol self.headers = Headers(headers) - self.on_close = on_close - self.is_closed = False - self.is_streamed = False - if isinstance(body, bytes): + if isinstance(content, bytes): self.is_closed = True - self.body = self.decoder.decode(body) + self.decoder.flush() + self.is_stream_consumed = True + self._raw_content = content else: - self.body_aiter = body + self.is_closed = False + self.is_stream_consumed = False + self._raw_stream = content + self.on_close = on_close self.request = request self.history = [] if history is None else list(history) self.next = None # typing.Optional[typing.Callable] @@ -418,6 +422,17 @@ class Response: """ return None if self.request is None else self.request.url + @property + def content(self) -> bytes: + if not hasattr(self, "_content"): + if hasattr(self, "_raw_content"): + self._content = ( + self.decoder.decode(self._raw_content) + self.decoder.flush() + ) + else: + raise ResponseNotRead() + return self._content + @property def decoder(self) -> Decoder: """ @@ -426,7 +441,7 @@ class Response: """ if not hasattr(self, "_decoder"): decoders = [] # type: typing.List[Decoder] - values = self.headers.getlist("content-encoding", ["identity"]) + values = self.headers.getlist("content-encoding", split_commas=True) for value in values: value = value.strip().lower() decoder_cls = SUPPORTED_DECODERS[value] @@ -445,20 +460,20 @@ class Response: """ Read and return the response content. """ - if not hasattr(self, "body"): - body = b"" + if not hasattr(self, "_content"): + content = b"" async for part in self.stream(): - body += part - self.body = body - return self.body + content += part + self._content = content + return self._content async def stream(self) -> typing.AsyncIterator[bytes]: """ A byte-iterator over the decoded response content. This allows us to handle gzip, deflate, and brotli encoded responses. """ - if hasattr(self, "body"): - yield self.body + if hasattr(self, "_content"): + yield self._content else: async for chunk in self.raw(): yield self.decoder.decode(chunk) @@ -468,14 +483,18 @@ class Response: """ A byte-iterator over the raw response content. """ - if self.is_streamed: - raise StreamConsumed() - if self.is_closed: - raise ResponseClosed() - self.is_streamed = True - async for part in self.body_aiter: - yield part - await self.close() + if hasattr(self, "_raw_content"): + yield self._raw_content + else: + if self.is_stream_consumed: + raise StreamConsumed() + if self.is_closed: + raise ResponseClosed() + + self.is_stream_consumed = True + async for part in self._raw_stream: + yield part + await self.close() async def close(self) -> None: """ diff --git a/httpcore/sync.py b/httpcore/sync.py index 907dc80b..e87c0ef7 100644 --- a/httpcore/sync.py +++ b/httpcore/sync.py @@ -26,8 +26,8 @@ class SyncResponse: return self._response.headers @property - def body(self) -> bytes: - return self._response.body + def content(self) -> bytes: + return self._response.content def read(self) -> bytes: return self._loop.run_until_complete(self._response.read()) diff --git a/tests/adapters/test_redirects.py b/tests/adapters/test_redirects.py index 97a5393e..4e44a1ee 100644 --- a/tests/adapters/test_redirects.py +++ b/tests/adapters/test_redirects.py @@ -66,7 +66,7 @@ class MockDispatch(Adapter): elif request.url.path == "/cross_domain_target": headers = dict(request.headers.items()) body = json.dumps({"headers": headers}).encode() - return Response(codes.ok, body=body, request=request) + return Response(codes.ok, content=body, request=request) elif request.url.path == "/redirect_body": body = await request.read() @@ -76,9 +76,9 @@ class MockDispatch(Adapter): elif request.url.path == "/redirect_body_target": body = await request.read() body = json.dumps({"body": body.decode()}).encode() - return Response(codes.ok, body=body, request=request) + return Response(codes.ok, content=body, request=request) - return Response(codes.ok, body=b"Hello, world!", request=request) + return Response(codes.ok, content=b"Hello, world!", request=request) @pytest.mark.asyncio @@ -202,7 +202,7 @@ async def test_cross_domain_redirect(): url = "https://example.com/cross_domain" headers = {"Authorization": "abc"} response = await client.request("GET", url, headers=headers) - data = json.loads(response.body.decode()) + data = json.loads(response.content.decode()) assert response.url == URL("https://example.org/cross_domain_target") assert data == {"headers": {}} @@ -213,7 +213,7 @@ async def test_same_domain_redirect(): url = "https://example.org/cross_domain" headers = {"Authorization": "abc"} response = await client.request("GET", url, headers=headers) - data = json.loads(response.body.decode()) + data = json.loads(response.content.decode()) assert response.url == URL("https://example.org/cross_domain_target") assert data == {"headers": {"authorization": "abc"}} @@ -224,7 +224,7 @@ async def test_body_redirect(): url = "https://example.org/redirect_body" body = b"Example request body" response = await client.request("POST", url, body=body) - data = json.loads(response.body.decode()) + data = json.loads(response.content.decode()) assert response.url == URL("https://example.org/redirect_body_target") assert data == {"body": "Example request body"} diff --git a/tests/dispatch/test_connections.py b/tests/dispatch/test_connections.py index 11031106..cf1192f2 100644 --- a/tests/dispatch/test_connections.py +++ b/tests/dispatch/test_connections.py @@ -8,7 +8,7 @@ async def test_get(server): http = httpcore.HTTPConnection(origin="http://127.0.0.1:8000/") response = await http.request("GET", "http://127.0.0.1:8000/") assert response.status_code == 200 - assert response.body == b"Hello, world!" + assert response.content == b"Hello, world!" @pytest.mark.asyncio diff --git a/tests/dispatch/test_http2.py b/tests/dispatch/test_http2.py index dc287bce..7cc3ebbf 100644 --- a/tests/dispatch/test_http2.py +++ b/tests/dispatch/test_http2.py @@ -82,7 +82,7 @@ async def test_http2_get_request(): async with httpcore.HTTP2Connection(reader=server, writer=server) as conn: response = await conn.request("GET", "http://example.org") assert response.status_code == 200 - assert json.loads(response.body) == {"method": "GET", "path": "/", "body": ""} + assert json.loads(response.content) == {"method": "GET", "path": "/", "body": ""} @pytest.mark.asyncio @@ -91,7 +91,7 @@ async def test_http2_post_request(): async with httpcore.HTTP2Connection(reader=server, writer=server) as conn: response = await conn.request("POST", "http://example.org", body=b"") assert response.status_code == 200 - assert json.loads(response.body) == { + assert json.loads(response.content) == { "method": "POST", "path": "/", "body": "", @@ -107,10 +107,10 @@ async def test_http2_multiple_requests(): response_3 = await conn.request("GET", "http://example.org/3") assert response_1.status_code == 200 - assert json.loads(response_1.body) == {"method": "GET", "path": "/1", "body": ""} + assert json.loads(response_1.content) == {"method": "GET", "path": "/1", "body": ""} assert response_2.status_code == 200 - assert json.loads(response_2.body) == {"method": "GET", "path": "/2", "body": ""} + assert json.loads(response_2.content) == {"method": "GET", "path": "/2", "body": ""} assert response_3.status_code == 200 - assert json.loads(response_3.body) == {"method": "GET", "path": "/3", "body": ""} + assert json.loads(response_3.content) == {"method": "GET", "path": "/3", "body": ""} diff --git a/tests/models/test_headers.py b/tests/models/test_headers.py index b7995e3d..8b7556bc 100644 --- a/tests/models/test_headers.py +++ b/tests/models/test_headers.py @@ -147,8 +147,8 @@ def test_multiple_headers(): """ Most headers should split by commas for `getlist`, except 'Set-Cookie'. """ - h = httpcore.Headers([('set-cookie', 'a, b'), ('set-cookie', 'c')]) - h.getlist('Set-Cookie') == ['a, b', 'b'] + h = httpcore.Headers([("set-cookie", "a, b"), ("set-cookie", "c")]) + h.getlist("Set-Cookie") == ["a, b", "b"] - h = httpcore.Headers([('vary', 'a, b'), ('vary', 'c')]) - h.getlist('Vary') == ['a', 'b', 'c'] + h = httpcore.Headers([("vary", "a, b"), ("vary", "c")]) + h.getlist("Vary") == ["a", "b", "c"] diff --git a/tests/models/test_responses.py b/tests/models/test_responses.py index 9bd3af80..4cc340b6 100644 --- a/tests/models/test_responses.py +++ b/tests/models/test_responses.py @@ -9,50 +9,49 @@ async def streaming_body(): def test_response(): - response = httpcore.Response(200, body=b"Hello, world!") + response = httpcore.Response(200, content=b"Hello, world!") assert response.status_code == 200 assert response.reason_phrase == "OK" - assert response.body == b"Hello, world!" + assert response.content == b"Hello, world!" assert response.is_closed @pytest.mark.asyncio async def test_read_response(): - response = httpcore.Response(200, body=b"Hello, world!") + response = httpcore.Response(200, content=b"Hello, world!") assert response.status_code == 200 - assert response.body == b"Hello, world!" + assert response.content == b"Hello, world!" assert response.is_closed - body = await response.read() + content = await response.read() - assert body == b"Hello, world!" - assert response.body == b"Hello, world!" + assert content == b"Hello, world!" + assert response.content == b"Hello, world!" assert response.is_closed @pytest.mark.asyncio async def test_streaming_response(): - response = httpcore.Response(200, body=streaming_body()) + response = httpcore.Response(200, content=streaming_body()) assert response.status_code == 200 - assert not hasattr(response, "body") assert not response.is_closed - body = await response.read() + content = await response.read() - assert body == b"Hello, world!" - assert response.body == b"Hello, world!" + assert content == b"Hello, world!" + assert response.content == b"Hello, world!" assert response.is_closed @pytest.mark.asyncio async def test_cannot_read_after_stream_consumed(): - response = httpcore.Response(200, body=streaming_body()) + response = httpcore.Response(200, content=streaming_body()) - body = b"" + content = b"" async for part in response.stream(): - body += part + content += part with pytest.raises(httpcore.StreamConsumed): await response.read() @@ -60,7 +59,7 @@ async def test_cannot_read_after_stream_consumed(): @pytest.mark.asyncio async def test_cannot_read_after_response_closed(): - response = httpcore.Response(200, body=streaming_body()) + response = httpcore.Response(200, content=streaming_body()) await response.close() diff --git a/tests/test_client.py b/tests/test_client.py index 09711f75..8933a83e 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -9,7 +9,7 @@ async def test_get(server): async with httpcore.Client() as client: response = await client.get(url) assert response.status_code == 200 - assert response.body == b"Hello, world!" + assert response.content == b"Hello, world!" @pytest.mark.asyncio @@ -25,9 +25,18 @@ async def test_stream_response(server): async with httpcore.Client() as client: response = await client.request("GET", "http://127.0.0.1:8000/", stream=True) assert response.status_code == 200 - assert not hasattr(response, "body") body = await response.read() assert body == b"Hello, world!" + assert response.content == b"Hello, world!" + + +@pytest.mark.asyncio +async def test_access_content_stream_response(server): + async with httpcore.Client() as client: + response = await client.request("GET", "http://127.0.0.1:8000/", stream=True) + assert response.status_code == 200 + with pytest.raises(httpcore.ResponseNotRead): + response.content @pytest.mark.asyncio diff --git a/tests/test_decoders.py b/tests/test_decoders.py index 0ed7e668..20273eec 100644 --- a/tests/test_decoders.py +++ b/tests/test_decoders.py @@ -12,8 +12,8 @@ def test_deflate(): compressed_body = compressor.compress(body) + compressor.flush() headers = [(b"Content-Encoding", b"deflate")] - response = httpcore.Response(200, headers=headers, body=compressed_body) - assert response.body == body + response = httpcore.Response(200, headers=headers, content=compressed_body) + assert response.content == body def test_gzip(): @@ -22,8 +22,8 @@ def test_gzip(): compressed_body = compressor.compress(body) + compressor.flush() headers = [(b"Content-Encoding", b"gzip")] - response = httpcore.Response(200, headers=headers, body=compressed_body) - assert response.body == body + response = httpcore.Response(200, headers=headers, content=compressed_body) + assert response.content == body def test_brotli(): @@ -31,8 +31,8 @@ def test_brotli(): compressed_body = brotli.compress(body) headers = [(b"Content-Encoding", b"br")] - response = httpcore.Response(200, headers=headers, body=compressed_body) - assert response.body == body + response = httpcore.Response(200, headers=headers, content=compressed_body) + assert response.content == body def test_multi(): @@ -47,8 +47,8 @@ def test_multi(): ) headers = [(b"Content-Encoding", b"deflate, gzip")] - response = httpcore.Response(200, headers=headers, body=compressed_body) - assert response.body == body + response = httpcore.Response(200, headers=headers, content=compressed_body) + assert response.content == body def test_multi_with_identity(): @@ -56,12 +56,12 @@ def test_multi_with_identity(): compressed_body = brotli.compress(body) headers = [(b"Content-Encoding", b"br, identity")] - response = httpcore.Response(200, headers=headers, body=compressed_body) - assert response.body == body + response = httpcore.Response(200, headers=headers, content=compressed_body) + assert response.content == body headers = [(b"Content-Encoding", b"identity, br")] - response = httpcore.Response(200, headers=headers, body=compressed_body) - assert response.body == body + response = httpcore.Response(200, headers=headers, content=compressed_body) + assert response.content == body @pytest.mark.asyncio @@ -74,7 +74,7 @@ async def test_streaming(): yield compressor.flush() headers = [(b"Content-Encoding", b"gzip")] - response = httpcore.Response(200, headers=headers, body=compress(body)) + response = httpcore.Response(200, headers=headers, content=compress(body)) assert not hasattr(response, "body") assert await response.read() == body @@ -85,4 +85,5 @@ def test_decoding_errors(header_value): body = b"test 123" compressed_body = brotli.compress(body)[3:] with pytest.raises(httpcore.exceptions.DecodingError): - response = httpcore.Response(200, headers=headers, body=compressed_body) + response = httpcore.Response(200, headers=headers, content=compressed_body) + response.content diff --git a/tests/test_sync.py b/tests/test_sync.py index 5879d650..bb3e1cbd 100644 --- a/tests/test_sync.py +++ b/tests/test_sync.py @@ -28,7 +28,7 @@ def test_get(server): with httpcore.SyncConnectionPool() as http: response = http.request("GET", "http://127.0.0.1:8000/") assert response.status_code == 200 - assert response.body == b"Hello, world!" + assert response.content == b"Hello, world!" @threadpool @@ -43,9 +43,8 @@ def test_stream_response(server): with httpcore.SyncConnectionPool() as http: response = http.request("GET", "http://127.0.0.1:8000/", stream=True) assert response.status_code == 200 - assert not hasattr(response, "body") - body = response.read() - assert body == b"Hello, world!" + content = response.read() + assert content == b"Hello, world!" @threadpool @@ -53,7 +52,7 @@ def test_stream_iterator(server): with httpcore.SyncConnectionPool() as http: response = http.request("GET", "http://127.0.0.1:8000/", stream=True) assert response.status_code == 200 - body = b"" + content = b"" for chunk in response.stream(): - body += chunk - assert body == b"Hello, world!" + content += chunk + assert content == b"Hello, world!"