Skip to content

Commit c1d399e

Browse files
committed
subscribe: replace per_event_executor with map_source_to_response_event
Replicates graphql/graphql-js@0cd2890
1 parent 5b127f1 commit c1d399e

5 files changed

Lines changed: 88 additions & 62 deletions

File tree

docs/modules/execution.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ Execution
6565

6666
.. autofunction:: create_source_event_stream
6767

68+
.. autofunction:: map_source_to_response_event
69+
70+
.. autoclass:: RootSelectionSetExecutor
71+
6872
.. autoclass:: Middleware
6973

7074
.. autoclass:: MiddlewareManager

src/graphql/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -513,7 +513,9 @@
513513
# Subscription
514514
subscribe,
515515
create_source_event_stream,
516+
map_source_to_response_event,
516517
map_async_iterable,
518+
RootSelectionSetExecutor,
517519
# Middleware
518520
Middleware,
519521
MiddlewareManager,
@@ -742,6 +744,7 @@
742744
"ResolvedNamedType",
743745
"ResolvedSchemaElement",
744746
"ResponsePath",
747+
"RootSelectionSetExecutor",
745748
"SDLValidationRule",
746749
"SafeChange",
747750
"SafeChangeType",
@@ -906,6 +909,7 @@
906909
"lexicographic_sort_schema",
907910
"located_error",
908911
"map_async_iterable",
912+
"map_source_to_response_event",
909913
"parse",
910914
"parse_const_value",
911915
"parse_schema_coordinate",

src/graphql/execution/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,12 +40,14 @@
4040
execute_sync,
4141
default_field_resolver,
4242
default_type_resolver,
43+
map_source_to_response_event,
4344
subscribe,
4445
AsyncWorkFinishedInfo,
4546
ExecutionHooks,
4647
Executor,
4748
GraphQLWrappedResult,
4849
Middleware,
50+
RootSelectionSetExecutor,
4951
)
5052

5153
__all__ = [
@@ -72,6 +74,7 @@
7274
"Middleware",
7375
"MiddlewareManager",
7476
"PendingResult",
77+
"RootSelectionSetExecutor",
7578
"StreamItemRecord",
7679
"StreamItemResult",
7780
"StreamItemsResult",
@@ -89,5 +92,6 @@
8992
"get_directive_values",
9093
"get_variable_values",
9194
"map_async_iterable",
95+
"map_source_to_response_event",
9296
"subscribe",
9397
]

src/graphql/execution/execute.py

Lines changed: 64 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@
140140
"Executor",
141141
"GraphQLWrappedResult",
142142
"Middleware",
143+
"RootSelectionSetExecutor",
143144
"create_source_event_stream",
144145
"default_field_resolver",
145146
"default_type_resolver",
@@ -148,6 +149,7 @@
148149
"execute_subscription_event",
149150
"execute_sync",
150151
"experimental_execute_incrementally",
152+
"map_source_to_response_event",
151153
"subscribe",
152154
]
153155

