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

Support subscriptions extensions #3554

Merged
Merged
Show file tree
Hide file tree
Changes from 85 commits
Commits
Show all changes
102 commits
Select commit Hold shift + click to select a range
2c534a1
Support subscriptions extensions
nrbnlulu Jun 4, 2023
6525ee9
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 4, 2023
b92c51b
Add RELEASE.md
nrbnlulu Jun 4, 2023
01a76cd
Merge remote-tracking branch 'origin/support_extensions_on_subscripti…
nrbnlulu Jun 4, 2023
6ff3315
mypy fixes.
nrbnlulu Jun 5, 2023
20bcfed
remove positional only / (py3.7)
nrbnlulu Jun 5, 2023
9ba707a
merge main
nrbnlulu Jul 2, 2024
f69d746
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 2, 2024
060c719
reverts
nrbnlulu Jul 2, 2024
62a130b
intial subscription test pass
nrbnlulu Jul 2, 2024
7b45bfa
wip. pass manager to graphql core
nrbnlulu Jul 2, 2024
e1db94b
wip: migrate to new graphql core
nrbnlulu Jul 2, 2024
70d2f4c
don't use supports resolve
nrbnlulu Jul 2, 2024
7f44c77
revert
nrbnlulu Jul 2, 2024
dc32183
revert unneeded changes
nrbnlulu Jul 3, 2024
5701d21
restore tests; inject execution_context on operation
nrbnlulu Jul 3, 2024
9dcb3fc
don't use class; depecate
nrbnlulu Jul 3, 2024
7e1c770
wip: ensure execution context is distinct.
nrbnlulu Jul 4, 2024
6a84415
wip: first inital successful run after refactor
nrbnlulu Jul 4, 2024
222a4e5
wip: more tests pass
nrbnlulu Jul 4, 2024
4a0f1be
better release.md
nrbnlulu Jul 5, 2024
8fa0cec
all previous extensions tests pass
nrbnlulu Jul 5, 2024
fc7977b
update release.md
nrbnlulu Jul 5, 2024
81914a2
improve tests readability
nrbnlulu Jul 5, 2024
89e501b
test_subscription_success_many_fields pass
nrbnlulu Jul 5, 2024
29b5db8
test_subscription_first_yields_error
nrbnlulu Jul 5, 2024
1438390
test_extensino_results_are_cleared_between_yields
nrbnlulu Jul 5, 2024
cb917ab
test_extensino_results_are_cleared_between_yields
nrbnlulu Jul 5, 2024
ece37b4
fix extensions tests
nrbnlulu Jul 5, 2024
50063c2
ai lints
nrbnlulu Jul 5, 2024
1c65fce
refactor; fix more tests
nrbnlulu Jul 7, 2024
3ab7bb4
ensure `on_execute` and `get_result` hooks are deterministically orde…
nrbnlulu Jul 7, 2024
0533539
handle `on_execute` exceptions.
nrbnlulu Jul 7, 2024
a6680c1
move missing query error before parsing phase.
nrbnlulu Jul 7, 2024
e6ca184
wip: remove unneeded exception handler for `sync_execute`
nrbnlulu Jul 7, 2024
923699a
fix more tests
nrbnlulu Jul 7, 2024
446dc5e
refactor: separate subscription tests from normal tests.
nrbnlulu Jul 7, 2024
39a1beb
fix websocket tests
nrbnlulu Jul 7, 2024
88f2bd5
fix mypy
nrbnlulu Jul 7, 2024
2e9db61
move to graphql-core origin@main
nrbnlulu Jul 8, 2024
9b166a3
nit
nrbnlulu Jul 9, 2024
9c57abd
fix: handle not awaitable result of `original_subscribe`
nrbnlulu Jul 9, 2024
8a8fd2a
add th `assert_next`
nrbnlulu Jul 14, 2024
bdb568b
nits
nrbnlulu Jul 14, 2024
062d9c0
nit
nrbnlulu Jul 14, 2024
9a1c071
nit
nrbnlulu Jul 14, 2024
dd909b3
nits
nrbnlulu Jul 14, 2024
b8f40a1
reorder execute.py
nrbnlulu Jul 14, 2024
61154dc
docs.
nrbnlulu Jul 14, 2024
e455da0
use `.aclose`
nrbnlulu Jul 14, 2024
9a12488
fix mypy issues.
nrbnlulu Jul 14, 2024
f6a924c
fix unused `else`
nrbnlulu Jul 14, 2024
f070858
move execution context injection upwards.
nrbnlulu Jul 14, 2024
fe86760
nit
nrbnlulu Jul 14, 2024
2bdfa9e
add test for when extensios not return anything.
nrbnlulu Jul 15, 2024
f8bf56d
merge main
nrbnlulu Jul 18, 2024
7d702c7
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jul 18, 2024
945465e
wip: remove implements_get_result check
nrbnlulu Jul 23, 2024
5d10ad9
Merge branch 'refs/heads/main' into support_extensions_on_subscriptions
nrbnlulu Jul 24, 2024
da0b46e
Merge branch 'refs/heads/main' into support_extensions_on_subscriptions
nrbnlulu Jul 24, 2024
785d227
working on tests
nrbnlulu Jul 24, 2024
db3180f
schema tests pass
nrbnlulu Jul 24, 2024
4d7fa9e
lints
nrbnlulu Jul 24, 2024
3e55bc2
fix more tests
nrbnlulu Jul 25, 2024
e0f4158
wip: working on tests.
nrbnlulu Jul 25, 2024
a0972a8
fix graphql-transport-ws
nrbnlulu Jul 25, 2024
5567971
graphql-ws tests pass
nrbnlulu Jul 25, 2024
aa7a4bf
tests were always running on 3.3 XD
nrbnlulu Jul 25, 2024
0dc97dd
pass middleware manager only in 3.3
nrbnlulu Jul 25, 2024
e57d115
fix some mypy issues
nrbnlulu Jul 25, 2024
28f6b07
fix more tests.
nrbnlulu Jul 25, 2024
c728171
fix graphql-ws-transport protocol behaviour on (pre)execution errors
nrbnlulu Jul 25, 2024
7f810c5
resolve optimization todos.
nrbnlulu Jul 25, 2024
5b5b7ce
add `long_runnning` subscription benchmark; update release.md; improv…
nrbnlulu Jul 25, 2024
4df5523
fix contextvar issue + few redundant changes.
nrbnlulu Jul 28, 2024
ea1041e
feat: add lazy loading for images in test_subscriptions benchmark
nrbnlulu Aug 1, 2024
a5e5068
typos in readme
nrbnlulu Aug 1, 2024
6d5a754
fix tests.
nrbnlulu Aug 1, 2024
d33dbd7
recify reviews; nits & move `_implements_resolve` to `SchemaExtension`
nrbnlulu Aug 8, 2024
925d3e7
refactor: update GraphQL version check logic in utils/__init__.py
nrbnlulu Aug 8, 2024
4c25c81
Merge branch 'main' into support_extensions_on_subscriptions
nrbnlulu Aug 8, 2024
1fc8c54
Merge branch 'main' into support_extensions_on_subscriptions
nrbnlulu Aug 8, 2024
d98bef2
rectify review comments
nrbnlulu Aug 9, 2024
66b0266
rectify @DoctorJohn review
nrbnlulu Aug 15, 2024
ee1ee1c
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 15, 2024
fd6bdaf
rectify @bellini666 comments
nrbnlulu Aug 18, 2024
26f8437
Merge branch 'main' into support_extensions_on_subscriptions
nrbnlulu Aug 18, 2024
835d1f4
Improve protocol handling for subscriptions
patrick91 Sep 2, 2024
9ce6ff0
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Sep 2, 2024
da3b12a
Lint
patrick91 Sep 2, 2024
7af8bc9
Restore tests
patrick91 Sep 2, 2024
3ba6a83
Update wrong return type
patrick91 Sep 2, 2024
83061ed
Merge branch 'feature/multiple-protocols' into support_extensions_on_…
patrick91 Sep 2, 2024
b65322e
Update doc
patrick91 Sep 2, 2024
87fd45d
Temporarily skip on_execute, see #3613
patrick91 Sep 2, 2024
5d37f83
Update multipart tests
patrick91 Sep 2, 2024
cbd7c90
Fix bad merge
patrick91 Sep 3, 2024
ea1e4fd
Fix type
patrick91 Sep 3, 2024
fdc2e4e
remove unneeded async gen wrapper since we removed support for on_exe…
nrbnlulu Sep 7, 2024
9e0d0aa
Merge branch 'main' into support_extensions_on_subscriptions
patrick91 Sep 10, 2024
27b626b
Update release notes
patrick91 Sep 10, 2024
56e0a6f
Add tweet.md
patrick91 Sep 10, 2024
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
116 changes: 116 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
Release type: minor

