Skip to content

Commit 0e8a4e0

Browse files
Fixup (#151)
* Enable equality operator for JSON * Fixup
1 parent 050fafe commit 0e8a4e0

File tree

8 files changed

+128
-54
lines changed

8 files changed

+128
-54
lines changed

src/ahttpx/_client.py

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -138,20 +138,34 @@ class RedirectMiddleware(Transport):
138138
def __init__(self, transport: Transport) -> None:
139139
self._transport = transport
140140

141-
def is_redirect(self, response: Response) -> bool:
142-
return (
143-
response.status_code in (301, 302, 303, 307, 308)
144-
and "Location" in response.headers
145-
)
141+
def build_redirect_request(self, request: Request, response: Response) -> Request | None:
142+
# Redirect status codes...
143+
if response.status_code not in (301, 302, 303, 307, 308):
144+
return None
145+
146+
# Redirects need a valid location header...
147+
try:
148+
location = URL(response.headers['Location'])
149+
except (KeyError, ValueError):
150+
return None
146151

147-
def build_redirect_request(self, request: Request, response: Response) -> Request:
148-
raise NotImplementedError()
152+
# Instantiate a redirect request...
153+
method = request.method
154+
url = request.url.join(location)
155+
headers = request.headers
156+
content = request.content
157+
158+
return Request(method, url, headers, content)
149159

150160
async def send(self, request: Request) -> Response:
151161
while True:
152162
response = await self._transport.send(request)
153163

154-
if not self.is_redirect(response):
164+
# Determine if we have a redirect or not.
165+
redirect = self.build_redirect_request(request, response)
166+
167+
# If we don't have a redirect, we're done.
168+
if redirect is None:
155169
return response
156170

157171
# If we have a redirect, then we read the body of the response.
@@ -160,8 +174,8 @@ async def send(self, request: Request) -> Response:
160174
async with response as stream:
161175
await stream.read()
162176

163-
# We've made a request-response and now need to issue a redirect request.
164-
request = self.build_redirect_request(request, response)
177+
# Make the next request
178+
request = redirect
165179

166180
async def close(self):
167181
pass

src/ahttpx/_content.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import copy
12
import json
23
import os
34
import typing
@@ -339,8 +340,26 @@ async def parse(self, stream: Stream) -> 'Content':
339340
data = json.loads(source)
340341
return JSON(data, source)
341342

343+
# Return the underlying data. Copied to ensure immutability.
344+
@property
345+
def value(self) -> typing.Any:
346+
return copy.deepcopy(self._data)
347+
348+
# dict and list style accessors, eg. for casting.
349+
def keys(self) -> typing.KeysView[str]:
350+
return self._data.keys()
351+
352+
def __len__(self) -> int:
353+
return len(self._data)
354+
342355
def __getitem__(self, key: typing.Any) -> typing.Any:
343-
return self._data[key]
356+
return copy.deepcopy(self._data[key])
357+
358+
# Built-ins.
359+
def __eq__(self, other: typing.Any) -> bool:
360+
if isinstance(other, JSON):
361+
return self._data == other._data
362+
return self._data == other
344363

345364
def __str__(self) -> str:
346365
return self._content.decode('utf-8')

src/httpx/_client.py

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -138,20 +138,34 @@ class RedirectMiddleware(Transport):
138138
def __init__(self, transport: Transport) -> None:
139139
self._transport = transport
140140

141-
def is_redirect(self, response: Response) -> bool:
142-
return (
143-
response.status_code in (301, 302, 303, 307, 308)
144-
and "Location" in response.headers
145-
)
141+
def build_redirect_request(self, request: Request, response: Response) -> Request | None:
142+
# Redirect status codes...
143+
if response.status_code not in (301, 302, 303, 307, 308):
144+
return None
145+
146+
# Redirects need a valid location header...
147+
try:
148+
location = URL(response.headers['Location'])
149+
except (KeyError, ValueError):
150+
return None
146151

147-
def build_redirect_request(self, request: Request, response: Response) -> Request:
148-
raise NotImplementedError()
152+
# Instantiate a redirect request...
153+
method = request.method
154+
url = request.url.join(location)
155+
headers = request.headers
156+
content = request.content
157+
158+
return Request(method, url, headers, content)
149159

150160
def send(self, request: Request) -> Response:
151161
while True:
152162
response = self._transport.send(request)
153163

154-
if not self.is_redirect(response):
164+
# Determine if we have a redirect or not.
165+
redirect = self.build_redirect_request(request, response)
166+
167+
# If we don't have a redirect, we're done.
168+
if redirect is None:
155169
return response
156170

157171
# If we have a redirect, then we read the body of the response.
@@ -160,8 +174,8 @@ def send(self, request: Request) -> Response:
160174
with response as stream:
161175
stream.read()
162176

163-
# We've made a request-response and now need to issue a redirect request.
164-
request = self.build_redirect_request(request, response)
177+
# Make the next request
178+
request = redirect
165179

166180
def close(self):
167181
pass

src/httpx/_content.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import copy
12
import json
23
import os
34
import typing
@@ -339,8 +340,26 @@ def parse(self, stream: Stream) -> 'Content':
339340
data = json.loads(source)
340341
return JSON(data, source)
341342

343+
# Return the underlying data. Copied to ensure immutability.
344+
@property
345+
def value(self) -> typing.Any:
346+
return copy.deepcopy(self._data)
347+
348+
# dict and list style accessors, eg. for casting.
349+
def keys(self) -> typing.KeysView[str]:
350+
return self._data.keys()
351+
352+
def __len__(self) -> int:
353+
return len(self._data)
354+
342355
def __getitem__(self, key: typing.Any) -> typing.Any:
343-
return self._data[key]
356+
return copy.deepcopy(self._data[key])
357+
358+
# Built-ins.
359+
def __eq__(self, other: typing.Any) -> bool:
360+
if isinstance(other, JSON):
361+
return self._data == other._data
362+
return self._data == other
344363

345364
def __str__(self) -> str:
346365
return self._content.decode('utf-8')

tests/test_ahttpx/test_client.py

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,20 +31,24 @@ async def test_client(client):
3131

3232

3333
@pytest.mark.trio
34-
async def test_get(client):
35-
async with ahttpx.serve_http(echo) as server:
36-
r = await client.get(server.url)
37-
assert r.status_code == 200
38-
assert r.body == b'{"method":"GET","query-params":{},"content-type":null,"json":null}'
39-
# assert r.text == '{"method":"GET","query-params":{},"content-type":null,"json":null}'
34+
async def test_get(client, server):
35+
r = await client.get(server.url)
36+
assert r.status_code == 200
37+
assert r.body == b'{"method":"GET","query-params":{},"content-type":null,"json":null}'
38+
assert r.content == {
39+
"method": "GET",
40+
"query-params": {},
41+
"content-type": None,
42+
"json": None
43+
}
4044

4145

4246
@pytest.mark.trio
4347
async def test_post(client, server):
4448
data = ahttpx.JSON({"data": 123})
4549
r = await client.post(server.url, content=data)
4650
assert r.status_code == 200
47-
assert json.loads(r.body) == {
51+
assert r.content == {
4852
'method': 'POST',
4953
'query-params': {},
5054
'content-type': 'application/json',
@@ -57,7 +61,7 @@ async def test_put(client, server):
5761
data = ahttpx.JSON({"data": 123})
5862
r = await client.put(server.url, content=data)
5963
assert r.status_code == 200
60-
assert json.loads(r.body) == {
64+
assert r.content == {
6165
'method': 'PUT',
6266
'query-params': {},
6367
'content-type': 'application/json',
@@ -70,7 +74,7 @@ async def test_patch(client, server):
7074
data = ahttpx.JSON({"data": 123})
7175
r = await client.patch(server.url, content=data)
7276
assert r.status_code == 200
73-
assert json.loads(r.body) == {
77+
assert r.content == {
7478
'method': 'PATCH',
7579
'query-params': {},
7680
'content-type': 'application/json',
@@ -82,7 +86,7 @@ async def test_patch(client, server):
8286
async def test_delete(client, server):
8387
r = await client.delete(server.url)
8488
assert r.status_code == 200
85-
assert json.loads(r.body) == {
89+
assert r.content == {
8690
'method': 'DELETE',
8791
'query-params': {},
8892
'content-type': None,
@@ -94,7 +98,7 @@ async def test_delete(client, server):
9498
async def test_request(client, server):
9599
r = await client.request("GET", server.url)
96100
assert r.status_code == 200
97-
assert json.loads(r.body) == {
101+
assert r.content == {
98102
'method': 'GET',
99103
'query-params': {},
100104
'content-type': None,

tests/test_ahttpx/test_quickstart.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ async def server():
2323
async def test_get(server):
2424
r = await ahttpx.get(server.url)
2525
assert r.status_code == 200
26-
assert json.loads(r.body) == {
26+
assert r.content == {
2727
'method': 'GET',
2828
'query-params': {},
2929
'content-type': None,
@@ -36,7 +36,7 @@ async def test_post(server):
3636
data = ahttpx.JSON({"data": 123})
3737
r = await ahttpx.post(server.url, content=data)
3838
assert r.status_code == 200
39-
assert json.loads(r.body) == {
39+
assert r.content == {
4040
'method': 'POST',
4141
'query-params': {},
4242
'content-type': 'application/json',
@@ -49,7 +49,7 @@ async def test_put(server):
4949
data = ahttpx.JSON({"data": 123})
5050
r = await ahttpx.put(server.url, content=data)
5151
assert r.status_code == 200
52-
assert json.loads(r.body) == {
52+
assert r.content == {
5353
'method': 'PUT',
5454
'query-params': {},
5555
'content-type': 'application/json',
@@ -62,7 +62,7 @@ async def test_patch(server):
6262
data = ahttpx.JSON({"data": 123})
6363
r = await ahttpx.patch(server.url, content=data)
6464
assert r.status_code == 200
65-
assert json.loads(r.body) == {
65+
assert r.content == {
6666
'method': 'PATCH',
6767
'query-params': {},
6868
'content-type': 'application/json',
@@ -74,7 +74,7 @@ async def test_patch(server):
7474
async def test_delete(server):
7575
r = await ahttpx.delete(server.url)
7676
assert r.status_code == 200
77-
assert json.loads(r.body) == {
77+
assert r.content == {
7878
'method': 'DELETE',
7979
'query-params': {},
8080
'content-type': None,

tests/test_httpx/test_client.py

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,19 +29,23 @@ def test_client(client):
2929
assert repr(client) == "<Client [0 active]>"
3030

3131

32-
def test_get(client):
33-
with httpx.serve_http(echo) as server:
34-
r = client.get(server.url)
35-
assert r.status_code == 200
36-
assert r.body == b'{"method":"GET","query-params":{},"content-type":null,"json":null}'
37-
# assert r.text == '{"method":"GET","query-params":{},"content-type":null,"json":null}'
32+
def test_get(client, server):
33+
r = client.get(server.url)
34+
assert r.status_code == 200
35+
assert r.body == b'{"method":"GET","query-params":{},"content-type":null,"json":null}'
36+
assert r.content == {
37+
"method": "GET",
38+
"query-params": {},
39+
"content-type": None,
40+
"json": None
41+
}
3842

3943

4044
def test_post(client, server):
4145
data = httpx.JSON({"data": 123})
4246
r = client.post(server.url, content=data)
4347
assert r.status_code == 200
44-
assert json.loads(r.body) == {
48+
assert r.content == {
4549
'method': 'POST',
4650
'query-params': {},
4751
'content-type': 'application/json',
@@ -53,7 +57,7 @@ def test_put(client, server):
5357
data = httpx.JSON({"data": 123})
5458
r = client.put(server.url, content=data)
5559
assert r.status_code == 200
56-
assert json.loads(r.body) == {
60+
assert r.content == {
5761
'method': 'PUT',
5862
'query-params': {},
5963
'content-type': 'application/json',
@@ -65,7 +69,7 @@ def test_patch(client, server):
6569
data = httpx.JSON({"data": 123})
6670
r = client.patch(server.url, content=data)
6771
assert r.status_code == 200
68-
assert json.loads(r.body) == {
72+
assert r.content == {
6973
'method': 'PATCH',
7074
'query-params': {},
7175
'content-type': 'application/json',
@@ -76,7 +80,7 @@ def test_patch(client, server):
7680
def test_delete(client, server):
7781
r = client.delete(server.url)
7882
assert r.status_code == 200
79-
assert json.loads(r.body) == {
83+
assert r.content == {
8084
'method': 'DELETE',
8185
'query-params': {},
8286
'content-type': None,
@@ -87,7 +91,7 @@ def test_delete(client, server):
8791
def test_request(client, server):
8892
r = client.request("GET", server.url)
8993
assert r.status_code == 200
90-
assert json.loads(r.body) == {
94+
assert r.content == {
9195
'method': 'GET',
9296
'query-params': {},
9397
'content-type': None,

tests/test_httpx/test_quickstart.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ def server():
2222
def test_get(server):
2323
r = httpx.get(server.url)
2424
assert r.status_code == 200
25-
assert json.loads(r.body) == {
25+
assert r.content == {
2626
'method': 'GET',
2727
'query-params': {},
2828
'content-type': None,
@@ -34,7 +34,7 @@ def test_post(server):
3434
data = httpx.JSON({"data": 123})
3535
r = httpx.post(server.url, content=data)
3636
assert r.status_code == 200
37-
assert json.loads(r.body) == {
37+
assert r.content == {
3838
'method': 'POST',
3939
'query-params': {},
4040
'content-type': 'application/json',
@@ -46,7 +46,7 @@ def test_put(server):
4646
data = httpx.JSON({"data": 123})
4747
r = httpx.put(server.url, content=data)
4848
assert r.status_code == 200
49-
assert json.loads(r.body) == {
49+
assert r.content == {
5050
'method': 'PUT',
5151
'query-params': {},
5252
'content-type': 'application/json',
@@ -58,7 +58,7 @@ def test_patch(server):
5858
data = httpx.JSON({"data": 123})
5959
r = httpx.patch(server.url, content=data)
6060
assert r.status_code == 200
61-
assert json.loads(r.body) == {
61+
assert r.content == {
6262
'method': 'PATCH',
6363
'query-params': {},
6464
'content-type': 'application/json',
@@ -69,7 +69,7 @@ def test_patch(server):
6969
def test_delete(server):
7070
r = httpx.delete(server.url)
7171
assert r.status_code == 200
72-
assert json.loads(r.body) == {
72+
assert r.content == {
7373
'method': 'DELETE',
7474
'query-params': {},
7575
'content-type': None,

0 commit comments

Comments
 (0)