Skip to content

Commit a3f0bd9

Browse files
authored
Adding json_deserialize parameter to aiohttp and httpx transports (#465)
1 parent 3a641b1 commit a3f0bd9

File tree

5 files changed

+120
-11
lines changed

5 files changed

+120
-11
lines changed

gql/client.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ def __init__(
106106
:param serialize_variables: whether the variable values should be
107107
serialized. Used for custom scalars and/or enums. Default: False.
108108
:param parse_results: Whether gql will try to parse the serialized output
109-
sent by the backend. Can be used to unserialize custom scalars or enums.
109+
sent by the backend. Can be used to deserialize custom scalars or enums.
110110
:param batch_interval: Time to wait in seconds for batching requests together.
111111
Batching is disabled (by default) if 0.
112112
:param batch_max: Maximum number of requests in a single batch.
@@ -892,7 +892,7 @@ def _execute(
892892
:param serialize_variables: whether the variable values should be
893893
serialized. Used for custom scalars and/or enums.
894894
By default use the serialize_variables argument of the client.
895-
:param parse_result: Whether gql will unserialize the result.
895+
:param parse_result: Whether gql will deserialize the result.
896896
By default use the parse_results argument of the client.
897897
898898
The extra arguments are passed to the transport execute method."""
@@ -1006,7 +1006,7 @@ def execute(
10061006
:param serialize_variables: whether the variable values should be
10071007
serialized. Used for custom scalars and/or enums.
10081008
By default use the serialize_variables argument of the client.
1009-
:param parse_result: Whether gql will unserialize the result.
1009+
:param parse_result: Whether gql will deserialize the result.
10101010
By default use the parse_results argument of the client.
10111011
:param get_execution_result: return the full ExecutionResult instance instead of
10121012
only the "data" field. Necessary if you want to get the "extensions" field.
@@ -1057,7 +1057,7 @@ def _execute_batch(
10571057
:param serialize_variables: whether the variable values should be
10581058
serialized. Used for custom scalars and/or enums.
10591059
By default use the serialize_variables argument of the client.
1060-
:param parse_result: Whether gql will unserialize the result.
1060+
:param parse_result: Whether gql will deserialize the result.
10611061
By default use the parse_results argument of the client.
10621062
:param validate_document: Whether we still need to validate the document.
10631063
@@ -1151,7 +1151,7 @@ def execute_batch(
11511151
:param serialize_variables: whether the variable values should be
11521152
serialized. Used for custom scalars and/or enums.
11531153
By default use the serialize_variables argument of the client.
1154-
:param parse_result: Whether gql will unserialize the result.
1154+
:param parse_result: Whether gql will deserialize the result.
11551155
By default use the parse_results argument of the client.
11561156
:param get_execution_result: return the full ExecutionResult instance instead of
11571157
only the "data" field. Necessary if you want to get the "extensions" field.
@@ -1333,7 +1333,7 @@ async def _subscribe(
13331333
:param serialize_variables: whether the variable values should be
13341334
serialized. Used for custom scalars and/or enums.
13351335
By default use the serialize_variables argument of the client.
1336-
:param parse_result: Whether gql will unserialize the result.
1336+
:param parse_result: Whether gql will deserialize the result.
13371337
By default use the parse_results argument of the client.
13381338
13391339
The extra arguments are passed to the transport subscribe method."""
@@ -1454,7 +1454,7 @@ async def subscribe(
14541454
:param serialize_variables: whether the variable values should be
14551455
serialized. Used for custom scalars and/or enums.
14561456
By default use the serialize_variables argument of the client.
1457-
:param parse_result: Whether gql will unserialize the result.
1457+
:param parse_result: Whether gql will deserialize the result.
14581458
By default use the parse_results argument of the client.
14591459
:param get_execution_result: yield the full ExecutionResult instance instead of
14601460
only the "data" field. Necessary if you want to get the "extensions" field.
@@ -1511,7 +1511,7 @@ async def _execute(
15111511
:param serialize_variables: whether the variable values should be
15121512
serialized. Used for custom scalars and/or enums.
15131513
By default use the serialize_variables argument of the client.
1514-
:param parse_result: Whether gql will unserialize the result.
1514+
:param parse_result: Whether gql will deserialize the result.
15151515
By default use the parse_results argument of the client.
15161516
15171517
The extra arguments are passed to the transport execute method."""
@@ -1617,7 +1617,7 @@ async def execute(
16171617
:param serialize_variables: whether the variable values should be
16181618
serialized. Used for custom scalars and/or enums.
16191619
By default use the serialize_variables argument of the client.
1620-
:param parse_result: Whether gql will unserialize the result.
1620+
:param parse_result: Whether gql will deserialize the result.
16211621
By default use the parse_results argument of the client.
16221622
:param get_execution_result: return the full ExecutionResult instance instead of
16231623
only the "data" field. Necessary if you want to get the "extensions" field.

gql/transport/aiohttp.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ def __init__(
5050
timeout: Optional[int] = None,
5151
ssl_close_timeout: Optional[Union[int, float]] = 10,
5252
json_serialize: Callable = json.dumps,
53+
json_deserialize: Callable = json.loads,
5354
client_session_args: Optional[Dict[str, Any]] = None,
5455
) -> None:
5556
"""Initialize the transport with the given aiohttp parameters.
@@ -64,6 +65,8 @@ def __init__(
6465
to close properly
6566
:param json_serialize: Json serializer callable.
6667
By default json.dumps() function
68+
:param json_deserialize: Json deserializer callable.
69+
By default json.loads() function
6770
:param client_session_args: Dict of extra args passed to
6871
`aiohttp.ClientSession`_
6972
@@ -81,6 +84,7 @@ def __init__(
8184
self.session: Optional[aiohttp.ClientSession] = None
8285
self.response_headers: Optional[CIMultiDictProxy[str]]
8386
self.json_serialize: Callable = json_serialize
87+
self.json_deserialize: Callable = json_deserialize
8488

8589
async def connect(self) -> None:
8690
"""Coroutine which will create an aiohttp ClientSession() as self.session.
@@ -328,7 +332,7 @@ async def raise_response_error(resp: aiohttp.ClientResponse, reason: str):
328332
)
329333

330334
try:
331-
result = await resp.json(content_type=None)
335+
result = await resp.json(loads=self.json_deserialize, content_type=None)
332336

333337
if log.isEnabledFor(logging.INFO):
334338
result_text = await resp.text()

gql/transport/httpx.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,17 +38,21 @@ def __init__(
3838
self,
3939
url: Union[str, httpx.URL],
4040
json_serialize: Callable = json.dumps,
41+
json_deserialize: Callable = json.loads,
4142
**kwargs,
4243
):
4344
"""Initialize the transport with the given httpx parameters.
4445
4546
:param url: The GraphQL server URL. Example: 'https://server.com:PORT/path'.
4647
:param json_serialize: Json serializer callable.
4748
By default json.dumps() function.
49+
:param json_deserialize: Json deserializer callable.
50+
By default json.loads() function.
4851
:param kwargs: Extra args passed to the `httpx` client.
4952
"""
5053
self.url = url
5154
self.json_serialize = json_serialize
55+
self.json_deserialize = json_deserialize
5256
self.kwargs = kwargs
5357

5458
def _prepare_request(
@@ -145,7 +149,7 @@ def _prepare_result(self, response: httpx.Response) -> ExecutionResult:
145149
log.debug("<<< %s", response.text)
146150

147151
try:
148-
result: Dict[str, Any] = response.json()
152+
result: Dict[str, Any] = self.json_deserialize(response.content)
149153

150154
except Exception:
151155
self._raise_response_error(response, "Not a JSON answer")

tests/test_aiohttp.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1511,6 +1511,56 @@ async def handler(request):
15111511
assert expected_log in caplog.text
15121512

15131513

1514+
query_float_str = """
1515+
query getPi {
1516+
pi
1517+
}
1518+
"""
1519+
1520+
query_float_server_answer_data = '{"pi": 3.141592653589793238462643383279502884197}'
1521+
1522+
query_float_server_answer = f'{{"data":{query_float_server_answer_data}}}'
1523+
1524+
1525+
@pytest.mark.asyncio
1526+
async def test_aiohttp_json_deserializer(event_loop, aiohttp_server):
1527+
from aiohttp import web
1528+
from decimal import Decimal
1529+
from functools import partial
1530+
from gql.transport.aiohttp import AIOHTTPTransport
1531+
1532+
async def handler(request):
1533+
return web.Response(
1534+
text=query_float_server_answer,
1535+
content_type="application/json",
1536+
)
1537+
1538+
app = web.Application()
1539+
app.router.add_route("POST", "/", handler)
1540+
server = await aiohttp_server(app)
1541+
1542+
url = server.make_url("/")
1543+
1544+
json_loads = partial(json.loads, parse_float=Decimal)
1545+
1546+
transport = AIOHTTPTransport(
1547+
url=url,
1548+
timeout=10,
1549+
json_deserialize=json_loads,
1550+
)
1551+
1552+
async with Client(transport=transport) as session:
1553+
1554+
query = gql(query_float_str)
1555+
1556+
# Execute query asynchronously
1557+
result = await session.execute(query)
1558+
1559+
pi = result["pi"]
1560+
1561+
assert pi == Decimal("3.141592653589793238462643383279502884197")
1562+
1563+
15141564
@pytest.mark.asyncio
15151565
async def test_aiohttp_connector_owner_false(event_loop, aiohttp_server):
15161566
from aiohttp import web, TCPConnector

tests/test_httpx_async.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1389,3 +1389,54 @@ async def handler(request):
13891389
# Checking that there is no space after the colon in the log
13901390
expected_log = '"query":"query getContinents'
13911391
assert expected_log in caplog.text
1392+
1393+
1394+
query_float_str = """
1395+
query getPi {
1396+
pi
1397+
}
1398+
"""
1399+
1400+
query_float_server_answer_data = '{"pi": 3.141592653589793238462643383279502884197}'
1401+
1402+
query_float_server_answer = f'{{"data":{query_float_server_answer_data}}}'
1403+
1404+
1405+
@pytest.mark.aiohttp
1406+
@pytest.mark.asyncio
1407+
async def test_httpx_json_deserializer(event_loop, aiohttp_server):
1408+
from aiohttp import web
1409+
from decimal import Decimal
1410+
from functools import partial
1411+
from gql.transport.httpx import HTTPXAsyncTransport
1412+
1413+
async def handler(request):
1414+
return web.Response(
1415+
text=query_float_server_answer,
1416+
content_type="application/json",
1417+
)
1418+
1419+
app = web.Application()
1420+
app.router.add_route("POST", "/", handler)
1421+
server = await aiohttp_server(app)
1422+
1423+
url = str(server.make_url("/"))
1424+
1425+
json_loads = partial(json.loads, parse_float=Decimal)
1426+
1427+
transport = HTTPXAsyncTransport(
1428+
url=url,
1429+
timeout=10,
1430+
json_deserialize=json_loads,
1431+
)
1432+
1433+
async with Client(transport=transport) as session:
1434+
1435+
query = gql(query_float_str)
1436+
1437+
# Execute query asynchronously
1438+
result = await session.execute(query)
1439+
1440+
pi = result["pi"]
1441+
1442+
assert pi == Decimal("3.141592653589793238462643383279502884197")

0 commit comments

Comments
 (0)