Support for schema-extensions in subscriptions.
nrbnlulu marked this conversation as resolved.
Show resolved Hide resolved

nrbnlulu marked this conversation as resolved.
Show resolved Hide resolved
i.e:
```python
nrbnlulu marked this conversation as resolved.
Show resolved Hide resolved
import asyncio
from typing import AsyncIterator

import strawberry
from strawberry.extensions.base_extension import SchemaExtension
from strawberry.types.execution import PreExecutionError


@strawberry.type
class Query:
@strawberry.field
def hello(self) -> str:
return "Hello, world!"


@strawberry.type
class Subscription:
@strawberry.subscription
async def notifications(self, info: strawberry.Info) -> AsyncIterator[str]:
for _ in range(3):
yield "!dlrow ,olleH"


class MyExtension(SchemaExtension):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.count = 0

async def on_operation(self):
# This would run when the subscription starts
print("Subscription started")
yield
# The subscription has ended
print("Subscription ended")

async def on_execute(self):
# The subscription is trying to yield a new result
print(f"before yield {self.count}")
yield
# the subscription has yielded a new result
print(f"after yield {self.count}")
self.count += 1

# Other hooks are the same as in normal execution.
async def resolve(self, _next, root, info, *args, **kwargs):
res = _next(root, info, *args, **kwargs)
return res[::-1]


schema = strawberry.Schema(
query=Query, subscription=Subscription, extensions=[MyExtension]
)


async def main():
agen = await schema.subscribe("subscription { notifications }")
if isinstance(agen, PreExecutionError):
print("this is an initial execution error.")
print(agen.errors)
else:
async for res in agen:
print(res.data)


asyncio.run(main())
```

