Skip to content

Commit 7124741

Browse files
committed
Implemented async queries support
1 parent 42a9ce0 commit 7124741

9 files changed

Lines changed: 271 additions & 89 deletions

File tree

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ This will add `/graphql` endpoint to your app.
2626

2727
### Sharing eventloop with Sanic
2828

29-
In order to pass Sanic’s eventloop to GraphQL’s `AsyncioExecutor`, use `before_start` event:
29+
In order to pass Sanic’s eventloop to GraphQL’s `AsyncioExecutor`, use `before_start` listener:
3030

3131
```python
3232
def before_start(app, loop):
@@ -41,7 +41,7 @@ app.run(before_start=before_start)
4141
- `context`: A value to pass as the `context` to the `graphql()` function.
4242
- `root_value`: The `root_value` you want to provide to `executor.execute`.
4343
- `pretty`: Whether or not you want the response to be pretty printed JSON.
44-
- `executor`: The `Executor` that you want to use to execute queries.
44+
- `executor`: The `Executor` that you want to use to execute queries. If an `AsyncExecutor` instance is provided, performs queries asynchronously within executor’s loop.
4545
- `graphiql`: If `True`, may present [GraphiQL] when loaded directly from a browser (a useful tool for debugging and exploration).
4646
- `graphiql_template`: Inject a Jinja template string to customize GraphiQL.
4747
- `jinja_env`: Sets jinja environment to be used to process GraphiQL template. If Jinja’s async mode is enabled (by `enable_async=True`), uses

README.rst

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -24,9 +24,9 @@ Just use the ``GraphQLView`` view from ``sanic_graphql``
2424
This will add ``/graphql`` endpoint to your app.
2525

2626
Sharing eventloop with Sanic
27-
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
27+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
2828

29-
In order to pass Sanic's eventloop to GraphQL's ``AsyncioExecutor``, use ``before_start`` event:
29+
In order to pass Sanic's eventloop to GraphQL's ``AsyncioExecutor``, use ``before_start`` listener:
3030

3131
.. code:: python
3232
@@ -47,16 +47,16 @@ Supported options
4747
``executor.execute``.
4848
- ``pretty``: Whether or not you want the response to be pretty printed
4949
JSON.
50-
- ``executor``: The ``Executor`` that you want to use to execute
51-
queries.
50+
- ``executor``: The ``Executor`` that you want to use to execute queries. If an ``AsyncExecutor`` instance is provided,
51+
performs queries asynchronously within executor's loop.
5252
- ``graphiql``: If ``True``, may present
5353
`GraphiQL <https://github.com/graphql/graphiql>`__ when loaded
5454
directly from a browser (a useful tool for debugging and
5555
exploration).
5656
- ``graphiql_template``: Inject a Jinja template string to customize
5757
GraphiQL.
58-
- ``jinja_env``: Sets jinja environment to be used to process GraphiQL template. If Jinja's async mode is enabled (by ``enable_async=True``), uses
59-
``Template.render_async`` instead of ``Template.render``. If environment is not set, fallbacks to simple regex-based renderer.
58+
- ``jinja_env``: Sets jinja environment to be used to process GraphiQL template. If Jinja's async mode is enabled (by ``enable_async=True``), uses
59+
``Template.render_async`` instead of ``Template.render``. If environment is not set, fallbacks to simple regex-based renderer.
6060
- ``batch``: Set the GraphQL view as batch (for using in
6161
`Apollo-Client <http://dev.apollodata.com/core/network.html#query-batching>`__
6262
or

sanic_graphql/graphqlview.py

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,12 +6,14 @@
66
from sanic.views import HTTPMethodView
77
from sanic.exceptions import SanicException
88

9+
from promise import Promise
910
from graphql import Source, execute, parse, validate
1011
from graphql.error import format_error as format_graphql_error
1112
from graphql.error import GraphQLError
1213
from graphql.execution import ExecutionResult
1314
from graphql.type.schema import GraphQLSchema
1415
from graphql.utils.get_operation_ast import get_operation_ast
16+
from graphql.execution.executors.asyncio import AsyncioExecutor
1517

1618
from .render_graphiql import render_graphiql
1719

@@ -36,14 +38,19 @@ class GraphQLView(HTTPMethodView):
3638
batch = False
3739
jinja_env = None
3840

41+
_enable_async = True
42+
3943
methods = ['GET', 'POST', 'PUT', 'DELETE']
4044

4145
def __init__(self, **kwargs):
4246
super(GraphQLView, self).__init__()
47+
4348
for key, value in kwargs.items():
4449
if hasattr(self, key):
4550
setattr(self, key, value)
4651

52+
self._enable_async = self._enable_async and isinstance(kwargs.get('executor'), AsyncioExecutor)
53+
4754
assert not all((self.graphiql, self.batch)), 'Use either graphiql or batch processing'
4855
assert isinstance(self.schema, GraphQLSchema), 'A Schema is required to be provided to GraphQLView.'
4956

@@ -71,11 +78,14 @@ async def dispatch_request(self, request, *args, **kwargs):
7178
show_graphiql = self.graphiql and self.can_display_graphiql(request, data)
7279

7380
if self.batch:
74-
responses = [self.get_response(request, entry) for entry in data]
81+
responses = []
82+
for entry in data:
83+
responses.append(await self.get_response(request, entry))
84+
7585
result = '[{}]'.format(','.join([response[0] for response in responses]))
7686
status_code = max(responses, key=lambda response: response[1])[1]
7787
else:
78-
result, status_code = self.get_response(request, data, show_graphiql)
88+
result, status_code = await self.get_response(request, data, show_graphiql)
7989

8090
if show_graphiql:
8191
query, variables, operation_name, id = self.get_graphql_params(request, data)
@@ -105,10 +115,10 @@ async def dispatch_request(self, request, *args, **kwargs):
105115
content_type='application/json'
106116
)
107117

