diff --git a/src/ahttpx/_client.py b/src/ahttpx/_client.py index 64c3947..07be4b2 100644 --- a/src/ahttpx/_client.py +++ b/src/ahttpx/_client.py @@ -138,20 +138,34 @@ class RedirectMiddleware(Transport): def __init__(self, transport: Transport) -> None: self._transport = transport - def is_redirect(self, response: Response) -> bool: - return ( - response.status_code in (301, 302, 303, 307, 308) - and "Location" in response.headers - ) + def build_redirect_request(self, request: Request, response: Response) -> Request | None: + # Redirect status codes... + if response.status_code not in (301, 302, 303, 307, 308): + return None + + # Redirects need a valid location header... + try: + location = URL(response.headers['Location']) + except (KeyError, ValueError): + return None - def build_redirect_request(self, request: Request, response: Response) -> Request: - raise NotImplementedError() + # Instantiate a redirect request... + method = request.method + url = request.url.join(location) + headers = request.headers + content = request.content + + return Request(method, url, headers, content) async def send(self, request: Request) -> Response: while True: response = await self._transport.send(request) - if not self.is_redirect(response): + # Determine if we have a redirect or not. + redirect = self.build_redirect_request(request, response) + + # If we don't have a redirect, we're done. + if redirect is None: return response # If we have a redirect, then we read the body of the response. @@ -160,8 +174,8 @@ async def send(self, request: Request) -> Response: async with response as stream: await stream.read() - # We've made a request-response and now need to issue a redirect request. - request = self.build_redirect_request(request, response) + # Make the next request + request = redirect async def close(self): pass diff --git a/src/ahttpx/_content.py b/src/ahttpx/_content.py index 0a6ebc2..73e32b6 100644 --- a/src/ahttpx/_content.py +++ b/src/ahttpx/_content.py @@ -1,3 +1,4 @@ +import copy import json import os import typing @@ -339,8 +340,26 @@ async def parse(self, stream: Stream) -> 'Content': data = json.loads(source) return JSON(data, source) + # Return the underlying data. Copied to ensure immutability. + @property + def value(self) -> typing.Any: + return copy.deepcopy(self._data) + + # dict and list style accessors, eg. for casting. + def keys(self) -> typing.KeysView[str]: + return self._data.keys() + + def __len__(self) -> int: + return len(self._data) + def __getitem__(self, key: typing.Any) -> typing.Any: - return self._data[key] + return copy.deepcopy(self._data[key]) + + # Built-ins. + def __eq__(self, other: typing.Any) -> bool: + if isinstance(other, JSON): + return self._data == other._data + return self._data == other def __str__(self) -> str: return self._content.decode('utf-8') diff --git a/src/httpx/_client.py b/src/httpx/_client.py index 8d87b9f..1654b66 100644 --- a/src/httpx/_client.py +++ b/src/httpx/_client.py @@ -138,20 +138,34 @@ class RedirectMiddleware(Transport): def __init__(self, transport: Transport) -> None: self._transport = transport - def is_redirect(self, response: Response) -> bool: - return ( - response.status_code in (301, 302, 303, 307, 308) - and "Location" in response.headers - ) + def build_redirect_request(self, request: Request, response: Response) -> Request | None: + # Redirect status codes... + if response.status_code not in (301, 302, 303, 307, 308): + return None + + # Redirects need a valid location header... + try: + location = URL(response.headers['Location']) + except (KeyError, ValueError): + return None - def build_redirect_request(self, request: Request, response: Response) -> Request: - raise NotImplementedError() + # Instantiate a redirect request... + method = request.method + url = request.url.join(location) + headers = request.headers + content = request.content + + return Request(method, url, headers, content) def send(self, request: Request) -> Response: while True: response = self._transport.send(request) - if not self.is_redirect(response): + # Determine if we have a redirect or not. + redirect = self.build_redirect_request(request, response) + + # If we don't have a redirect, we're done. + if redirect is None: return response # If we have a redirect, then we read the body of the response. @@ -160,8 +174,8 @@ def send(self, request: Request) -> Response: with response as stream: stream.read() - # We've made a request-response and now need to issue a redirect request. - request = self.build_redirect_request(request, response) + # Make the next request + request = redirect def close(self): pass diff --git a/src/httpx/_content.py b/src/httpx/_content.py index 5d9b4ec..895e52c 100644 --- a/src/httpx/_content.py +++ b/src/httpx/_content.py @@ -1,3 +1,4 @@ +import copy import json import os import typing @@ -339,8 +340,26 @@ def parse(self, stream: Stream) -> 'Content': data = json.loads(source) return JSON(data, source) + # Return the underlying data. Copied to ensure immutability. + @property + def value(self) -> typing.Any: + return copy.deepcopy(self._data) + + # dict and list style accessors, eg. for casting. + def keys(self) -> typing.KeysView[str]: + return self._data.keys() + + def __len__(self) -> int: + return len(self._data) + def __getitem__(self, key: typing.Any) -> typing.Any: - return self._data[key] + return copy.deepcopy(self._data[key]) + + # Built-ins. + def __eq__(self, other: typing.Any) -> bool: + if isinstance(other, JSON): + return self._data == other._data + return self._data == other def __str__(self) -> str: return self._content.decode('utf-8') diff --git a/tests/test_ahttpx/test_client.py b/tests/test_ahttpx/test_client.py index b714152..f48133f 100644 --- a/tests/test_ahttpx/test_client.py +++ b/tests/test_ahttpx/test_client.py @@ -31,12 +31,16 @@ async def test_client(client): @pytest.mark.trio -async def test_get(client): - async with ahttpx.serve_http(echo) as server: - r = await client.get(server.url) - assert r.status_code == 200 - assert r.body == b'{"method":"GET","query-params":{},"content-type":null,"json":null}' - # assert r.text == '{"method":"GET","query-params":{},"content-type":null,"json":null}' +async def test_get(client, server): + r = await client.get(server.url) + assert r.status_code == 200 + assert r.body == b'{"method":"GET","query-params":{},"content-type":null,"json":null}' + assert r.content == { + "method": "GET", + "query-params": {}, + "content-type": None, + "json": None + } @pytest.mark.trio @@ -44,7 +48,7 @@ async def test_post(client, server): data = ahttpx.JSON({"data": 123}) r = await client.post(server.url, content=data) assert r.status_code == 200 - assert json.loads(r.body) == { + assert r.content == { 'method': 'POST', 'query-params': {}, 'content-type': 'application/json', @@ -57,7 +61,7 @@ async def test_put(client, server): data = ahttpx.JSON({"data": 123}) r = await client.put(server.url, content=data) assert r.status_code == 200 - assert json.loads(r.body) == { + assert r.content == { 'method': 'PUT', 'query-params': {}, 'content-type': 'application/json', @@ -70,7 +74,7 @@ async def test_patch(client, server): data = ahttpx.JSON({"data": 123}) r = await client.patch(server.url, content=data) assert r.status_code == 200 - assert json.loads(r.body) == { + assert r.content == { 'method': 'PATCH', 'query-params': {}, 'content-type': 'application/json', @@ -82,7 +86,7 @@ async def test_patch(client, server): async def test_delete(client, server): r = await client.delete(server.url) assert r.status_code == 200 - assert json.loads(r.body) == { + assert r.content == { 'method': 'DELETE', 'query-params': {}, 'content-type': None, @@ -94,7 +98,7 @@ async def test_delete(client, server): async def test_request(client, server): r = await client.request("GET", server.url) assert r.status_code == 200 - assert json.loads(r.body) == { + assert r.content == { 'method': 'GET', 'query-params': {}, 'content-type': None, diff --git a/tests/test_ahttpx/test_quickstart.py b/tests/test_ahttpx/test_quickstart.py index b426c7a..c16a92e 100644 --- a/tests/test_ahttpx/test_quickstart.py +++ b/tests/test_ahttpx/test_quickstart.py @@ -23,7 +23,7 @@ async def server(): async def test_get(server): r = await ahttpx.get(server.url) assert r.status_code == 200 - assert json.loads(r.body) == { + assert r.content == { 'method': 'GET', 'query-params': {}, 'content-type': None, @@ -36,7 +36,7 @@ async def test_post(server): data = ahttpx.JSON({"data": 123}) r = await ahttpx.post(server.url, content=data) assert r.status_code == 200 - assert json.loads(r.body) == { + assert r.content == { 'method': 'POST', 'query-params': {}, 'content-type': 'application/json', @@ -49,7 +49,7 @@ async def test_put(server): data = ahttpx.JSON({"data": 123}) r = await ahttpx.put(server.url, content=data) assert r.status_code == 200 - assert json.loads(r.body) == { + assert r.content == { 'method': 'PUT', 'query-params': {}, 'content-type': 'application/json', @@ -62,7 +62,7 @@ async def test_patch(server): data = ahttpx.JSON({"data": 123}) r = await ahttpx.patch(server.url, content=data) assert r.status_code == 200 - assert json.loads(r.body) == { + assert r.content == { 'method': 'PATCH', 'query-params': {}, 'content-type': 'application/json', @@ -74,7 +74,7 @@ async def test_patch(server): async def test_delete(server): r = await ahttpx.delete(server.url) assert r.status_code == 200 - assert json.loads(r.body) == { + assert r.content == { 'method': 'DELETE', 'query-params': {}, 'content-type': None, diff --git a/tests/test_httpx/test_client.py b/tests/test_httpx/test_client.py index 312b742..b9c261b 100644 --- a/tests/test_httpx/test_client.py +++ b/tests/test_httpx/test_client.py @@ -29,19 +29,23 @@ def test_client(client): assert repr(client) == "" -def test_get(client): - with httpx.serve_http(echo) as server: - r = client.get(server.url) - assert r.status_code == 200 - assert r.body == b'{"method":"GET","query-params":{},"content-type":null,"json":null}' - # assert r.text == '{"method":"GET","query-params":{},"content-type":null,"json":null}' +def test_get(client, server): + r = client.get(server.url) + assert r.status_code == 200 + assert r.body == b'{"method":"GET","query-params":{},"content-type":null,"json":null}' + assert r.content == { + "method": "GET", + "query-params": {}, + "content-type": None, + "json": None + } def test_post(client, server): data = httpx.JSON({"data": 123}) r = client.post(server.url, content=data) assert r.status_code == 200 - assert json.loads(r.body) == { + assert r.content == { 'method': 'POST', 'query-params': {}, 'content-type': 'application/json', @@ -53,7 +57,7 @@ def test_put(client, server): data = httpx.JSON({"data": 123}) r = client.put(server.url, content=data) assert r.status_code == 200 - assert json.loads(r.body) == { + assert r.content == { 'method': 'PUT', 'query-params': {}, 'content-type': 'application/json', @@ -65,7 +69,7 @@ def test_patch(client, server): data = httpx.JSON({"data": 123}) r = client.patch(server.url, content=data) assert r.status_code == 200 - assert json.loads(r.body) == { + assert r.content == { 'method': 'PATCH', 'query-params': {}, 'content-type': 'application/json', @@ -76,7 +80,7 @@ def test_patch(client, server): def test_delete(client, server): r = client.delete(server.url) assert r.status_code == 200 - assert json.loads(r.body) == { + assert r.content == { 'method': 'DELETE', 'query-params': {}, 'content-type': None, @@ -87,7 +91,7 @@ def test_delete(client, server): def test_request(client, server): r = client.request("GET", server.url) assert r.status_code == 200 - assert json.loads(r.body) == { + assert r.content == { 'method': 'GET', 'query-params': {}, 'content-type': None, diff --git a/tests/test_httpx/test_quickstart.py b/tests/test_httpx/test_quickstart.py index 9c11cc9..8686beb 100644 --- a/tests/test_httpx/test_quickstart.py +++ b/tests/test_httpx/test_quickstart.py @@ -22,7 +22,7 @@ def server(): def test_get(server): r = httpx.get(server.url) assert r.status_code == 200 - assert json.loads(r.body) == { + assert r.content == { 'method': 'GET', 'query-params': {}, 'content-type': None, @@ -34,7 +34,7 @@ def test_post(server): data = httpx.JSON({"data": 123}) r = httpx.post(server.url, content=data) assert r.status_code == 200 - assert json.loads(r.body) == { + assert r.content == { 'method': 'POST', 'query-params': {}, 'content-type': 'application/json', @@ -46,7 +46,7 @@ def test_put(server): data = httpx.JSON({"data": 123}) r = httpx.put(server.url, content=data) assert r.status_code == 200 - assert json.loads(r.body) == { + assert r.content == { 'method': 'PUT', 'query-params': {}, 'content-type': 'application/json', @@ -58,7 +58,7 @@ def test_patch(server): data = httpx.JSON({"data": 123}) r = httpx.patch(server.url, content=data) assert r.status_code == 200 - assert json.loads(r.body) == { + assert r.content == { 'method': 'PATCH', 'query-params': {}, 'content-type': 'application/json', @@ -69,7 +69,7 @@ def test_patch(server): def test_delete(server): r = httpx.delete(server.url) assert r.status_code == 200 - assert json.loads(r.body) == { + assert r.content == { 'method': 'DELETE', 'query-params': {}, 'content-type': None,