Should output this
nrbnlulu marked this conversation as resolved.
Show resolved Hide resolved
```console
Subscription started
before yield 0
after yield 0
before yield 1
after yield 1
{'notifications': 'Hello, world!'}
before yield 2
after yield 2
{'notifications': 'Hello, world!'}
before yield 3
after yield 3
{'notifications': 'Hello, world!'}
DoctorJohn marked this conversation as resolved.
Show resolved Hide resolved
before yield 4
after yield 4
Subscription ended
```
### Breaking changes
This release also updates the signature of `Schema.subscribe`.
From:
```py
async def subscribe(
self,
query: str,
variable_values: Optional[Dict[str, Any]] = None,
context_value: Optional[Any] = None,
root_value: Optional[Any] = None,
operation_name: Optional[str] = None,
) -> Union[AsyncIterator[GraphQLExecutionResult], GraphQLExecutionResult]:
```
To:
```py
async def subscribe(
self,
query: Optional[str],
variable_values: Optional[Dict[str, Any]] = None,
context_value: Optional[Any] = None,
root_value: Optional[Any] = None,
operation_name: Optional[str] = None,
) -> Union[AsyncGenerator[ExecutionResult, None], PreExecutionError]:
```
Due to moving away from graphql-core result types to our internal types.
nrbnlulu marked this conversation as resolved.
Show resolved Hide resolved
3 changes: 2 additions & 1 deletion docs/guides/custom-extensions.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,8 @@ class MyExtension(SchemaExtension):

- Execution

`on_execute` can be used to run code on the execution step of the GraphQL
`on_execute` can be used to run code on the execution step of the GraphQL. When
using subscriptions, `on_execute` will be called for each subscription yield.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if this should be a new hook or not, execute doesn't seem to be the right place

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a heavy question. I tried to look some references in other implementations though it seems like we are pioneers 🙃
https://github.com/graphql-go/graphql/blob/f2b39caf7c92cab3f3f92726b55f59c74f54cfbc/subscription.go#L26

IMO a new hook might be better assuming stream and defer can benefit from it. otherwise I think execute
should suffice.
Also I can't think of something you can do with the current on_execute that you can't do with on_operation and no ready made extension uses it as well.
image

execution.