@@ -237,7 +239,6 @@ class Executor(IncrementalPublisherContext):
237239
field_resolver: GraphQLFieldResolver
238240
type_resolver: GraphQLTypeResolver
239241
subscribe_field_resolver: GraphQLFieldResolver
240-
per_event_executor: Callable[[Executor], AwaitableOrValue[ExecutionResult]]
241242
enable_early_execution: bool
242243
hide_suggestions: bool
243244
abort_signal: AbortSignal | None
@@ -274,8 +275,6 @@ def __init__( # noqa: PLR0913
274275
middleware_manager: MiddlewareManager | None = None,
275276
is_awaitable: Callable[[Any], TypeGuard[Awaitable]] | None = None,
276277
is_async_iterable: Callable[[Any], TypeGuard[AsyncIterable]] | None = None,
277-
per_event_executor: Callable[[Executor], AwaitableOrValue[ExecutionResult]]
278-
| None = None,
279278
hide_suggestions: bool = False,
280279
abort_signal: AbortSignal | None = None,
281280
hooks: ExecutionHooks | None = None,
@@ -290,7 +289,6 @@ def __init__( # noqa: PLR0913
290289
self.field_resolver = field_resolver
291290
self.type_resolver = type_resolver
292291
self.subscribe_field_resolver = subscribe_field_resolver
293-
self.per_event_executor = per_event_executor or execute_subscription_event
294292
self.enable_early_execution = enable_early_execution
295293
self.hide_suggestions = hide_suggestions
296294
self.abort_signal = abort_signal
@@ -331,8 +329,6 @@ def build( # noqa: PLR0913
331329
middleware: Middleware | None = None,
332330
is_awaitable: Callable[[Any], TypeGuard[Awaitable]] | None = None,
333331
is_async_iterable: Callable[[Any], TypeGuard[AsyncIterable]] | None = None,
334-
per_event_executor: Callable[[Executor], AwaitableOrValue[ExecutionResult]]
335-
| None = None,
336332
hide_suggestions: bool = False,
337333
abort_signal: AbortSignal | None = None,
338334
hooks: ExecutionHooks | None = None,
@@ -425,7 +421,6 @@ def build( # noqa: PLR0913
425421
middleware_manager,
426422
is_awaitable,
427423
is_async_iterable,
428-
per_event_executor=per_event_executor,
429424
hide_suggestions=hide_suggestions,
430425
abort_signal=abort_signal,
431426
hooks=hooks,
@@ -1978,39 +1973,6 @@ def build_sub_execution_plan(
19781973
execution_plans[original_grouped_field_set] = exeecution_plan
19791974
return exeecution_plan
19801975

1981-
def map_source_to_response(
1982-
self, result_or_stream: ExecutionResult | AsyncIterable[Any]
1983-
) -> AsyncGenerator[ExecutionResult, None] | ExecutionResult:
1984-
"""Map source result to response.
1985-
1986-
For each payload yielded from a subscription,
1987-
map it over the normal GraphQL :func:`~graphql.execution.execute` function,
1988-
with ``payload`` as the ``root_value``.
1989-
This implements the "MapSourceToResponseEvent" algorithm
1990-
described in the GraphQL specification.
1991-
Each event is executed with the executor's ``per_event_executor``, which
1992-
defaults to :func:`~graphql.execution.execute_subscription_event` (providing
1993-
the "ExecuteSubscriptionEvent" algorithm) but can be overridden to set up and
1994-
tear down a custom executor around the execution of each event.
1995-
"""
1996-
if not self.is_async_iterable(result_or_stream):
1997-
return cast("ExecutionResult", result_or_stream) # pragma: no cover
1998-
1999-
build_executor = self.build_per_event_executor
2000-
per_event_executor = self.per_event_executor
2001-
2002-
async def callback(payload: Any) -> ExecutionResult:
2003-
result = per_event_executor(build_executor(payload))
2004-
# typecast to ExecutionResult, not possible to return
2005-
# ExperimentalIncrementalExecutionResults when operation is 'subscription'.
2006-
return (
2007-
await cast("Awaitable[ExecutionResult]", result)
2008-
if self.is_awaitable(result)
2009-
else cast("ExecutionResult", result)
2010-
)
2011-
2012-
return map_async_iterable(self.cancellable_iterable(result_or_stream), callback)
2013-
20141976
def collect_execution_groups(
20151977
self,
20161978
parent_type: GraphQLObjectType,
@@ -2924,8 +2886,6 @@ def subscribe(
29242886
enable_early_execution: bool = False,
29252887
executor_class: type[Executor] | None = None,
29262888
middleware: MiddlewareManager | None = None,
2927-
per_event_executor: Callable[[Executor], AwaitableOrValue[ExecutionResult]]
2928-
| None = None,
29292889
hide_suggestions: bool = False,
29302890
**custom_context_args: Any,
29312891
) -> AwaitableOrValue[AsyncIterator[ExecutionResult] | ExecutionResult]:
@@ -2952,10 +2912,12 @@ def subscribe(
29522912
If an operation that defers or streams data is executed with this function,
29532913
a field error will be raised at the location of the `@defer` or `@stream` directive.
29542914
2955-
A custom ``per_event_executor`` may be provided to execute each subscription event
2956-
with a custom executor. It receives the per-event executor and should
2957-
return an ExecutionResult, usually by calling
2958-
:func:`~graphql.execution.execute_subscription_event` (the default executor).
2915+
To customize how each subscription event is executed, compose the subscription
2916+
pipeline directly instead of calling this function: build an executor with
2917+
:meth:`Executor.build`, resolve the source event stream with
2918+
:func:`~graphql.execution.create_source_event_stream`, and map it to the response
2919+
stream with :func:`~graphql.execution.map_source_to_response_event`, passing a
2920+
custom ``root_selection_set_executor``.
29592921
"""
29602922
if executor_class is None:
29612923
executor_class = Executor
@@ -2975,7 +2937,6 @@ def subscribe(
29752937
max_coercion_errors,
29762938
enable_early_execution,
29772939
middleware=middleware,
2978-
per_event_executor=per_event_executor,
29792940
hide_suggestions=hide_suggestions,
29802941
**custom_context_args,
29812942
)
@@ -2994,16 +2955,19 @@ def subscribe(
29942955

29952956
async def await_result() -> Any:
29962957
awaited_result_or_stream = await result_or_stream
2997-
if isinstance(awaited_result_or_stream, ExecutionResult):
2998-
return awaited_result_or_stream
2999-
return executor.map_source_to_response(awaited_result_or_stream)
2958+
return (
2959+
map_source_to_response_event(executor, awaited_result_or_stream)
2960+
if executor.is_async_iterable(awaited_result_or_stream)
2961+
else awaited_result_or_stream
2962+
)
30002963

30012964
return await_result()
30022965

3003-
if isinstance(result_or_stream, ExecutionResult):
3004-
return result_or_stream
3005-
3006-
return executor.map_source_to_response(result_or_stream) # type: ignore
2966+
return (
2967+
map_source_to_response_event(executor, result_or_stream) # type: ignore
2968+
if executor.is_async_iterable(result_or_stream)
2969+
else result_or_stream
2970+
)
30072971

30082972

30092973
def execute_root_selection_set(
@@ -3023,17 +2987,59 @@ def execute_subscription_event(
30232987
) -> AwaitableOrValue[ExecutionResult]:
30242988
"""Execute a single subscription event.
30252989
3026-
This is the default ``per_event_executor`` used by :func:`subscribe`. It provides
3027-
the "ExecuteSubscriptionEvent" algorithm described in the GraphQL specification,
3028-
which is nearly identical to the "ExecuteQuery" algorithm. A custom executor may
3029-
wrap this function to set up and tear down a per-event executor.
2990+
This is the default ``root_selection_set_executor`` used by
2991+
:func:`map_source_to_response_event`. It provides the "ExecuteSubscriptionEvent"
2992+
algorithm described in the GraphQL specification, which is nearly identical to the
2993+
"ExecuteQuery" algorithm. A custom executor may wrap this function to set up and
2994+
tear down a per-event executor.
30302995
30312996
The passed executor should be a per-event executor as created by
30322997
:meth:`Executor.build_per_event_executor`.
30332998
"""
30342999
return cast("AwaitableOrValue[ExecutionResult]", executor.execute_operation(False))
30353000

30363001

3002+
RootSelectionSetExecutor: TypeAlias = Callable[
3003+
["Executor"], AwaitableOrValue[ExecutionResult]
3004+
]
3005+
3006+
3007+
def map_source_to_response_event(
3008+
executor: Executor,
3009+
source_event_stream: AsyncIterable[Any],
3010+
root_selection_set_executor: RootSelectionSetExecutor = execute_subscription_event,
3011+
) -> AsyncGenerator[ExecutionResult, None]:
3012+
"""Map a subscription source event stream to a response event stream.
3013+
3014+
Implements the "MapSourceToResponseEvent" algorithm described in the GraphQL
3015+
specification, mapping each event from a subscription source event stream to an
3016+
ExecutionResult in the response stream.
3017+
3018+
For each payload yielded from the source event stream, it is mapped over the normal
3019+
GraphQL :func:`~graphql.execution.execute` function, with ``payload`` as the
3020+
``root_value``. Each event is executed with the given
3021+
``root_selection_set_executor``, which defaults to
3022+
:func:`~graphql.execution.execute_subscription_event` (providing the
3023+
"ExecuteSubscriptionEvent" algorithm) but can be overridden to set up and tear
3024+
down a custom executor around the execution of each event.
3025+
"""
3026+
build_executor = executor.build_per_event_executor
3027+
3028+
async def callback(payload: Any) -> ExecutionResult:
3029+
result = root_selection_set_executor(build_executor(payload))
3030+
# typecast to ExecutionResult, not possible to return
3031+
# ExperimentalIncrementalExecutionResults when operation is 'subscription'.
3032+
return (
3033+
await cast("Awaitable[ExecutionResult]", result)
3034+
if executor.is_awaitable(result)
3035+
else cast("ExecutionResult", result)
3036+
)
3037+
3038+
return map_async_iterable(
3039+
executor.cancellable_iterable(source_event_stream), callback
3040+
)
3041+
3042+
30373043
def create_source_event_stream(
30383044
executor: Executor,
30393045
) -> AwaitableOrValue[AsyncIterable[Any] | ExecutionResult]:

tests/execution/test_subscribe.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
Executor,
1515
create_source_event_stream,
1616
execute_subscription_event,
17+
map_source_to_response_event,
1718
subscribe,
1819
)
1920
from graphql.language import DocumentNode, parse
@@ -315,7 +316,7 @@ async def subscribe_fn(obj, info):
315316

316317
await subscription.aclose() # type: ignore
317318

318-
async def uses_a_custom_default_per_event_executor():
319+
async def maps_a_source_stream_with_a_custom_root_selection_set_executor():
319320
schema = GraphQLSchema(
320321
query=DummyQueryType,
321322
subscription=GraphQLObjectType(
@@ -328,16 +329,23 @@ async def foo_generator(_info):
328329

329330
count = 0
330331

331-
def per_event_executor(context):
332+
def root_selection_set_executor(context):
332333
nonlocal count
333334
count += 1
334335
return execute_subscription_event(context)
335336

336-
subscription = subscribe(
337+
executor = Executor.build(
337338
schema,
338339
parse("subscription { foo }"),
339340
{"foo": foo_generator},
340-
per_event_executor=per_event_executor,
341+
)
342+
assert isinstance(executor, Executor)
343+
344+
result_or_stream = create_source_event_stream(executor)
345+
assert isinstance(result_or_stream, AsyncIterable)
346+
347+
subscription = map_source_to_response_event(
348+
executor, result_or_stream, root_selection_set_executor
341349
)
342350
assert isinstance(subscription, AsyncIterator)
343351

0 commit comments

Comments
 (0)