Skip to content

Commit 36bbd47

Browse files
authored
chore: asynchronously gather execution results (#101)
1 parent 8829c09 commit 36bbd47

File tree

7 files changed

+127
-20
lines changed

7 files changed

+127
-20
lines changed

graphql_server/aiohttp/graphqlview.py

+12-5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
import asyncio
12
import copy
23
from collections.abc import MutableMapping
34
from functools import partial
45
from typing import List
56

67
from aiohttp import web
7-
from graphql import ExecutionResult, GraphQLError, specified_rules
8+
from graphql import GraphQLError, specified_rules
9+
from graphql.pyutils import is_awaitable
810
from graphql.type.schema import GraphQLSchema
911

1012
from graphql_server import (
@@ -22,6 +24,7 @@
2224
GraphiQLOptions,
2325
render_graphiql_async,
2426
)
27+
from graphql_server.utils import wrap_in_async
2528

2629

2730
class GraphQLView:
@@ -166,10 +169,14 @@ async def __call__(self, request):
166169
)
167170

168171
exec_res = (
169-
[
170-
ex if ex is None or isinstance(ex, ExecutionResult) else await ex
171-
for ex in execution_results
172-
]
172+
await asyncio.gather(
173+
*(
174+
ex
175+
if ex is not None and is_awaitable(ex)
176+
else wrap_in_async(lambda: ex)()
177+
for ex in execution_results
178+
)
179+
)
173180
if self.enable_async
174181
else execution_results
175182
)

graphql_server/quart/graphqlview.py

+12-5
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
import asyncio
12
import copy
23
from collections.abc import MutableMapping
34
from functools import partial
45
from typing import List
56

6-
from graphql import ExecutionResult, specified_rules
7+
from graphql import specified_rules
78
from graphql.error import GraphQLError
9+
from graphql.pyutils import is_awaitable
810
from graphql.type.schema import GraphQLSchema
911
from quart import Response, render_template_string, request
1012
from quart.views import View
@@ -24,6 +26,7 @@
2426
GraphiQLOptions,
2527
render_graphiql_sync,
2628
)
29+
from graphql_server.utils import wrap_in_async
2730

2831

2932
class GraphQLView(View):
@@ -113,10 +116,14 @@ async def dispatch_request(self):
113116
execution_context_class=self.get_execution_context_class(),
114117
)
115118
exec_res = (
116-
[
117-
ex if ex is None or isinstance(ex, ExecutionResult) else await ex
118-
for ex in execution_results
119-
]
119+
await asyncio.gather(
120+
*(
121+
ex
122+
if ex is not None and is_awaitable(ex)
123+
else wrap_in_async(lambda: ex)()
124+
for ex in execution_results
125+
)
126+
)
120127
if self.enable_async
121128
else execution_results
122129
)

graphql_server/sanic/graphqlview.py

+12-7
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1+
import asyncio
12
import copy
23
from cgi import parse_header
34
from collections.abc import MutableMapping
45
from functools import partial
56
from typing import List
67

7-
from graphql import ExecutionResult, GraphQLError, specified_rules
8+
from graphql import GraphQLError, specified_rules
9+
from graphql.pyutils import is_awaitable
810
from graphql.type.schema import GraphQLSchema
911
from sanic.response import HTTPResponse, html
1012
from sanic.views import HTTPMethodView
@@ -24,6 +26,7 @@
2426
GraphiQLOptions,
2527
render_graphiql_async,
2628
)
29+
from graphql_server.utils import wrap_in_async
2730

2831

2932
class GraphQLView(HTTPMethodView):
@@ -119,12 +122,14 @@ async def __handle_request(self, request, *args, **kwargs):
119122
execution_context_class=self.get_execution_context_class(),
120123
)
121124
exec_res = (
122-
[
123-
ex
124-
if ex is None or isinstance(ex, ExecutionResult)
125-
else await ex
126-
for ex in execution_results
127-
]
125+
await asyncio.gather(
126+
*(
127+
ex
128+
if ex is not None and is_awaitable(ex)
129+
else wrap_in_async(lambda: ex)()
130+
for ex in execution_results
131+
)
132+
)
128133
if self.enable_async
129134
else execution_results
130135
)