```python
Expand Down
9 changes: 2 additions & 7 deletions noxfile.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
PYTHON_VERSIONS = ["3.12", "3.11", "3.10", "3.9", "3.8"]
GQL_CORE_VERSIONS = [
"3.2.3",
"3.3.0",
"3.3.0a6",
]

COMMON_PYTEST_OPTIONS = [
Expand Down Expand Up @@ -44,12 +44,7 @@


def _install_gql_core(session: Session, version: str) -> None:
# hack for better workflow names # noqa: FIX004
if version == "3.2.3":
session._session.install(f"graphql-core=={version}") # type: ignore
session._session.install(
"https://github.com/graphql-python/graphql-core/archive/876aef67b6f1e1f21b3b5db94c7ff03726cb6bdf.zip"
) # type: ignore
session._session.install(f"graphql-core=={version}")


gql_core_parametrize = nox.parametrize(
Expand Down
4 changes: 2 additions & 2 deletions strawberry/channels/testing.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,9 +144,9 @@ async def subscribe(
message_type = response["type"]
if message_type == NextMessage.type:
payload = NextMessage(**response).payload
ret = ExecutionResult(payload["data"], None)
ret = ExecutionResult(payload.get("data"), None)
if "errors" in payload:
ret.errors = self.process_errors(payload["errors"])
ret.errors = self.process_errors(payload.get("errors") or [])
nrbnlulu marked this conversation as resolved.
Show resolved Hide resolved
ret.extensions = payload.get("extensions", None)
yield ret
elif message_type == ErrorMessage.type:
Expand Down
13 changes: 10 additions & 3 deletions strawberry/extensions/base_extension.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,11 @@ class LifecycleStep(Enum):
class SchemaExtension:
execution_context: ExecutionContext

def __init__(self, *, execution_context: ExecutionContext) -> None:
self.execution_context = execution_context

# to support extensions that still use the old signature
# we have an optional argument here for ease of initialization.
nrbnlulu marked this conversation as resolved.
Show resolved Hide resolved
def __init__(
self, *, execution_context: ExecutionContext | None = None
) -> None: ...
def on_operation( # type: ignore
self,
) -> AsyncIteratorOrIterator[None]: # pragma: no cover
Expand Down Expand Up @@ -61,6 +63,11 @@ def resolve(
def get_results(self) -> AwaitableOrValue[Dict[str, Any]]:
return {}

@classmethod
def _implements_resolve(cls) -> bool:
"""Whether the extension implements the resolve method."""
return cls.resolve is not SchemaExtension.resolve


Hook = Callable[[SchemaExtension], AsyncIteratorOrIterator[None]]

Expand Down
45 changes: 8 additions & 37 deletions strawberry/extensions/runner.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
from __future__ import annotations

import inspect
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Type, Union

from graphql import MiddlewareManager
from typing import TYPE_CHECKING, Any, Dict, List, Optional

from strawberry.extensions.context import (
ExecutingContextManager,
Expand All @@ -13,39 +11,22 @@
)
from strawberry.utils.await_maybe import await_maybe

from . import SchemaExtension

if TYPE_CHECKING:
from strawberry.types import ExecutionContext

from . import SchemaExtension


class SchemaExtensionsRunner:
extensions: List[SchemaExtension]

def __init__(
self,
execution_context: ExecutionContext,
extensions: Optional[
List[Union[Type[SchemaExtension], SchemaExtension]]
] = None,
extensions: Optional[List[SchemaExtension]] = None,
) -> None:
self.execution_context = execution_context

if not extensions:
extensions = []

init_extensions: List[SchemaExtension] = []

for extension in extensions:
# If the extension has already been instantiated then set the
# `execution_context` attribute
if isinstance(extension, SchemaExtension):
extension.execution_context = execution_context
init_extensions.append(extension)
else:
init_extensions.append(extension(execution_context=execution_context))
nrbnlulu marked this conversation as resolved.
Show resolved Hide resolved

self.extensions = init_extensions
self.extensions = extensions or []
patrick91 marked this conversation as resolved.
Show resolved Hide resolved

def operation(self) -> OperationContextManager:
return OperationContextManager(self.extensions)
Expand All @@ -61,29 +42,19 @@ def executing(self) -> ExecutingContextManager:

def get_extensions_results_sync(self) -> Dict[str, Any]:
data: Dict[str, Any] = {}

for extension in self.extensions:
if inspect.iscoroutinefunction(extension.get_results):
msg = "Cannot use async extension hook during sync execution"
raise RuntimeError(msg)

data.update(extension.get_results()) # type: ignore

return data

async def get_extensions_results(self) -> Dict[str, Any]:
async def get_extensions_results(self, ctx: ExecutionContext) -> Dict[str, Any]:
data: Dict[str, Any] = {}

for extension in self.extensions:
results = await await_maybe(extension.get_results())
data.update(results)
data.update(await await_maybe(extension.get_results()))

data.update(ctx.extensions_results)
return data

def as_middleware_manager(self, *additional_middlewares: Any) -> MiddlewareManager:
middlewares = tuple(self.extensions) + additional_middlewares

return MiddlewareManager(*middlewares)


__all__ = ["SchemaExtensionsRunner"]
3 changes: 2 additions & 1 deletion strawberry/schema/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from strawberry.types.union import StrawberryUnion

from .config import StrawberryConfig
from .subscribe import SubscriptionResult


class BaseSchema(Protocol):
Expand Down Expand Up @@ -62,7 +63,7 @@ async def subscribe(
context_value: Optional[Any] = None,
root_value: Optional[Any] = None,
operation_name: Optional[str] = None,
) -> Any:
) -> SubscriptionResult:
nrbnlulu marked this conversation as resolved.
Show resolved Hide resolved
raise NotImplementedError

@abstractmethod
Expand Down
Loading
Loading