Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP defer support #3753

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
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 @@
)

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 @@
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 @@
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 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 @@
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 @@

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
Loading