Skip to content

Commit

Permalink
WIP defer support
Browse files Browse the repository at this point in the history
  • Loading branch information
patrick91 committed Jan 14, 2025
1 parent fa5c2d0 commit 573fa72
Show file tree
Hide file tree
Showing 12 changed files with 291 additions and 157 deletions.
3 changes: 3 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Release type: minor

@defer 👀
16 changes: 8 additions & 8 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ build-backend = "poetry.core.masonry.api"

[tool.poetry.dependencies]
python = "^3.9"
graphql-core = ">=3.2.0,<3.4.0"
graphql-core = ">=3.2.0"
typing-extensions = ">=4.5.0"
python-dateutil = "^2.7.0"
starlette = {version = ">=0.18.0", optional = true}
Expand Down Expand Up @@ -102,6 +102,7 @@ types-deprecated = "^1.2.15.20241117"
types-six = "^1.17.0.20241205"
types-pyyaml = "^6.0.12.20240917"
mypy = "^1.13.0"
graphql-core = "3.3.0a6"

[tool.poetry.group.integrations]
optional = true
Expand Down
101 changes: 96 additions & 5 deletions strawberry/http/async_base_view.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,18 @@

from graphql import GraphQLError

# TODO: only import this if exists
from graphql.execution.execute import (

Check warning on line 21 in strawberry/http/async_base_view.py

View check run for this annotation

Codecov / codecov/patch

strawberry/http/async_base_view.py#L21

Added line #L21 was not covered by tests
ExperimentalIncrementalExecutionResults,
InitialIncrementalExecutionResult,
)
from graphql.execution.incremental_publisher import (

Check warning on line 25 in strawberry/http/async_base_view.py

View check run for this annotation

Codecov / codecov/patch

strawberry/http/async_base_view.py#L25

Added line #L25 was not covered by tests
IncrementalDeferResult,
IncrementalResult,
IncrementalStreamResult,
SubsequentIncrementalExecutionResult,
)

