Skip to content

Commit 5df465b

Browse files
authored
Feature get_execution_result argument of execute and subscribe (#257)
1 parent 6cfab1e commit 5df465b

8 files changed

+102
-10
lines changed

docs/usage/extensions.rst

+36
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
.. _extensions:
2+
3+
Extensions
4+
----------
5+
6+
When you execute (or subscribe) GraphQL requests, the server will send
7+
responses which may have 3 fields:
8+
9+
- data: the serialized response from the backend
10+
- errors: a list of potential errors
11+
- extensions: an optional field for additional data
12+
13+
If there are errors in the response, then the
14+
:code:`execute` or :code:`subscribe` methods will
15+
raise a :code:`TransportQueryError`.
16+
17+
If no errors are present, then only the data from the response is returned by default.
18+
19+
.. code-block:: python
20+
21+
result = client.execute(query)
22+
# result is here the content of the data field
23+
24+
If you need to receive the extensions data too, then you can run the
25+
:code:`execute` or :code:`subscribe` methods with :code:`get_execution_result=True`.
26+
27+
In that case, the full execution result is returned and you can have access
28+
to the extensions field
29+
30+
.. code-block:: python
31+
32+
result = client.execute(query, get_execution_result=True)
33+
# result is here an ExecutionResult instance
34+
35+
# result.data is the content of the data field
36+
# result.extensions is the content of the extensions field

docs/usage/index.rst

+1
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ Usage
1111
headers
1212
file_upload
1313
custom_scalars_and_enums
14+
extensions

gql/client.py

+34-7
Original file line numberDiff line numberDiff line change
@@ -367,8 +367,9 @@ def execute(
367367
operation_name: Optional[str] = None,
368368
serialize_variables: Optional[bool] = None,
369369
parse_result: Optional[bool] = None,
370+
get_execution_result: bool = False,
370371
**kwargs,
371-
) -> Dict:
372+
) -> Union[Dict[str, Any], ExecutionResult]:
372373
"""Execute the provided document AST synchronously using
373374
the sync transport.
374375
@@ -382,6 +383,8 @@ def execute(
382383
serialized. Used for custom scalars and/or enums. Default: False.
383384
:param parse_result: Whether gql will unserialize the result.
384385
By default use the parse_results attribute of the client.
386+
:param get_execution_result: return the full ExecutionResult instance instead of
387+
only the "data" field. Necessary if you want to get the "extensions" field.
385388
386389
The extra arguments are passed to the transport execute method."""
387390

@@ -399,13 +402,19 @@ def execute(
399402
# Raise an error if an error is returned in the ExecutionResult object
400403
if result.errors:
401404
raise TransportQueryError(
402-
str(result.errors[0]), errors=result.errors, data=result.data
405+
str(result.errors[0]),
406+
errors=result.errors,
407+
data=result.data,
408+
extensions=result.extensions,
403409
)
404410

405411
assert (
406412
result.data is not None
407413
), "Transport returned an ExecutionResult without data or errors"
408414

415+
if get_execution_result:
416+
return result
417+
409418
return result.data
410419

411420
def fetch_schema(self) -> None:
@@ -519,8 +528,9 @@ async def subscribe(
519528
operation_name: Optional[str] = None,
520529
serialize_variables: Optional[bool] = None,
521530
parse_result: Optional[bool] = None,
531+
get_execution_result: bool = False,
522532
**kwargs,
523-
) -> AsyncGenerator[Dict, None]:
533+
) -> AsyncGenerator[Union[Dict[str, Any], ExecutionResult], None]:
524534
"""Coroutine to subscribe asynchronously to the provided document AST
525535
asynchronously using the async transport.
526536
@@ -534,6 +544,8 @@ async def subscribe(
534544
serialized. Used for custom scalars and/or enums. Default: False.
535545
:param parse_result: Whether gql will unserialize the result.
536546
By default use the parse_results attribute of the client.
547+
:param get_execution_result: yield the full ExecutionResult instance instead of
548+
only the "data" field. Necessary if you want to get the "extensions" field.
537549
538550
The extra arguments are passed to the transport subscribe method."""
539551

