Skip to content

Commit bd44e60

Browse files
committed
add graphql "harness" abstraction along with async parse/validate support
Replicates graphql/graphql-js@d926d63
1 parent bb02fcf commit bd44e60

4 files changed

Lines changed: 282 additions & 36 deletions

File tree

src/graphql/__init__.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,16 @@
516516
# The primary entry point into fulfilling a GraphQL request.
517517
from .graphql import graphql, graphql_sync
518518

519+
# The default parse/validate/execute/subscribe harness used by graphql.
520+
from .harness import (
521+
GraphQLExecuteFn,
522+
GraphQLHarness,
523+
GraphQLParseFn,
524+
GraphQLSubscribeFn,
525+
GraphQLValidateFn,
526+
default_harness,
527+
)
528+
519529
INVALID = Undefined # deprecated alias
520530

521531
# The GraphQL-core version info.
@@ -601,13 +611,15 @@
601611
"GraphQLEnumValuesDefinition",
602612
"GraphQLError",
603613
"GraphQLErrorExtensions",
614+
"GraphQLExecuteFn",
604615
"GraphQLField",
605616
"GraphQLFieldKwargs",
606617
"GraphQLFieldMap",
607618
"GraphQLFieldResolver",
608619
"GraphQLFloat",
609620
"GraphQLFormattedError",
610621
"GraphQLFormattedErrorExtensions",
622+
"GraphQLHarness",
611623
"GraphQLID",
612624
"GraphQLIncludeDirective",
613625
"GraphQLInputField",
@@ -635,6 +647,7 @@
635647
"GraphQLObjectTypeKwargs",
636648
"GraphQLOneOfDirective",
637649
"GraphQLOutputType",
650+
"GraphQLParseFn",
638651
"GraphQLResolveInfo",
639652
"GraphQLScalarInputLiteralCoercer",
640653
"GraphQLScalarInputValueCoercer",
@@ -651,11 +664,13 @@
651664
"GraphQLSpecifiedByDirective",
652665
"GraphQLStreamDirective",
653666
"GraphQLString",
667+
"GraphQLSubscribeFn",
654668
"GraphQLSyntaxError",
655669
"GraphQLType",
656670
"GraphQLTypeResolver",
657671
"GraphQLUnionType",
658672
"GraphQLUnionTypeKwargs",
673+
"GraphQLValidateFn",
659674
"GraphQLWrappingType",
660675
"IncrementalDeferResult",
661676
"IncrementalResult",
@@ -812,6 +827,7 @@
812827
"concat_ast",
813828
"create_source_event_stream",
814829
"default_field_resolver",
830+
"default_harness",
815831
"default_type_resolver",
816832
"do_types_overlap",
817833
"execute",

src/graphql/graphql.py

Lines changed: 77 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
from typing import TYPE_CHECKING, Any, cast
77