from strawberry.exceptions import MissingQueryError
from strawberry.file_uploads.utils import replace_placeholders_with_files
from strawberry.http import (
Expand Down Expand Up @@ -337,6 +349,29 @@ async def run(
except MissingQueryError as e:
raise HTTPException(400, "No GraphQL query found in the request") from e

if isinstance(result, ExperimentalIncrementalExecutionResults):

async def stream():
yield "---"
response = await self.process_result(request, result.initial_result)
yield self.encode_multipart_data(response, "-")

Check warning on line 357 in strawberry/http/async_base_view.py

View check run for this annotation

Codecov / codecov/patch

strawberry/http/async_base_view.py#L354-L357

Added lines #L354 - L357 were not covered by tests

async for value in result.subsequent_results:
response = await self.process_subsequent_result(request, value)
yield self.encode_multipart_data(response, "-")

Check warning on line 361 in strawberry/http/async_base_view.py

View check run for this annotation

Codecov / codecov/patch

strawberry/http/async_base_view.py#L360-L361

Added lines #L360 - L361 were not covered by tests

yield "--\r\n"

Check warning on line 363 in strawberry/http/async_base_view.py

View check run for this annotation

Codecov / codecov/patch

strawberry/http/async_base_view.py#L363

Added line #L363 was not covered by tests

return await self.create_streaming_response(

Check warning on line 365 in strawberry/http/async_base_view.py

View check run for this annotation

Codecov / codecov/patch

strawberry/http/async_base_view.py#L365

Added line #L365 was not covered by tests
request,
stream,
sub_response,
headers={
"Transfer-Encoding": "chunked",
"Content-Type": 'multipart/mixed; boundary="-"',
},
)

if isinstance(result, SubscriptionExecutionResult):
stream = self._get_stream(request, result)

Expand All @@ -360,12 +395,15 @@ async def run(
)

def encode_multipart_data(self, data: Any, separator: str) -> str:
encoded_data = self.encode_json(data)

Check warning on line 398 in strawberry/http/async_base_view.py

View check run for this annotation

Codecov / codecov/patch

strawberry/http/async_base_view.py#L398

Added line #L398 was not covered by tests

return "".join(
[
f"\r\n--{separator}\r\n",
"Content-Type: application/json\r\n\r\n",
self.encode_json(data),
"\n",
"\r\n",
"Content-Type: application/json; charset=utf-8\r\n",
"\r\n",
encoded_data,
f"\r\n--{separator}",
]
)

Expand Down Expand Up @@ -475,9 +513,62 @@ async def parse_http_body(
protocol=protocol,
)

def process_incremental_result(

Check warning on line 516 in strawberry/http/async_base_view.py

View check run for this annotation

Codecov / codecov/patch

strawberry/http/async_base_view.py#L516

Added line #L516 was not covered by tests
self, request: Request, result: IncrementalResult
) -> GraphQLHTTPResponse:
if isinstance(result, IncrementalDeferResult):
return {

Check warning on line 520 in strawberry/http/async_base_view.py

View check run for this annotation

Codecov / codecov/patch

strawberry/http/async_base_view.py#L520

Added line #L520 was not covered by tests
"data": result.data,
"errors": result.errors,
"path": result.path,
"label": result.label,
"extensions": result.extensions,
}
if isinstance(result, IncrementalStreamResult):
return {

Check warning on line 528 in strawberry/http/async_base_view.py

View check run for this annotation

Codecov / codecov/patch

strawberry/http/async_base_view.py#L528

Added line #L528 was not covered by tests
"items": result.items,
"errors": result.errors,
"path": result.path,
"label": result.label,
"extensions": result.extensions,
}

raise ValueError(f"Unsupported incremental result type: {type(result)}")

Check warning on line 536 in strawberry/http/async_base_view.py

View check run for this annotation

Codecov / codecov/patch

strawberry/http/async_base_view.py#L536

Added line #L536 was not covered by tests

async def process_subsequent_result(

Check warning on line 538 in strawberry/http/async_base_view.py

View check run for this annotation

Codecov / codecov/patch

strawberry/http/async_base_view.py#L538

Added line #L538 was not covered by tests
self,
request: Request,
result: SubsequentIncrementalExecutionResult,
# TODO: use proper return type
) -> GraphQLHTTPResponse:
data = {

Check warning on line 544 in strawberry/http/async_base_view.py

View check run for this annotation

Codecov / codecov/patch

strawberry/http/async_base_view.py#L544

Added line #L544 was not covered by tests
"incremental": [
await self.process_result(request, value)
for value in result.incremental
],
"hasNext": result.has_next,
"extensions": result.extensions,
}

return data

Check warning on line 553 in strawberry/http/async_base_view.py

View check run for this annotation

Codecov / codecov/patch

strawberry/http/async_base_view.py#L553

Added line #L553 was not covered by tests

async def process_result(
self, request: Request, result: ExecutionResult
self,
request: Request,
result: Union[ExecutionResult, InitialIncrementalExecutionResult],
) -> GraphQLHTTPResponse:
if isinstance(result, InitialIncrementalExecutionResult):
return {

Check warning on line 561 in strawberry/http/async_base_view.py

View check run for this annotation

Codecov / codecov/patch

strawberry/http/async_base_view.py#L561

Added line #L561 was not covered by tests
"data": result.data,
"incremental": [
self.process_incremental_result(request, value)
for value in result.incremental
]
if result.incremental
else [],
"hasNext": result.has_next,
"extensions": result.extensions,
}
return process_result(result)

async def on_ws_connect(
Expand Down
49 changes: 26 additions & 23 deletions strawberry/schema/execute.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

from graphql import ExecutionResult as GraphQLExecutionResult
from graphql import GraphQLError, parse
from graphql import execute as original_execute
from graphql.execution import experimental_execute_incrementally
from graphql.validation import validate

from strawberry.exceptions import MissingQueryError
Expand Down Expand Up @@ -121,16 +121,17 @@ async def _handle_execution_result(
extensions_runner: SchemaExtensionsRunner,
process_errors: ProcessErrors | None,
) -> ExecutionResult:
# Set errors on the context so that it's easier
# to access in extensions
if result.errors:
context.errors = result.errors
if process_errors:
process_errors(result.errors, context)
if isinstance(result, GraphQLExecutionResult):
result = ExecutionResult(data=result.data, errors=result.errors)
result.extensions = await extensions_runner.get_extensions_results(context)
context.result = result # type: ignore # mypy failed to deduce correct type.
# TODO: deal with this later
# # Set errors on the context so that it's easier
# # to access in extensions
# if result.errors:
# context.errors = result.errors
# if process_errors:
# process_errors(result.errors, context)
# if isinstance(result, GraphQLExecutionResult):
# result = ExecutionResult(data=result.data, errors=result.errors)
# result.extensions = await extensions_runner.get_extensions_results(context)
# context.result = result # type: ignore # mypy failed to deduce correct type.
return result


Expand Down Expand Up @@ -164,7 +165,7 @@ async def execute(
async with extensions_runner.executing():
if not execution_context.result:
result = await await_maybe(
original_execute(
experimental_execute_incrementally(
schema,
execution_context.graphql_document,
root_value=execution_context.root_value,
Expand All @@ -178,16 +179,18 @@ async def execute(
execution_context.result = result
else:
result = execution_context.result
# Also set errors on the execution_context so that it's easier
# to access in extensions
if result.errors:
execution_context.errors = result.errors

# Run the `Schema.process_errors` function here before
# extensions have a chance to modify them (see the MaskErrors
# extension). That way we can log the original errors but
# only return a sanitised version to the client.
process_errors(result.errors, execution_context)
# TODO: deal with this later
# # Also set errors on the execution_context so that it's easier
# # to access in extensions
# breakpoint()
# if result.errors:
# execution_context.errors = result.errors

# # Run the `Schema.process_errors` function here before
# # extensions have a chance to modify them (see the MaskErrors
# # extension). That way we can log the original errors but
# # only return a sanitised version to the client.
# process_errors(result.errors, execution_context)

except (MissingQueryError, InvalidOperationTypeError):
raise
Expand Down Expand Up @@ -252,7 +255,7 @@ def execute_sync(

with extensions_runner.executing():
if not execution_context.result:
result = original_execute(
result = experimental_execute_incrementally(

Check warning on line 258 in strawberry/schema/execute.py

View check run for this annotation

Codecov / codecov/patch

strawberry/schema/execute.py#L258

Added line #L258 was not covered by tests
schema,
execution_context.graphql_document,
root_value=execution_context.root_value,
Expand Down
12 changes: 10 additions & 2 deletions strawberry/schema/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,11 @@
validate_schema,
)
from graphql.execution.middleware import MiddlewareManager
from graphql.type.directives import specified_directives
from graphql.type.directives import (
GraphQLDeferDirective,
GraphQLStreamDirective,
specified_directives,
)

from strawberry import relay
from strawberry.annotation import StrawberryAnnotation
Expand Down Expand Up @@ -194,7 +198,11 @@ class Query:
query=query_type,
mutation=mutation_type,
subscription=subscription_type if subscription else None,
directives=specified_directives + tuple(graphql_directives),
directives=(
specified_directives
+ tuple(graphql_directives)
+ (GraphQLDeferDirective, GraphQLStreamDirective)
),
types=graphql_types,
extensions={
GraphQLCoreConverter.DEFINITION_BACKREF: self,
Expand Down
7 changes: 2 additions & 5 deletions strawberry/static/graphiql.html
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,7 @@
<link
crossorigin
rel="stylesheet"
href="https://unpkg.com/[email protected]/graphiql.min.css"
integrity="sha384-yz3/sqpuplkA7msMo0FE4ekg0xdwdvZ8JX9MVZREsxipqjU4h8IRfmAMRcb1QpUy"
href="https://unpkg.com/[email protected]/graphiql.min.css"
/>

<link
Expand All @@ -77,13 +76,11 @@
<div id="graphiql" class="graphiql-container">Loading...</div>
<script
crossorigin
src="https://unpkg.com/[email protected]/graphiql.min.js"
integrity="sha384-Mjte+vxCWz1ZYCzszGHiJqJa5eAxiqI4mc3BErq7eDXnt+UGLXSEW7+i0wmfPiji"
src="https://unpkg.com/[email protected]/graphiql.min.js"
></script>
<script
crossorigin
src="https://unpkg.com/@graphiql/[email protected]/dist/index.umd.js"
integrity="sha384-2oonKe47vfHIZnmB6ZZ10vl7T0Y+qrHQF2cmNTaFDuPshpKqpUMGMc9jgj9MLDZ9"
></script>
<script>
const EXAMPLE_QUERY = `# Welcome to GraphiQL 🍓
Expand Down
Empty file.
44 changes: 44 additions & 0 deletions tests/http/incremental/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import contextlib

Check warning on line 1 in tests/http/incremental/conftest.py

View check run for this annotation

Codecov / codecov/patch

tests/http/incremental/conftest.py#L1

Added line #L1 was not covered by tests

import pytest

Check warning on line 3 in tests/http/incremental/conftest.py

View check run for this annotation

Codecov / codecov/patch

tests/http/incremental/conftest.py#L3

Added line #L3 was not covered by tests

from tests.http.clients.base import HttpClient

Check warning on line 5 in tests/http/incremental/conftest.py

View check run for this annotation

Codecov / codecov/patch

tests/http/incremental/conftest.py#L5

Added line #L5 was not covered by tests


@pytest.fixture
def http_client(http_client_class: type[HttpClient]) -> HttpClient:
with contextlib.suppress(ImportError):
import django

Check warning on line 11 in tests/http/incremental/conftest.py

View check run for this annotation

Codecov / codecov/patch

tests/http/incremental/conftest.py#L8-L11

Added lines #L8 - L11 were not covered by tests

if django.VERSION < (4, 2):
pytest.skip(reason="Django < 4.2 doesn't async streaming responses")

Check warning on line 14 in tests/http/incremental/conftest.py

View check run for this annotation

Codecov / codecov/patch

tests/http/incremental/conftest.py#L14

Added line #L14 was not covered by tests

from tests.http.clients.django import DjangoHttpClient

Check warning on line 16 in tests/http/incremental/conftest.py

View check run for this annotation

Codecov / codecov/patch

tests/http/incremental/conftest.py#L16

Added line #L16 was not covered by tests

if http_client_class is DjangoHttpClient:
pytest.skip(reason="(sync) DjangoHttpClient doesn't support streaming")

Check warning on line 19 in tests/http/incremental/conftest.py

View check run for this annotation

Codecov / codecov/patch

tests/http/incremental/conftest.py#L19

Added line #L19 was not covered by tests

with contextlib.suppress(ImportError):
from tests.http.clients.channels import SyncChannelsHttpClient

Check warning on line 22 in tests/http/incremental/conftest.py

View check run for this annotation

Codecov / codecov/patch

tests/http/incremental/conftest.py#L21-L22

Added lines #L21 - L22 were not covered by tests

# TODO: why do we have a sync channels client?
if http_client_class is SyncChannelsHttpClient:
pytest.skip(reason="SyncChannelsHttpClient doesn't support streaming")

Check warning on line 26 in tests/http/incremental/conftest.py

View check run for this annotation

Codecov / codecov/patch

tests/http/incremental/conftest.py#L26

Added line #L26 was not covered by tests

with contextlib.suppress(ImportError):
from tests.http.clients.async_flask import AsyncFlaskHttpClient
from tests.http.clients.flask import FlaskHttpClient

Check warning on line 30 in tests/http/incremental/conftest.py

View check run for this annotation

Codecov / codecov/patch

tests/http/incremental/conftest.py#L28-L30

Added lines #L28 - L30 were not covered by tests

if http_client_class is FlaskHttpClient:
pytest.skip(reason="FlaskHttpClient doesn't support streaming")

Check warning on line 33 in tests/http/incremental/conftest.py

View check run for this annotation

Codecov / codecov/patch

tests/http/incremental/conftest.py#L33

Added line #L33 was not covered by tests

if http_client_class is AsyncFlaskHttpClient:
pytest.xfail(reason="AsyncFlaskHttpClient doesn't support streaming")

Check warning on line 36 in tests/http/incremental/conftest.py

View check run for this annotation

Codecov / codecov/patch

tests/http/incremental/conftest.py#L36

Added line #L36 was not covered by tests

with contextlib.suppress(ImportError):
from tests.http.clients.chalice import ChaliceHttpClient

Check warning on line 39 in tests/http/incremental/conftest.py

View check run for this annotation

Codecov / codecov/patch

tests/http/incremental/conftest.py#L38-L39

Added lines #L38 - L39 were not covered by tests

if http_client_class is ChaliceHttpClient:
pytest.skip(reason="ChaliceHttpClient doesn't support streaming")

Check warning on line 42 in tests/http/incremental/conftest.py

View check run for this annotation

Codecov / codecov/patch

tests/http/incremental/conftest.py#L42

Added line #L42 was not covered by tests

return http_client_class()

Check warning on line 44 in tests/http/incremental/conftest.py

View check run for this annotation

Codecov / codecov/patch

tests/http/incremental/conftest.py#L44

Added line #L44 was not covered by tests
Loading

0 comments on commit 573fa72

Please sign in to comment.