@@ -554,11 +566,17 @@ async def subscribe(
554566
# Raise an error if an error is returned in the ExecutionResult object
555567
if result.errors:
556568
raise TransportQueryError(
557-
str(result.errors[0]), errors=result.errors, data=result.data
569+
str(result.errors[0]),
570+
errors=result.errors,
571+
data=result.data,
572+
extensions=result.extensions,
558573
)
559574

560575
elif result.data is not None:
561-
yield result.data
576+
if get_execution_result:
577+
yield result
578+
else:
579+
yield result.data
562580
finally:
563581
await inner_generator.aclose()
564582

@@ -636,8 +654,9 @@ async def execute(
636654
operation_name: Optional[str] = None,
637655
serialize_variables: Optional[bool] = None,
638656
parse_result: Optional[bool] = None,
657+
get_execution_result: bool = False,
639658
**kwargs,
640-
) -> Dict:
659+
) -> Union[Dict[str, Any], ExecutionResult]:
641660
"""Coroutine to execute the provided document AST asynchronously using
642661
the async transport.
643662
@@ -651,6 +670,8 @@ async def execute(
651670
serialized. Used for custom scalars and/or enums. Default: False.
652671
:param parse_result: Whether gql will unserialize the result.
653672
By default use the parse_results attribute of the client.
673+
:param get_execution_result: return the full ExecutionResult instance instead of
674+
only the "data" field. Necessary if you want to get the "extensions" field.
654675
655676
The extra arguments are passed to the transport execute method."""
656677

@@ -668,13 +689,19 @@ async def execute(
668689
# Raise an error if an error is returned in the ExecutionResult object
669690
if result.errors:
670691
raise TransportQueryError(
671-
str(result.errors[0]), errors=result.errors, data=result.data
692+
str(result.errors[0]),
693+
errors=result.errors,
694+
data=result.data,
695+
extensions=result.extensions,
672696
)
673697

674698
assert (
675699
result.data is not None
676700
), "Transport returned an ExecutionResult without data or errors"
677701

702+
if get_execution_result:
703+
return result
704+
678705
return result.data
679706

680707
async def fetch_schema(self) -> None:

gql/transport/exceptions.py

+2
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,13 @@ def __init__(
3535
query_id: Optional[int] = None,
3636
errors: Optional[List[Any]] = None,
3737
data: Optional[Any] = None,
38+
extensions: Optional[Any] = None,
3839
):
3940
super().__init__(msg)
4041
self.query_id = query_id
4142
self.errors = errors
4243
self.data = data
44+
self.extensions = extensions
4345

4446

4547
class TransportClosed(TransportError):

tests/test_aiohttp.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1070,6 +1070,6 @@ async def handler(request):
10701070

10711071
query = gql(query1_str)
10721072

1073-
execution_result = await session._execute(query)
1073+
execution_result = await session.execute(query, get_execution_result=True)
10741074

10751075
assert execution_result.extensions["key1"] == "val1"

tests/test_requests.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,7 @@ def test_code():
328328

329329
query = gql(query1_str)
330330

331-
execution_result = session._execute(query)
331+
execution_result = session.execute(query, get_execution_result=True)
332332

333333
assert execution_result.extensions["key1"] == "val1"
334334

tests/test_websocket_query.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -596,6 +596,6 @@ async def test_websocket_simple_query_with_extensions(
596596

597597
query = gql(query_str)
598598

599-
execution_result = await session._execute(query)
599+
execution_result = await session.execute(query, get_execution_result=True)
600600

601601
assert execution_result.extensions["key1"] == "val1"

tests/test_websocket_subscription.py

+26
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from typing import List
55

66
import pytest
7+
from graphql import ExecutionResult
78
from parse import search
89

910
from gql import Client, gql
@@ -142,6 +143,31 @@ async def test_websocket_subscription(event_loop, client_and_server, subscriptio
142143
assert count == -1
143144

144145

146+
@pytest.mark.asyncio
147+
@pytest.mark.parametrize("server", [server_countdown], indirect=True)
148+
@pytest.mark.parametrize("subscription_str", [countdown_subscription_str])
149+
async def test_websocket_subscription_get_execution_result(
150+
event_loop, client_and_server, subscription_str
151+
):
152+
153+
session, server = client_and_server
154+
155+
count = 10
156+
subscription = gql(subscription_str.format(count=count))
157+
158+
async for result in session.subscribe(subscription, get_execution_result=True):
159+
160+
assert isinstance(result, ExecutionResult)
161+
162+
number = result.data["number"]
163+
print(f"Number received: {number}")
164+
165+
assert number == count
166+
count -= 1
167+
168+
assert count == -1
169+
170+
145171
@pytest.mark.asyncio
146172
@pytest.mark.parametrize("server", [server_countdown], indirect=True)
147173
@pytest.mark.parametrize("subscription_str", [countdown_subscription_str])

0 commit comments

Comments
 (0)