graphql_server/utils.py

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import sys
2+
from typing import Awaitable, Callable, TypeVar
3+
4+
if sys.version_info >= (3, 10):
5+
from typing import ParamSpec
6+
else:
7+
from typing_extensions import ParamSpec
8+
9+
10+
__all__ = ["wrap_in_async"]
11+
12+
P = ParamSpec("P")
13+
R = TypeVar("R")
14+
15+
16+
def wrap_in_async(f: Callable[P, R]) -> Callable[P, Awaitable[R]]:
17+
"""Convert a sync callable (normal def or lambda) to a coroutine (async def).
18+
19+
This is similar to asyncio.coroutine which was deprecated in Python 3.8.
20+
"""
21+
22+
async def f_async(*args: P.args, **kwargs: P.kwargs) -> R:
23+
return f(*args, **kwargs)
24+
25+
return f_async

tests/quart/app.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,11 @@
44
from tests.quart.schema import Schema
55

66

7-
def create_app(path="/graphql", **kwargs):
7+
def create_app(path="/graphql", schema=Schema, **kwargs):
88
server = Quart(__name__)
99
server.debug = True
1010
server.add_url_rule(
11-
path, view_func=GraphQLView.as_view("graphql", schema=Schema, **kwargs)
11+
path, view_func=GraphQLView.as_view("graphql", schema=schema, **kwargs)
1212
)
1313
return server
1414

tests/quart/schema.py

+49-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import asyncio
2+
13
from graphql.type.definition import (
24
GraphQLArgument,
35
GraphQLField,
@@ -12,6 +14,7 @@ def resolve_raises(*_):
1214
raise Exception("Throws!")
1315

1416

17+
# Sync schema
1518
QueryRootType = GraphQLObjectType(
1619
name="QueryRoot",
1720
fields={
@@ -36,7 +39,7 @@ def resolve_raises(*_):
3639
"test": GraphQLField(
3740
type_=GraphQLString,
3841
args={"who": GraphQLArgument(GraphQLString)},
39-
resolve=lambda obj, info, who="World": "Hello %s" % who,
42+
resolve=lambda obj, info, who="World": f"Hello {who}",
4043
),
4144
},
4245
)
@@ -49,3 +52,48 @@ def resolve_raises(*_):
4952
)
5053

5154
Schema = GraphQLSchema(QueryRootType, MutationRootType)
55+
56+
57+
# Schema with async methods
58+
async def resolver_field_async_1(_obj, info):
59+
await asyncio.sleep(0.001)
60+
return "hey"
61+
62+
63+
async def resolver_field_async_2(_obj, info):
64+
await asyncio.sleep(0.003)
65+
return "hey2"
66+
67+
68+
def resolver_field_sync(_obj, info):
69+
return "hey3"
70+
71+
72+
AsyncQueryType = GraphQLObjectType(
73+
name="AsyncQueryType",
74+
fields={
75+
"a": GraphQLField(GraphQLString, resolve=resolver_field_async_1),
76+
"b": GraphQLField(GraphQLString, resolve=resolver_field_async_2),
77+
"c": GraphQLField(GraphQLString, resolve=resolver_field_sync),
78+
},
79+
)
80+
81+
82+
def resolver_field_sync_1(_obj, info):
83+
return "synced_one"
84+
85+
86+
def resolver_field_sync_2(_obj, info):
87+
return "synced_two"
88+
89+
90+
SyncQueryType = GraphQLObjectType(
91+
"SyncQueryType",
92+
{
93+
"a": GraphQLField(GraphQLString, resolve=resolver_field_sync_1),
94+
"b": GraphQLField(GraphQLString, resolve=resolver_field_sync_2),
95+
},
96+
)
97+
98+
AsyncSchema = GraphQLSchema(AsyncQueryType)
99+
SyncSchema = GraphQLSchema(SyncQueryType)

tests/quart/test_graphqlview.py

+15
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
from ..utils import RepeatExecutionContext
1111
from .app import create_app
12+
from .schema import AsyncSchema
1213

1314

1415
@pytest.fixture
@@ -736,6 +737,20 @@ async def test_batch_allows_post_with_operation_name(
736737
]
737738

738739

740+
@pytest.mark.asyncio
741+
@pytest.mark.parametrize("app", [create_app(schema=AsyncSchema, enable_async=True)])
742+
async def test_async_schema(app, client):
743+
response = await execute_client(
744+
app,
745+
client,
746+
query="{a,b,c}",
747+
)
748+
749+
assert response.status_code == 200
750+
result = await response.get_data(as_text=True)
751+
assert response_json(result) == {"data": {"a": "hey", "b": "hey2", "c": "hey3"}}
752+
753+
739754
@pytest.mark.asyncio
740755
@pytest.mark.parametrize(
741756
"app", [create_app(execution_context_class=RepeatExecutionContext)]

0 commit comments

Comments
 (0)