Skip to content

Commit 9e7d000

Browse files
committed
WIP defer support
1 parent fa5c2d0 commit 9e7d000

12 files changed

+291
-157
lines changed

RELEASE.md

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Release type: minor
2+
3+
@defer 👀

poetry.lock

+8-8
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

+2-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ build-backend = "poetry.core.masonry.api"
3535

3636
[tool.poetry.dependencies]
3737
python = "^3.9"
38-
graphql-core = ">=3.2.0,<3.4.0"
38+
graphql-core = ">=3.2.0"
3939
typing-extensions = ">=4.5.0"
4040
python-dateutil = "^2.7.0"
4141
starlette = {version = ">=0.18.0", optional = true}
@@ -102,6 +102,7 @@ types-deprecated = "^1.2.15.20241117"
102102
types-six = "^1.17.0.20241205"
103103
types-pyyaml = "^6.0.12.20240917"
104104
mypy = "^1.13.0"
105+
graphql-core = "3.3.0a6"
105106

106107
[tool.poetry.group.integrations]
107108
optional = true

strawberry/http/async_base_view.py

+96-5
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,18 @@
1717

1818
from graphql import GraphQLError
1919

20+
# TODO: only import this if exists
21+
from graphql.execution.execute import (
22+
ExperimentalIncrementalExecutionResults,
23+
InitialIncrementalExecutionResult,
24+
)
25+
from graphql.execution.incremental_publisher import (
26+
IncrementalDeferResult,
27+
IncrementalResult,
28+
IncrementalStreamResult,
29+
SubsequentIncrementalExecutionResult,
30+
)
31+
2032
from strawberry.exceptions import MissingQueryError
2133
from strawberry.file_uploads.utils import replace_placeholders_with_files
2234
from strawberry.http import (
@@ -337,6 +349,29 @@ async def run(
337349
except MissingQueryError as e:
338350
raise HTTPException(400, "No GraphQL query found in the request") from e
339351

352+
if isinstance(result, ExperimentalIncrementalExecutionResults):
353+
354+
async def stream():
355+
yield "---"
356+
response = await self.process_result(request, result.initial_result)
357+
yield self.encode_multipart_data(response, "-")
358+
359+
async for value in result.subsequent_results:
360+
response = await self.process_subsequent_result(request, value)
361+
yield self.encode_multipart_data(response, "-")
362+
363+
yield "--\r\n"
364+
365+
return await self.create_streaming_response(
366+
request,
367+
stream,
368+
sub_response,
369+
headers={
370+
"Transfer-Encoding": "chunked",
371+
"Content-Type": 'multipart/mixed; boundary="-"',
372+
},
373+
)
374+
340375
if isinstance(result, SubscriptionExecutionResult):
341376
stream = self._get_stream(request, result)
342377

@@ -360,12 +395,15 @@ async def run(
360395
)
361396

362397
def encode_multipart_data(self, data: Any, separator: str) -> str:
398+
encoded_data = self.encode_json(data)
399+
363400
return "".join(
364401
[
365-
f"\r\n--{separator}\r\n",
366-
"Content-Type: application/json\r\n\r\n",
367-
self.encode_json(data),
368-
"\n",
402+
"\r\n",
403+
"Content-Type: application/json; charset=utf-8\r\n",
404+
"\r\n",
405+
encoded_data,
406+
f"\r\n--{separator}",
369407
]
370408
)
371409

@@ -475,9 +513,62 @@ async def parse_http_body(
475513
protocol=protocol,
476514
)
477515

516+
def process_incremental_result(
517+
self, request: Request, result: IncrementalResult
518+
) -> GraphQLHTTPResponse:
519+
if isinstance(result, IncrementalDeferResult):
520+
return {
521+
"data": result.data,
522+
"errors": result.errors,
523+
"path": result.path,
524+
"label": result.label,
525+
"extensions": result.extensions,
526+
}
527+
if isinstance(result, IncrementalStreamResult):
528+
return {
529+
"items": result.items,
530+
"errors": result.errors,
531+
"path": result.path,
532+
"label": result.label,
533+
"extensions": result.extensions,
534+
}
535+
536+
raise ValueError(f"Unsupported incremental result type: {type(result)}")
537+
538+
async def process_subsequent_result(
539+
self,
540+
request: Request,
541+
result: SubsequentIncrementalExecutionResult,
542+
# TODO: use proper return type
543+
) -> GraphQLHTTPResponse:
544+
data = {
545+
"incremental": [
546+
await self.process_result(request, value)
547+
for value in result.incremental
548+
],
549+
"hasNext": result.has_next,
550+
"extensions": result.extensions,
551+
}
552+
553+
return data
554+
478555
async def process_result(
479-
self, request: Request, result: ExecutionResult
556+
self,
557+
request: Request,
558+
result: ExecutionResult | InitialIncrementalExecutionResult,
480559
) -> GraphQLHTTPResponse:
560+
if isinstance(result, InitialIncrementalExecutionResult):
561+
return {
562+
"data": result.data,
563+
"incremental": [
564+
self.process_incremental_result(request, value)
565+
for value in result.incremental
566+
]
567+
if result.incremental
568+
else [],
569+
"hasNext": result.has_next,
570+
"extensions": result.extensions,
571+
}
481572
return process_result(result)
482573

483574
async def on_ws_connect(

strawberry/schema/execute.py

+26-23
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
from graphql import ExecutionResult as GraphQLExecutionResult
1616
from graphql import GraphQLError, parse
17-
from graphql import execute as original_execute
17+
from graphql.execution import experimental_execute_incrementally
1818
from graphql.validation import validate
1919

2020
from strawberry.exceptions import MissingQueryError
@@ -121,16 +121,17 @@ async def _handle_execution_result(
121121
extensions_runner: SchemaExtensionsRunner,
122122
process_errors: ProcessErrors | None,
123123
) -> ExecutionResult:
124-
# Set errors on the context so that it's easier
125-
# to access in extensions
126-
if result.errors:
127-
context.errors = result.errors
128-
if process_errors:
129-
process_errors(result.errors, context)
130-
if isinstance(result, GraphQLExecutionResult):
131-
result = ExecutionResult(data=result.data, errors=result.errors)
132-
result.extensions = await extensions_runner.get_extensions_results(context)
133-
context.result = result # type: ignore # mypy failed to deduce correct type.
124+
# TODO: deal with this later
125+
# # Set errors on the context so that it's easier
126+
# # to access in extensions
127+
# if result.errors:
128+
# context.errors = result.errors
129+
# if process_errors:
130+
# process_errors(result.errors, context)
131+
# if isinstance(result, GraphQLExecutionResult):
132+
# result = ExecutionResult(data=result.data, errors=result.errors)
133+
# result.extensions = await extensions_runner.get_extensions_results(context)
134+
# context.result = result # type: ignore # mypy failed to deduce correct type.
134135
return result
135136

136137

@@ -164,7 +165,7 @@ async def execute(
164165
async with extensions_runner.executing():
165166
if not execution_context.result:
166167
result = await await_maybe(
167-
original_execute(
168+
experimental_execute_incrementally(
168169
schema,
169170
execution_context.graphql_document,
170171
root_value=execution_context.root_value,
@@ -178,16 +179,18 @@ async def execute(
178179
execution_context.result = result
179180
else:
180181
result = execution_context.result
181-
# Also set errors on the execution_context so that it's easier
182-
# to access in extensions
183-
if result.errors:
184-
execution_context.errors = result.errors
185-
186-
# Run the `Schema.process_errors` function here before
187-
# extensions have a chance to modify them (see the MaskErrors
188-
# extension). That way we can log the original errors but
189-
# only return a sanitised version to the client.
190-
process_errors(result.errors, execution_context)
182+
# TODO: deal with this later
183+
# # Also set errors on the execution_context so that it's easier
184+
# # to access in extensions
185+
# breakpoint()
186+
# if result.errors:
187+
# execution_context.errors = result.errors
188+
189+
# # Run the `Schema.process_errors` function here before
190+
# # extensions have a chance to modify them (see the MaskErrors
191+
# # extension). That way we can log the original errors but
192+
# # only return a sanitised version to the client.
193+
# process_errors(result.errors, execution_context)
191194

192195
except (MissingQueryError, InvalidOperationTypeError):
193196
raise
@@ -252,7 +255,7 @@ def execute_sync(
252255

253256
with extensions_runner.executing():
254257
if not execution_context.result:
255-
result = original_execute(
258+
result = experimental_execute_incrementally(
256259
schema,
257260
execution_context.graphql_document,
258261
root_value=execution_context.root_value,

strawberry/schema/schema.py

+10-2
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,11 @@
2020
validate_schema,
2121
)
2222
from graphql.execution.middleware import MiddlewareManager
23-
from graphql.type.directives import specified_directives
23+
from graphql.type.directives import (
24+
GraphQLDeferDirective,
25+
GraphQLStreamDirective,
26+
specified_directives,
27+
)
2428

2529
from strawberry import relay
2630
from strawberry.annotation import StrawberryAnnotation
@@ -194,7 +198,11 @@ class Query:
194198
query=query_type,
195199
mutation=mutation_type,
196200
subscription=subscription_type if subscription else None,
197-
directives=specified_directives + tuple(graphql_directives),
201+
directives=(
202+
specified_directives
203+
+ tuple(graphql_directives)
204+
+ (GraphQLDeferDirective, GraphQLStreamDirective)
205+
),
198206
types=graphql_types,
199207
extensions={
200208
GraphQLCoreConverter.DEFINITION_BACKREF: self,

strawberry/static/graphiql.html

+2-5
Original file line numberDiff line numberDiff line change
@@ -61,8 +61,7 @@
6161
<link
6262
crossorigin
6363
rel="stylesheet"
64-
href="https://unpkg.com/[email protected]/graphiql.min.css"
65-
integrity="sha384-yz3/sqpuplkA7msMo0FE4ekg0xdwdvZ8JX9MVZREsxipqjU4h8IRfmAMRcb1QpUy"
64+
href="https://unpkg.com/[email protected]/graphiql.min.css"
6665
/>
6766

6867
<link
@@ -77,13 +76,11 @@
7776
<div id="graphiql" class="graphiql-container">Loading...</div>
7877
<script
7978
crossorigin
80-
src="https://unpkg.com/[email protected]/graphiql.min.js"
81-
integrity="sha384-Mjte+vxCWz1ZYCzszGHiJqJa5eAxiqI4mc3BErq7eDXnt+UGLXSEW7+i0wmfPiji"
79+
src="https://unpkg.com/[email protected]/graphiql.min.js"
8280
></script>
8381
<script
8482
crossorigin
8583
src="https://unpkg.com/@graphiql/[email protected]/dist/index.umd.js"
86-
integrity="sha384-2oonKe47vfHIZnmB6ZZ10vl7T0Y+qrHQF2cmNTaFDuPshpKqpUMGMc9jgj9MLDZ9"
8784
></script>
8885
<script>
8986
const EXAMPLE_QUERY = `# Welcome to GraphiQL 🍓

tests/http/incremental/__init__.py

Whitespace-only changes.

tests/http/incremental/conftest.py

+44
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import contextlib
2+
3+
import pytest
4+
5+
from tests.http.clients.base import HttpClient
6+
7+
8+
@pytest.fixture
9+
def http_client(http_client_class: type[HttpClient]) -> HttpClient:
10+
with contextlib.suppress(ImportError):
11+
import django
12+
13+
if django.VERSION < (4, 2):
14+
pytest.skip(reason="Django < 4.2 doesn't async streaming responses")
15+
16+
from tests.http.clients.django import DjangoHttpClient
17+
18+
if http_client_class is DjangoHttpClient:
19+
pytest.skip(reason="(sync) DjangoHttpClient doesn't support streaming")
20+
21+
with contextlib.suppress(ImportError):
22+
from tests.http.clients.channels import SyncChannelsHttpClient
23+
24+
# TODO: why do we have a sync channels client?
25+
if http_client_class is SyncChannelsHttpClient:
26+
pytest.skip(reason="SyncChannelsHttpClient doesn't support streaming")
27+
28+
with contextlib.suppress(ImportError):
29+
from tests.http.clients.async_flask import AsyncFlaskHttpClient
30+
from tests.http.clients.flask import FlaskHttpClient
31+
32+
if http_client_class is FlaskHttpClient:
33+
pytest.skip(reason="FlaskHttpClient doesn't support streaming")
34+
35+
if http_client_class is AsyncFlaskHttpClient:
36+
pytest.xfail(reason="AsyncFlaskHttpClient doesn't support streaming")
37+
38+
with contextlib.suppress(ImportError):
39+
from tests.http.clients.chalice import ChaliceHttpClient
40+
41+
if http_client_class is ChaliceHttpClient:
42+
pytest.skip(reason="ChaliceHttpClient doesn't support streaming")
43+
44+
return http_client_class()

0 commit comments

Comments
 (0)