88
from .error import GraphQLError
9-
from .execution import ExecutionResult, Executor, Middleware, execute
10-
from .language import Source, parse
9+
from .execution import ExecutionResult, Executor, Middleware
10+
from .harness import GraphQLHarness, default_harness
1111
from .pyutils.is_awaitable import is_awaitable as default_is_awaitable
1212
from .type import (
1313
GraphQLFieldResolver,
@@ -20,6 +20,7 @@
2020
from collections.abc import AsyncIterable, Awaitable, Callable, Collection
2121
from typing import TypeGuard
2222

23+
from .language import DocumentNode, Source
2324
from .pyutils import AbortSignal, AwaitableOrValue
2425
from .validation import ASTValidationRule
2526

@@ -46,6 +47,7 @@ async def graphql( # noqa: PLR0913
4647
experimental_fragment_arguments: bool = False,
4748
rules: Collection[type[ASTValidationRule]] | None = None,
4849
max_errors: int | None = None,
50+
harness: GraphQLHarness = default_harness,
4951
) -> ExecutionResult:
5052
"""Execute a GraphQL operation asynchronously.
5153
@@ -109,6 +111,9 @@ async def graphql( # noqa: PLR0913
109111
If not provided, the specified rules are used.
110112
:arg max_errors:
111113
The maximum number of validation errors to report before stopping.
114+
:arg harness:
115+
A custom set of parse/validate/execute/subscribe functions to use when
116+
fulfilling the operation. Defaults to ``default_harness``.
112117
"""
113118
# Always return asynchronously for a consistent API.
114119
result = graphql_impl(
@@ -131,6 +136,7 @@ async def graphql( # noqa: PLR0913
131136
experimental_fragment_arguments,
132137
rules,
133138
max_errors,
139+
harness,
134140
)
135141

136142
if default_is_awaitable(result):
@@ -168,6 +174,7 @@ def graphql_sync( # noqa: PLR0913
168174
experimental_fragment_arguments: bool = False,
169175
rules: Collection[type[ASTValidationRule]] | None = None,
170176
max_errors: int | None = None,
177+
harness: GraphQLHarness = default_harness,
171178
) -> ExecutionResult:
172179
"""Execute a GraphQL operation synchronously.
173180
@@ -204,6 +211,7 @@ def graphql_sync( # noqa: PLR0913
204211
experimental_fragment_arguments,
205212
rules,
206213
max_errors,
214+
harness,
207215
)
208216

209217
# Assert that the execution was synchronous.
@@ -235,15 +243,68 @@ def graphql_impl( # noqa: PLR0913
235243
experimental_fragment_arguments: bool = False,
236244
rules: Collection[type[ASTValidationRule]] | None = None,
237245
max_errors: int | None = None,
246+
harness: GraphQLHarness = default_harness,
238247
) -> AwaitableOrValue[ExecutionResult]:
239248
"""Execute a query, return asynchronously only if necessary."""
240249
# Validate Schema
241250
if schema_validation_errors := validate_schema(schema):
242251
return ExecutionResult(data=None, errors=schema_validation_errors)
243252

253+
def check_validation_and_execute(
254+
validation_errors: list[GraphQLError], document: DocumentNode
255+
) -> AwaitableOrValue[ExecutionResult]:
256+
if validation_errors:
257+
return ExecutionResult(data=None, errors=validation_errors)
258+
259+
# Execute
260+
return harness.execute(
261+
schema,
262+
document,
263+
root_value,
264+
context_value,
265+
variable_values,
266+
operation_name,
267+
field_resolver,
268+
type_resolver,
269+
None,
270+
50,
271+
False,
272+
middleware,
273+
executor_class,
274+
is_awaitable,
275+
is_async_iterable,
276+
hide_suggestions=hide_suggestions,
277+
abort_signal=abort_signal,
278+
)
279+
280+
def validate_and_execute(
281+
document: DocumentNode,
282+
) -> AwaitableOrValue[ExecutionResult]:
283+
# Validate
284+
validation_result = harness.validate(
285+
schema, document, rules, max_errors, hide_suggestions=hide_suggestions
286+
)
287+
288+
if default_is_awaitable(validation_result):
289+
290+
async def await_validation() -> ExecutionResult:
291+
validation_errors = await cast(
292+
"Awaitable[list[GraphQLError]]", validation_result
293+
)
294+
result = check_validation_and_execute(validation_errors, document)
295+
if default_is_awaitable(result):
296+
return await cast("Awaitable[ExecutionResult]", result)
297+
return cast("ExecutionResult", result)
298+
299+
return await_validation()
300+
301+
return check_validation_and_execute(
302+
cast("list[GraphQLError]", validation_result), document
303+
)
304+
244305
# Parse
245306
try:
246-
document = parse(
307+
document = harness.parse(
247308
source,
248309
no_location=no_location,
249310
max_tokens=max_tokens,
@@ -252,31 +313,18 @@ def graphql_impl( # noqa: PLR0913
252313
except GraphQLError as error:
253314
return ExecutionResult(data=None, errors=[error])
254315

255-
# Validate
256-
from .validation import validate
316+
if default_is_awaitable(document):
257317

258-
if validation_errors := validate(
259-
schema, document, rules, max_errors, hide_suggestions=hide_suggestions
260-
):
261-
return ExecutionResult(data=None, errors=validation_errors)
318+
async def await_document() -> ExecutionResult:
319+
try:
320+
resolved_document = await cast("Awaitable[DocumentNode]", document)
321+
except GraphQLError as error:
322+
return ExecutionResult(data=None, errors=[error])
323+
result = validate_and_execute(resolved_document)
324+
if default_is_awaitable(result):
325+
return await cast("Awaitable[ExecutionResult]", result)
326+
return cast("ExecutionResult", result)
262327

263-
# Execute
264-
return execute(
265-
schema,
266-
document,
267-
root_value,
268-
context_value,
269-
variable_values,
270-
operation_name,
271-
field_resolver,
272-
type_resolver,
273-
None,
274-
50,
275-
False,
276-
middleware,
277-
executor_class,
278-
is_awaitable,
279-
is_async_iterable,
280-
hide_suggestions=hide_suggestions,
281-
abort_signal=abort_signal,
282-
)
328+
return await_document()
329+
330+
return validate_and_execute(cast("DocumentNode", document))

src/graphql/harness.py

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
"""The parse/validate/execute/subscribe harness used by ``graphql``."""
2+
3+
from __future__ import annotations
4+
5+
from collections.abc import Callable
6+
from typing import TYPE_CHECKING, NamedTuple
7+
8+
from .execution import execute, subscribe
9+
from .language import parse
10+
from .validation import validate
11+
12+
if TYPE_CHECKING:
13+
from collections.abc import AsyncIterator
14+
15+
from .error import GraphQLError
16+
from .execution import ExecutionResult
17+
from .language import DocumentNode
18+
from .pyutils import AwaitableOrValue
19+
20+
__all__ = [
21+
"GraphQLExecuteFn",
22+
"GraphQLHarness",
23+
"GraphQLParseFn",
24+
"GraphQLSubscribeFn",
25+
"GraphQLValidateFn",
26+
"default_harness",
27+
]
28+
29+
# The functions which make up the harness purposefully expose maybe-async return
30+
# types, even though the internal parse and validate functions are always sync, to
31+
# encourage servers and other tooling to expect that user-supplied versions of these
32+
# functions may have async pre/post hooks.
33+
GraphQLParseFn = Callable[..., "AwaitableOrValue[DocumentNode]"]
34+
GraphQLValidateFn = Callable[..., "AwaitableOrValue[list[GraphQLError]]"]
35+
GraphQLExecuteFn = Callable[..., "AwaitableOrValue[ExecutionResult]"]
36+
GraphQLSubscribeFn = Callable[
37+
..., "AwaitableOrValue[AsyncIterator[ExecutionResult] | ExecutionResult]"
38+
]
39+
40+
41+
class GraphQLHarness(NamedTuple):
42+
"""The set of functions used by ``graphql`` to fulfill an operation.
43+
44+
A custom harness can be passed to ``graphql`` to supply user-defined versions of
45+
these functions, enabling a simple API for adding pre/post hooks. Replace
46+
individual functions with :meth:`~typing.NamedTuple._replace`, e.g.
47+
``default_harness._replace(execute=my_execute)``.
48+
"""
49+
50+
parse: GraphQLParseFn
51+
validate: GraphQLValidateFn
52+
execute: GraphQLExecuteFn
53+
subscribe: GraphQLSubscribeFn
54+
55+
56+
default_harness = GraphQLHarness(
57+
parse=parse,
58+
validate=validate,
59+
execute=execute,
60+
subscribe=subscribe,
61+
)
62+
"""The default parse/validate/execute/subscribe harness used by ``graphql``."""

0 commit comments

Comments
 (0)