108-
def get_response(self, request, data, show_graphiql=False):
118+
async def get_response(self, request, data, show_graphiql=False):
109119
query, variables, operation_name, id = self.get_graphql_params(request, data)
110120

111-
execution_result = self.execute_graphql_request(
121+
execution_result = await self.execute_graphql_request(
112122
request,
113123
data,
114124
query,
@@ -175,10 +185,14 @@ def parse_body(self, request):
175185

176186
return {}
177187

178-
def execute(self, *args, **kwargs):
179-
return execute(self.schema, *args, **kwargs)
188+
async def execute(self, *args, **kwargs):
189+
result = execute(self.schema, return_promise=self._enable_async, *args, **kwargs)
190+
if isinstance(result, Promise):
191+
return await result
192+
else:
193+
return result
180194

181-
def execute_graphql_request(self, request, data, query, variables, operation_name, show_graphiql=False):
195+
async def execute_graphql_request(self, request, data, query, variables, operation_name, show_graphiql=False):
182196
if not query:
183197
if show_graphiql:
184198
return None
@@ -207,7 +221,7 @@ def execute_graphql_request(self, request, data, query, variables, operation_nam
207221
))
208222

209223
try:
210-
return self.execute(
224+
return await self.execute(
211225
ast,
212226
root_value=self.get_root_value(request),
213227
variable_values=variables or {},

sanic_graphql/render_graphiql.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ def simple_renderer(template, **values):
152152
return template
153153

154154

155-
async def render_graphiql(*, graphiql_version=None, graphiql_template=None, jinja_env=None, **kwargs):
155+
async def render_graphiql(*, jinja_env=None, graphiql_version=None, graphiql_template=None, **kwargs):
156156
graphiql_version = graphiql_version or GRAPHIQL_VERSION
157157
template = graphiql_template or TEMPLATE
158158
kwargs['graphiql_version'] = graphiql_version

tests/app.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,20 @@ def create_app(path='/graphql', **kwargs):
1515
async_executor = kwargs.pop('async_executor', False)
1616

1717
if async_executor:
18-
@app.listener('before_start')
1918
def init_async_executor(app, loop):
2019
executor = AsyncioExecutor(loop)
21-
app.add_route(GraphQLView.as_view(schema=schema, **kwargs), path)
20+
app.add_route(GraphQLView.as_view(schema=schema, executor=executor, **kwargs), path)
21+
22+
def remove_graphql_endpoint(app, loop):
23+
app.remove_route(path)
24+
25+
app.server_kwargs = dict(
26+
before_start=init_async_executor,
27+
after_stop=remove_graphql_endpoint
28+
)
2229
else:
2330
app.add_route(GraphQLView.as_view(schema=schema, **kwargs), path)
31+
app.server_kwargs = dict()
2432

2533
return app
2634

tests/schema.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import asyncio
2+
13
from graphql.type.definition import GraphQLArgument, GraphQLField, GraphQLNonNull, GraphQLObjectType
24
from graphql.type.scalars import GraphQLString
35
from graphql.type.schema import GraphQLSchema
@@ -7,6 +9,7 @@ def resolve_raises(*_):
79
raise Exception("Throws!")
810

911

12+
# Sync schema
1013
QueryRootType = GraphQLObjectType(
1114
name='QueryRoot',
1215
fields={
@@ -36,3 +39,24 @@ def resolve_raises(*_):
3639
)
3740

3841
Schema = GraphQLSchema(QueryRootType, MutationRootType)
42+
43+
44+
# Schema with async methods
45+
async def resolver(context, *_):
46+
await asyncio.sleep(0.001)
47+
return 'hey'
48+
49+
async def resolver_2(context, *_):
50+
await asyncio.sleep(0.003)
51+
return 'hey2'
52+
53+
def resolver_3(context, *_):
54+
return 'hey3'
55+
56+
AsyncQueryType = GraphQLObjectType('AsyncQueryType', {
57+
'a': GraphQLField(GraphQLString, resolver=resolver),
58+
'b': GraphQLField(GraphQLString, resolver=resolver_2),
59+
'c': GraphQLField(GraphQLString, resolver=resolver_3)
60+
})
61+
62+
AsyncSchema = GraphQLSchema(AsyncQueryType)

tests/test_graphiqlview.py

Lines changed: 36 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,61 +2,64 @@
22

33
from jinja2 import Environment
44

5-
from .util import app, client, url_string, create_app
5+
from .app import create_app
6+
from .util import app, client, url_string
67

78

8-
@pytest.mark.parametrize('app', [create_app(graphiql=True)])
9-
def test_graphiql_is_enabled(app, client):
10-
response = client.get(app, uri=url_string(), headers={'Accept': 'text/html'})
11-
assert response.status == 200
12-
13-
14-
@pytest.mark.parametrize('app', [create_app(graphiql=True)])
15-
def test_graphiql_simple_renderer(app, client):
16-
response = client.get(app, uri=url_string(query='{test}'), headers={'Accept': 'text/html'})
17-
assert response.status == 200
18-
pretty_response = (
9+
@pytest.fixture
10+
def pretty_response():
11+
return (
1912
'{\n'
2013
' "data": {\n'
2114
' "test": "Hello World"\n'
2215
' }\n'
2316
'}'
2417
).replace('\"','\\\"').replace('\n', '\\n')
2518

26-
assert pretty_response in response.body.decode('utf-8')
19+
20+
@pytest.mark.parametrize('app', [
21+
(create_app(async_executor=False, graphiql=True)),
22+
(create_app(async_executor=True, graphiql=True)),
23+
])
24+
def test_graphiql_is_enabled(app, client):
25+
response = client.get(app, uri=url_string(), headers={'Accept': 'text/html'})
26+
assert response.status == 200
2727

2828

29-
@pytest.mark.parametrize('app', [create_app(graphiql=True, jinja_env=Environment())])
30-
def test_graphiql_jinja_renderer(app, client):
29+
@pytest.mark.parametrize('app', [
30+
(create_app(async_executor=False, graphiql=True)),
31+
(create_app(async_executor=True, graphiql=True)),
32+
])
33+
def test_graphiql_simple_renderer(app, client, pretty_response):
3134
response = client.get(app, uri=url_string(query='{test}'), headers={'Accept': 'text/html'})
3235
assert response.status == 200
33-
pretty_response = (
34-
'{\n'
35-
' "data": {\n'
36-
' "test": "Hello World"\n'
37-
' }\n'
38-
'}'
39-
).replace('\"','\\\"').replace('\n', '\\n')
40-
4136
assert pretty_response in response.body.decode('utf-8')
4237

4338

44-
@pytest.mark.parametrize('app', [create_app(graphiql=True, jinja_env=Environment(enable_async=True))])
45-
def test_graphiql_jinja_async_renderer(app, client):
39+
@pytest.mark.parametrize('app', [
40+
(create_app(async_executor=False, graphiql=True, jinja_env=Environment())),
41+
(create_app(async_executor=True, graphiql=True, jinja_env=Environment())),
42+
])
43+
def test_graphiql_jinja_renderer(app, client, pretty_response):
4644
response = client.get(app, uri=url_string(query='{test}'), headers={'Accept': 'text/html'})
4745
assert response.status == 200
48-
pretty_response = (
49-
'{\n'
50-
' "data": {\n'
51-
' "test": "Hello World"\n'
52-
' }\n'
53-
'}'
54-
).replace('\"','\\\"').replace('\n', '\\n')
46+
assert pretty_response in response.body.decode('utf-8')
5547

48+
49+
@pytest.mark.parametrize('app', [
50+
(create_app(async_executor=False, graphiql=True, jinja_env=Environment(enable_async=True))),
51+
(create_app(async_executor=True, graphiql=True, jinja_env=Environment(enable_async=True))),
52+
])
53+
def test_graphiql_jinja_async_renderer(app, client, pretty_response):
54+
response = client.get(app, uri=url_string(query='{test}'), headers={'Accept': 'text/html'})
55+
assert response.status == 200
5656
assert pretty_response in response.body.decode('utf-8')
5757

5858

59-
@pytest.mark.parametrize('app', [create_app(graphiql=True)])
59+
@pytest.mark.parametrize('app', [
60+
(create_app(async_executor=False, graphiql=True, jinja_env=Environment())),
61+
(create_app(async_executor=True, graphiql=True, jinja_env=Environment())),
62+
])
6063
def test_graphiql_html_is_not_accepted(app, client):
6164
response = client.get(app, uri=url_string(), headers={'Accept': 'application/json'})
6265
assert response.status == 400

0 commit comments

Comments
 (0)