Skip to content

Commit 56fb108

Browse files
authored
Merge branch 'master' into custom-execution-context
2 parents e49f851 + a16edf1 commit 56fb108

20 files changed

+481
-125
lines changed

.flake8

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
[flake8]
22
exclude = .git,.mypy_cache,.pytest_cache,.tox,.venv,__pycache__,build,dist,docs
3+
max-line-length = 88

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@ a query language for APIs created by Facebook.
1111
[![Dependency Updates](https://pyup.io/repos/github/graphql-python/graphql-core-next/shield.svg)](https://pyup.io/repos/github/graphql-python/graphql-core-next/)
1212
[![Python 3 Status](https://pyup.io/repos/github/graphql-python/graphql-core-next/python-3-shield.svg)](https://pyup.io/repos/github/graphql-python/graphql-core-next/)
1313

14-
The current version 1.0.0 of GraphQL-core-next is up-to-date with GraphQL.js
15-
version 14.0.0. All parts of the API are covered by an extensive test suite of
16-
currently 1603 unit tests.
14+
The current version 1.0.1 of GraphQL-core-next is up-to-date with GraphQL.js
15+
version 14.0.2. All parts of the API are covered by an extensive test suite of
16+
currently 1614 unit tests.
1717

1818

1919
## Documentation

docs/conf.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@
6161
# The short X.Y version.
6262
version = u'1.0'
6363
# The full version, including alpha/beta/rc tags.
64-
release = u'1.0.0'
64+
release = u'1.0.1'
6565

6666
# The language for content autogenerated by Sphinx. Refer to documentation
6767
# for a list of supported languages.

graphql/__init__.py

Lines changed: 34 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@
3737
- `graphql/subscription`: Subscribe to data updates.
3838
"""
3939

40-
__version__ = '1.0.0'
41-
__version_js__ = '14.0.0'
40+
__version__ = '1.0.1'
41+
__version_js__ = '14.0.2'
4242

4343
# The primary entry point into fulfilling a GraphQL request.
4444

@@ -260,36 +260,37 @@
260260

261261
# Validate GraphQL queries.
262262
from .validation import (
263-
validate,
264-
ValidationContext,
265-
# All validation rules in the GraphQL Specification.
266-
specified_rules,
267-
# Individual validation rules.
268-
FieldsOnCorrectTypeRule,
269-
FragmentsOnCompositeTypesRule,
270-
KnownArgumentNamesRule,
271-
KnownDirectivesRule,
272-
KnownFragmentNamesRule,
273-
KnownTypeNamesRule,
274-
LoneAnonymousOperationRule,
275-
NoFragmentCyclesRule,
276-
NoUndefinedVariablesRule,
277-
NoUnusedFragmentsRule,
278-
NoUnusedVariablesRule,
279-
OverlappingFieldsCanBeMergedRule,
280-
PossibleFragmentSpreadsRule,
281-
ProvidedRequiredArgumentsRule,
282-
ScalarLeafsRule,
283-
SingleFieldSubscriptionsRule,
284-
UniqueArgumentNamesRule,
285-
UniqueDirectivesPerLocationRule,
286-
UniqueFragmentNamesRule,
287-
UniqueInputFieldNamesRule,
288-
UniqueOperationNamesRule,
289-
UniqueVariableNamesRule,
290-
ValuesOfCorrectTypeRule,
291-
VariablesAreInputTypesRule,
292-
VariablesInAllowedPositionRule)
263+
validate,
264+
ValidationContext,
265+
ValidationRule, ASTValidationRule, SDLValidationRule,
266+
# All validation rules in the GraphQL Specification.
267+
specified_rules,
268+
# Individual validation rules.
269+
FieldsOnCorrectTypeRule,
270+
FragmentsOnCompositeTypesRule,
271+
KnownArgumentNamesRule,
272+
KnownDirectivesRule,
273+
KnownFragmentNamesRule,
274+
KnownTypeNamesRule,
275+
LoneAnonymousOperationRule,
276+
NoFragmentCyclesRule,
277+
NoUndefinedVariablesRule,
278+
NoUnusedFragmentsRule,
279+
NoUnusedVariablesRule,
280+
OverlappingFieldsCanBeMergedRule,
281+
PossibleFragmentSpreadsRule,
282+
ProvidedRequiredArgumentsRule,
283+
ScalarLeafsRule,
284+
SingleFieldSubscriptionsRule,
285+
UniqueArgumentNamesRule,
286+
UniqueDirectivesPerLocationRule,
287+
UniqueFragmentNamesRule,
288+
UniqueInputFieldNamesRule,
289+
UniqueOperationNamesRule,
290+
UniqueVariableNamesRule,
291+
ValuesOfCorrectTypeRule,
292+
VariablesAreInputTypesRule,
293+
VariablesInAllowedPositionRule)
293294

294295
# Create, format, and print GraphQL errors.
295296
from .error import (
@@ -430,6 +431,7 @@
430431
'get_directive_values', 'ExecutionContext', 'ExecutionResult',
431432
'subscribe', 'create_source_event_stream',
432433
'validate', 'ValidationContext',
434+
'ValidationRule', 'ASTValidationRule', 'SDLValidationRule',
433435
'specified_rules',
434436
'FieldsOnCorrectTypeRule', 'FragmentsOnCompositeTypesRule',
435437
'KnownArgumentNamesRule', 'KnownDirectivesRule', 'KnownFragmentNamesRule',

graphql/execution/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66

77
from .execute import (
88
execute, default_field_resolver, response_path_as_list,
9-
ExecutionContext, ExecutionResult)
9+
ExecutionContext, ExecutionResult, Middleware)
10+
from .middleware import MiddlewareManager
1011
from .values import get_directive_values
1112

1213
__all__ = [
1314
'execute', 'default_field_resolver', 'response_path_as_list',
1415
'ExecutionContext', 'ExecutionResult',
16+
'Middleware', 'MiddlewareManager',
1517
'get_directive_values']

graphql/execution/execute.py

Lines changed: 71 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
DocumentNode, FieldNode, FragmentDefinitionNode,
99
FragmentSpreadNode, InlineFragmentNode, OperationDefinitionNode,
1010
OperationType, SelectionSetNode)
11+
from .middleware import MiddlewareManager
1112
from ..pyutils import is_invalid, is_nullish, MaybeAwaitable
1213
from ..utilities import get_operation_root_type, type_from_ast
1314
from ..type import (
@@ -24,7 +25,7 @@
2425
__all__ = [
2526
'add_path', 'assert_valid_execution_arguments', 'default_field_resolver',
2627
'execute', 'get_field_def', 'response_path_as_list',
27-
'ExecutionResult', 'ExecutionContext']
28+
'ExecutionResult', 'ExecutionContext', 'Middleware']
2829

2930

3031
# Terminology
@@ -59,6 +60,53 @@ class ExecutionResult(NamedTuple):
5960

6061
ExecutionResult.__new__.__defaults__ = (None, None) # type: ignore
6162

63+
Middleware = Optional[Union[Tuple, List, MiddlewareManager]]
64+
65+
66+
def execute(
67+
schema: GraphQLSchema, document: DocumentNode,
68+
root_value: Any=None, context_value: Any=None,
69+
variable_values: Dict[str, Any]=None,
70+
operation_name: str=None,
71+
field_resolver: GraphQLFieldResolver=None,
72+
execution_context_class: Type[ExecutionContext]=ExecutionContext,
73+
middleware: Middleware=None
74+
) -> MaybeAwaitable[ExecutionResult]:
75+
"""Execute a GraphQL operation.
76+
77+
Implements the "Evaluating requests" section of the GraphQL specification.
78+
79+
Returns an ExecutionResult (if all encountered resolvers are synchronous),
80+
or a coroutine object eventually yielding an ExecutionResult.
81+
82+
If the arguments to this function do not result in a legal execution
83+
context, a GraphQLError will be thrown immediately explaining the invalid
84+
input.
85+
"""
86+
# If arguments are missing or incorrect, throw an error.
87+
assert_valid_execution_arguments(schema, document, variable_values)
88+
89+
# If a valid execution context cannot be created due to incorrect
90+
# arguments, a "Response" with only errors is returned.
91+
exe_context = execution_context_class.build(
92+
schema, document, root_value, context_value,
93+
variable_values, operation_name, field_resolver, middleware)
94+
95+
# Return early errors if execution context failed.
96+
if isinstance(exe_context, list):
97+
return ExecutionResult(data=None, errors=exe_context)
98+
99+
# Return a possible coroutine object that will eventually yield the data
100+
# described by the "Response" section of the GraphQL specification.
101+
#
102+
# If errors are encountered while executing a GraphQL field, only that
103+
# field and its descendants will be omitted, and sibling fields will still
104+
# be executed. An execution which encounters errors will still result in a
105+
# coroutine object that can be executed without errors.
106+
107+
data = exe_context.execute_operation(exe_context.operation, root_value)
108+
return exe_context.build_response(data)
109+
62110

63111
class ExecutionContext:
64112
"""Data that must be available at all points during query execution.
@@ -74,6 +122,7 @@ class ExecutionContext:
74122
operation: OperationDefinitionNode
75123
variable_values: Dict[str, Any]
76124
field_resolver: GraphQLFieldResolver
125+
middleware_manager: Optional[MiddlewareManager]
77126
errors: List[GraphQLError]
78127

79128
def __init__(
@@ -83,6 +132,7 @@ def __init__(
83132
operation: OperationDefinitionNode,
84133
variable_values: Dict[str, Any],
85134
field_resolver: GraphQLFieldResolver,
135+
middleware_manager: Optional[MiddlewareManager],
86136
errors: List[GraphQLError]) -> None:
87137
self.schema = schema
88138
self.fragments = fragments
@@ -91,6 +141,7 @@ def __init__(
91141
self.operation = operation
92142
self.variable_values = variable_values
93143
self.field_resolver = field_resolver # type: ignore
144+
self.middleware_manager = middleware_manager
94145
self.errors = errors
95146
self._subfields_cache: Dict[
96147
Tuple[GraphQLObjectType, Tuple[FieldNode, ...]],
@@ -102,7 +153,8 @@ def build(
102153
root_value: Any=None, context_value: Any=None,
103154
raw_variable_values: Dict[str, Any]=None,
104155
operation_name: str=None,
105-
field_resolver: GraphQLFieldResolver=None
156+
field_resolver: GraphQLFieldResolver=None,
157+
middleware: Middleware=None
106158
) -> Union[List[GraphQLError], 'ExecutionContext']:
107159
"""Build an execution context
108160
@@ -115,6 +167,18 @@ def build(
115167
operation: Optional[OperationDefinitionNode] = None
116168
has_multiple_assumed_operations = False
117169
fragments: Dict[str, FragmentDefinitionNode] = {}
170+
middleware_manager: Optional[MiddlewareManager] = None
171+
if middleware is not None:
172+
if isinstance(middleware, (list, tuple)):
173+
middleware_manager = MiddlewareManager(*middleware)
174+
elif isinstance(middleware, MiddlewareManager):
175+
middleware_manager = middleware
176+
else:
177+
raise TypeError(
178+
"Middleware must be passed as a list or tuple of functions"
179+
" or objects, or as a single MiddlewareManager object."
180+
f" Got {middleware!r} instead.")
181+
118182
for definition in document.definitions:
119183
if isinstance(definition, OperationDefinitionNode):
120184
if not operation_name and operation:
@@ -159,7 +223,8 @@ def build(
159223

160224
return cls(
161225
schema, fragments, root_value, context_value, operation,
162-
variable_values, field_resolver or default_field_resolver, errors)
226+
variable_values, field_resolver or default_field_resolver,
227+
middleware_manager, errors)
163228

164229
def build_response(
165230
self, data: MaybeAwaitable[Optional[Dict[str, Any]]]
@@ -405,6 +470,9 @@ def resolve_field(
405470

406471
resolve_fn = field_def.resolve or self.field_resolver
407472

473+
if self.middleware_manager:
474+
resolve_fn = self.middleware_manager.get_field_resolver(resolve_fn)
475+
408476
info = self.build_resolve_info(
409477
field_def, field_nodes, parent_type, path)
410478

@@ -752,49 +820,6 @@ def collect_subfields(
752820
return sub_field_nodes
753821

754822

755-
def execute(
756-
schema: GraphQLSchema, document: DocumentNode,
757-
root_value: Any=None, context_value: Any=None,
758-
variable_values: Dict[str, Any]=None,
759-
operation_name: str=None, field_resolver: GraphQLFieldResolver=None,
760-
execution_context_class: Type[ExecutionContext]=ExecutionContext,
761-
) -> MaybeAwaitable[ExecutionResult]:
762-
"""Execute a GraphQL operation.
763-
764-
Implements the "Evaluating requests" section of the GraphQL specification.
765-
766-
Returns an ExecutionResult (if all encountered resolvers are synchronous),
767-
or a coroutine object eventually yielding an ExecutionResult.
768-
769-
If the arguments to this function do not result in a legal execution
770-
context, a GraphQLError will be thrown immediately explaining the invalid
771-
input.
772-
"""
773-
# If arguments are missing or incorrect, throw an error.
774-
assert_valid_execution_arguments(schema, document, variable_values)
775-
776-
# If a valid execution context cannot be created due to incorrect
777-
# arguments, a "Response" with only errors is returned.
778-
exe_context = execution_context_class.build(
779-
schema, document, root_value, context_value,
780-
variable_values, operation_name, field_resolver)
781-
782-
# Return early errors if execution context failed.
783-
if isinstance(exe_context, list):
784-
return ExecutionResult(data=None, errors=exe_context)
785-
786-
# Return a possible coroutine object that will eventually yield the data
787-
# described by the "Response" section of the GraphQL specification.
788-
#
789-
# If errors are encountered while executing a GraphQL field, only that
790-
# field and its descendants will be omitted, and sibling fields will still
791-
# be executed. An execution which encounters errors will still result in a
792-
# coroutine object that can be executed without errors.
793-
794-
data = exe_context.execute_operation(exe_context.operation, root_value)
795-
return exe_context.build_response(data)
796-
797-
798823
def assert_valid_execution_arguments(
799824
schema: GraphQLSchema, document: DocumentNode,
800825
raw_variable_values: Dict[str, Any]=None) -> None:

graphql/execution/middleware.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
from functools import partial
2+
from inspect import isfunction
3+
from itertools import chain
4+
5+
from typing import (
6+
Callable, Iterator, Dict, Tuple, Any, Iterable, Optional, cast)
7+
8+
__all__ = ['MiddlewareManager']
9+
10+
GraphQLFieldResolver = Callable[..., Any]
11+
12+
13+
class MiddlewareManager:
14+
"""Manager for the middleware chain.
15+
16+
This class helps to wrap resolver functions with the provided middleware
17+
functions and/or objects. The functions take the next middleware function
18+
as first argument. If middleware is provided as an object, it must provide
19+
a method 'resolve' that is used as the middleware function.
20+
"""
21+
22+
__slots__ = 'middlewares', '_middleware_resolvers', '_cached_resolvers'
23+
24+
_cached_resolvers: Dict[GraphQLFieldResolver, GraphQLFieldResolver]
25+
_middleware_resolvers: Optional[Iterator[Callable]]
26+
27+
def __init__(self, *middlewares: Any) -> None:
28+
self.middlewares = middlewares
29+
self._middleware_resolvers = get_middleware_resolvers(
30+
middlewares) if middlewares else None
31+
self._cached_resolvers = {}
32+
33+
def get_field_resolver(
34+
self, field_resolver: GraphQLFieldResolver
35+
) -> GraphQLFieldResolver:
36+
"""Wrap the provided resolver with the middleware.
37+
38+
Returns a function that chains the middleware functions with the
39+
provided resolver function
40+
"""
41+
if self._middleware_resolvers is None:
42+
return field_resolver
43+
if field_resolver not in self._cached_resolvers:
44+
self._cached_resolvers[field_resolver] = middleware_chain(
45+
field_resolver, self._middleware_resolvers)
46+
return self._cached_resolvers[field_resolver]
47+
48+
49+
def get_middleware_resolvers(
50+
middlewares: Tuple[Any, ...]) -> Iterator[Callable]:
51+
"""Get a list of resolver functions from a list of classes or functions."""
52+
for middleware in middlewares:
53+
if isfunction(middleware):
54+
yield middleware
55+
else: # middleware provided as object with 'resolve' method
56+
resolver_func = getattr(middleware, 'resolve', None)
57+
if resolver_func is not None:
58+
yield resolver_func
59+
60+
61+
def middleware_chain(
62+
func: GraphQLFieldResolver, middlewares: Iterable[Callable]
63+
) -> GraphQLFieldResolver:
64+
"""Chain the given function with the provided middlewares.
65+
66+
Returns a new resolver function that is the chain of both.
67+
"""
68+
if not middlewares:
69+
return func
70+
middlewares = chain((func,), middlewares)
71+
last_func: Optional[GraphQLFieldResolver] = None
72+
for middleware in middlewares:
73+
last_func = partial(middleware, last_func) if last_func else middleware
74+
return cast(GraphQLFieldResolver, last_func)

0 commit comments

Comments
 (0)