From 33f1a1fe981fc3d7b1706d727af6217748046f87 Mon Sep 17 00:00:00 2001 From: nickharris Date: Mon, 27 Aug 2018 11:10:47 -0600 Subject: [PATCH 01/14] Added ExecutionResult return type support to resolver on execute --- graphql/execution/executor.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/graphql/execution/executor.py b/graphql/execution/executor.py index 1b5e884e..21067907 100644 --- a/graphql/execution/executor.py +++ b/graphql/execution/executor.py @@ -133,10 +133,16 @@ def on_resolve(data): if isinstance(data, Observable): return data - if not exe_context.errors: - return ExecutionResult(data=data) + errors = exe_context.errors or [] + extensions = None - return ExecutionResult(data=data, errors=exe_context.errors) + if isinstance(data, ExecutionResult): + extensions = getattr(data, 'extensions', None) + data_errors = getattr(data, 'errors', None) or [] + data = getattr(data, 'data', None) + errors = errors + data_errors + + return ExecutionResult(data=data, errors=errors or None, extensions=extensions) promise = ( Promise.resolve(None).then(promise_executor).catch(on_rejected).then(on_resolve) From 562739a1b6490d6e4e4ed617539c316eec087a2a Mon Sep 17 00:00:00 2001 From: nickharris Date: Mon, 27 Aug 2018 11:55:38 -0600 Subject: [PATCH 02/14] moved ExecutionResult return type handling to complete_value() --- graphql/execution/base.py | 3 +++ graphql/execution/executor.py | 28 ++++++++++++++++++---------- graphql/execution/utils.py | 7 +++++++ 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/graphql/execution/base.py b/graphql/execution/base.py index d71151a5..f0759199 100644 --- a/graphql/execution/base.py +++ b/graphql/execution/base.py @@ -76,6 +76,7 @@ class ResolveInfo(object): "variable_values", "context", "path", + "extensions", ) def __init__( @@ -91,6 +92,7 @@ def __init__( variable_values, # type: Dict context, # type: Optional[Any] path=None, # type: Union[List[Union[int, str]], List[str]] + extensions=None, # type: Dict ): # type: (...) -> None self.field_name = field_name @@ -104,6 +106,7 @@ def __init__( self.variable_values = variable_values self.context = context self.path = path + self.extensions = extensions __all__ = [ diff --git a/graphql/execution/executor.py b/graphql/execution/executor.py index 21067907..e8aa5252 100644 --- a/graphql/execution/executor.py +++ b/graphql/execution/executor.py @@ -133,16 +133,10 @@ def on_resolve(data): if isinstance(data, Observable): return data - errors = exe_context.errors or [] - extensions = None + if not exe_context.errors: + return ExecutionResult(data=data) - if isinstance(data, ExecutionResult): - extensions = getattr(data, 'extensions', None) - data_errors = getattr(data, 'errors', None) or [] - data = getattr(data, 'data', None) - errors = errors + data_errors - - return ExecutionResult(data=data, errors=errors or None, extensions=extensions) + return ExecutionResult(data=data, errors=exe_context.errors, extensions=exe_context.extensions) promise = ( Promise.resolve(None).then(promise_executor).catch(on_rejected).then(on_resolve) @@ -360,6 +354,7 @@ def resolve_field( variable_values=exe_context.variable_values, context=context, path=field_path, + extensions=exe_context.extensions, ) executor = exe_context.executor @@ -414,6 +409,7 @@ def subscribe_field( variable_values=exe_context.variable_values, context=context, path=path, + extensions=exe_context.extensions ) executor = exe_context.executor @@ -468,7 +464,7 @@ def complete_value_catching_error( info, # type: ResolveInfo path, # type: List[Union[int, str]] result, # type: Any -): +): # type: (...) -> Any # If the field type is non-nullable, then it is resolved without any # protection from errors. @@ -537,6 +533,18 @@ def complete_value( ), ) + if isinstance(result, ExecutionResult): + data = getattr(result, 'data', None) + extensions = getattr(result, 'extensions', None) + errors = getattr(result, 'errors', None) + + if extensions: + exe_context.update_extensions(extensions) + if errors: + exe_context.report_error(error) for error in errors + + return complete_value(exe_context, return_type, field_ast, info, path, data) + # print return_type, type(result) if isinstance(result, Exception): raise GraphQLLocatedError(field_asts, original_error=result, path=path) diff --git a/graphql/execution/utils.py b/graphql/execution/utils.py index b1e7ff25..26d811ab 100644 --- a/graphql/execution/utils.py +++ b/graphql/execution/utils.py @@ -54,6 +54,7 @@ class ExecutionContext(object): "middleware", "allow_subscriptions", "_subfields_cache", + "extensions", ) def __init__( @@ -67,6 +68,7 @@ def __init__( executor, # type: Any middleware, # type: Optional[Any] allow_subscriptions, # type: bool + extensions=None, # type: Dict ): # type: (...) -> None """Constructs a ExecutionContext object from the arguments passed @@ -126,6 +128,7 @@ def __init__( self.middleware = middleware self.allow_subscriptions = allow_subscriptions self._subfields_cache = {} # type: Dict[Tuple[GraphQLObjectType, Tuple[Field, ...]], DefaultOrderedDict] + self.extensions = extensions def get_field_resolver(self, field_resolver): # type: (Callable) -> Callable @@ -151,6 +154,10 @@ def report_error(self, error, traceback=None): logger.error("".join(exception)) self.errors.append(error) + def update_extensions(self, extensions): + self.extensions = extensions.copy() if self.extensions else {} + self.extensions.update(extensions) + def get_sub_fields(self, return_type, field_asts): # type: (GraphQLObjectType, List[Field]) -> DefaultOrderedDict k = return_type, tuple(field_asts) From 37c4fb0c78752f264708abac41529c6c7d8a5f0f Mon Sep 17 00:00:00 2001 From: nickharris Date: Mon, 27 Aug 2018 11:58:38 -0600 Subject: [PATCH 03/14] fixed bug in result errors reporting --- graphql/execution/executor.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/graphql/execution/executor.py b/graphql/execution/executor.py index e8aa5252..442aed74 100644 --- a/graphql/execution/executor.py +++ b/graphql/execution/executor.py @@ -541,10 +541,11 @@ def complete_value( if extensions: exe_context.update_extensions(extensions) if errors: - exe_context.report_error(error) for error in errors + for error in errors: + exe_context.report_error(error) return complete_value(exe_context, return_type, field_ast, info, path, data) - + # print return_type, type(result) if isinstance(result, Exception): raise GraphQLLocatedError(field_asts, original_error=result, path=path) From 9f7fe142165295c764eedd18322df5606f0f0167 Mon Sep 17 00:00:00 2001 From: nickharris Date: Mon, 27 Aug 2018 12:22:56 -0600 Subject: [PATCH 04/14] changed to deepcopy for cloning and updating extensions --- graphql/execution/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/graphql/execution/utils.py b/graphql/execution/utils.py index 26d811ab..d04a027c 100644 --- a/graphql/execution/utils.py +++ b/graphql/execution/utils.py @@ -1,4 +1,5 @@ # -*- coding: utf-8 -*- +from copy import deepcopy import logging from traceback import format_exception @@ -155,7 +156,7 @@ def report_error(self, error, traceback=None): self.errors.append(error) def update_extensions(self, extensions): - self.extensions = extensions.copy() if self.extensions else {} + self.extensions = deepcopy(extensions) if self.extensions else {} self.extensions.update(extensions) def get_sub_fields(self, return_type, field_asts): From 1adfc0d68e50dda4e32f15ace3e3847cf1f9e29b Mon Sep 17 00:00:00 2001 From: nickharris Date: Mon, 27 Aug 2018 12:31:46 -0600 Subject: [PATCH 05/14] added type docs --- graphql/execution/utils.py | 1 + 1 file changed, 1 insertion(+) diff --git a/graphql/execution/utils.py b/graphql/execution/utils.py index d04a027c..d0e81419 100644 --- a/graphql/execution/utils.py +++ b/graphql/execution/utils.py @@ -156,6 +156,7 @@ def report_error(self, error, traceback=None): self.errors.append(error) def update_extensions(self, extensions): + # type: Dict self.extensions = deepcopy(extensions) if self.extensions else {} self.extensions.update(extensions) From 38871cfd7f75102a68466fb4638deeb43cfa2dad Mon Sep 17 00:00:00 2001 From: nickharris Date: Mon, 27 Aug 2018 12:50:25 -0600 Subject: [PATCH 06/14] fixed formatting --- graphql/execution/executor.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/graphql/execution/executor.py b/graphql/execution/executor.py index 442aed74..6608fbb8 100644 --- a/graphql/execution/executor.py +++ b/graphql/execution/executor.py @@ -136,7 +136,9 @@ def on_resolve(data): if not exe_context.errors: return ExecutionResult(data=data) - return ExecutionResult(data=data, errors=exe_context.errors, extensions=exe_context.extensions) + return ExecutionResult( + data=data, errors=exe_context.errors, extensions=exe_context.extensions + ) promise = ( Promise.resolve(None).then(promise_executor).catch(on_rejected).then(on_resolve) @@ -409,7 +411,7 @@ def subscribe_field( variable_values=exe_context.variable_values, context=context, path=path, - extensions=exe_context.extensions + extensions=exe_context.extensions, ) executor = exe_context.executor @@ -464,7 +466,7 @@ def complete_value_catching_error( info, # type: ResolveInfo path, # type: List[Union[int, str]] result, # type: Any -): +): # type: (...) -> Any # If the field type is non-nullable, then it is resolved without any # protection from errors. @@ -534,15 +536,15 @@ def complete_value( ) if isinstance(result, ExecutionResult): - data = getattr(result, 'data', None) - extensions = getattr(result, 'extensions', None) - errors = getattr(result, 'errors', None) + data = getattr(result, "data", None) + extensions = getattr(result, "extensions", None) + errors = getattr(result, "errors", None) if extensions: exe_context.update_extensions(extensions) if errors: for error in errors: - exe_context.report_error(error) + exe_context.report_error(error) return complete_value(exe_context, return_type, field_ast, info, path, data) From 6e3908a2ab33e663b86da7b35e4cbb8d04d1d65f Mon Sep 17 00:00:00 2001 From: nickharris Date: Mon, 27 Aug 2018 12:56:38 -0600 Subject: [PATCH 07/14] fixed type comment --- graphql/execution/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphql/execution/utils.py b/graphql/execution/utils.py index d0e81419..e7876e53 100644 --- a/graphql/execution/utils.py +++ b/graphql/execution/utils.py @@ -156,7 +156,7 @@ def report_error(self, error, traceback=None): self.errors.append(error) def update_extensions(self, extensions): - # type: Dict + # type: (Dict[str, Any]) -> None self.extensions = deepcopy(extensions) if self.extensions else {} self.extensions.update(extensions) From 9a9b0650a0b675b46440d0d5fd82f8515ff54ca9 Mon Sep 17 00:00:00 2001 From: nickharris Date: Mon, 27 Aug 2018 13:02:21 -0600 Subject: [PATCH 08/14] fixed field_asts invocation error --- graphql/execution/executor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/graphql/execution/executor.py b/graphql/execution/executor.py index 6608fbb8..8794b267 100644 --- a/graphql/execution/executor.py +++ b/graphql/execution/executor.py @@ -546,7 +546,7 @@ def complete_value( for error in errors: exe_context.report_error(error) - return complete_value(exe_context, return_type, field_ast, info, path, data) + return complete_value(exe_context, return_type, field_asts, info, path, data) # print return_type, type(result) if isinstance(result, Exception): From 22340b65b48340bd23ddc1927f3d083cff8ad020 Mon Sep 17 00:00:00 2001 From: nickharris Date: Mon, 27 Aug 2018 13:14:41 -0600 Subject: [PATCH 09/14] added checks for missing errors and extensions kwargs for ExecutionResult --- graphql/execution/executor.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/graphql/execution/executor.py b/graphql/execution/executor.py index 8794b267..db319667 100644 --- a/graphql/execution/executor.py +++ b/graphql/execution/executor.py @@ -133,12 +133,15 @@ def on_resolve(data): if isinstance(data, Observable): return data - if not exe_context.errors: - return ExecutionResult(data=data) + kwargs = {} - return ExecutionResult( - data=data, errors=exe_context.errors, extensions=exe_context.extensions - ) + if exe_context.errors: + kwargs["errors"] = exe_context.errors + + if exe_context.extensions: + kwargs["extensions"] = exe_context.extensions + + return ExecutionResult(data=data, **kwargs) promise = ( Promise.resolve(None).then(promise_executor).catch(on_rejected).then(on_resolve) From 13253af8108533230362d4350be45c81340fc679 Mon Sep 17 00:00:00 2001 From: nickharris Date: Mon, 27 Aug 2018 18:58:07 -0600 Subject: [PATCH 10/14] fixed mypy type errors for executor on_resolve --- graphql/execution/executor.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/graphql/execution/executor.py b/graphql/execution/executor.py index db319667..795f24f7 100644 --- a/graphql/execution/executor.py +++ b/graphql/execution/executor.py @@ -133,15 +133,16 @@ def on_resolve(data): if isinstance(data, Observable): return data - kwargs = {} - - if exe_context.errors: - kwargs["errors"] = exe_context.errors - - if exe_context.extensions: - kwargs["extensions"] = exe_context.extensions - - return ExecutionResult(data=data, **kwargs) + if exe_context.errors and exe_context.extensions: + return ExecutionResult( + data=data, errors=exe_context.errors, extensions=exe_context.extensions + ) + elif exe_context.errors: + return ExecutionResult(data=data, errors=exe_context.errors) + elif exe_context.extensions: + return ExecutionResult(data=data, extensions=exe_context.extensions) + else: + return ExecutionResult(data=data) promise = ( Promise.resolve(None).then(promise_executor).catch(on_rejected).then(on_resolve) From de54321d967456a318358a8f3a35b479306a1dfd Mon Sep 17 00:00:00 2001 From: nickharris Date: Mon, 27 Aug 2018 19:24:53 -0600 Subject: [PATCH 11/14] added complete_value docs for ExecutionResult path --- graphql/execution/executor.py | 1 + 1 file changed, 1 insertion(+) diff --git a/graphql/execution/executor.py b/graphql/execution/executor.py index 795f24f7..e6b75842 100644 --- a/graphql/execution/executor.py +++ b/graphql/execution/executor.py @@ -539,6 +539,7 @@ def complete_value( ), ) + # If result is ExecutionResult, update exe_context and complete for data field if isinstance(result, ExecutionResult): data = getattr(result, "data", None) extensions = getattr(result, "extensions", None) From fa09a4b2643be8327189a81eb58921c870392f4a Mon Sep 17 00:00:00 2001 From: nickharris Date: Mon, 27 Aug 2018 20:11:45 -0600 Subject: [PATCH 12/14] added resolve extensions tests --- graphql/execution/tests/test_resolve.py | 52 +++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/graphql/execution/tests/test_resolve.py b/graphql/execution/tests/test_resolve.py index d6788f38..e66660d0 100644 --- a/graphql/execution/tests/test_resolve.py +++ b/graphql/execution/tests/test_resolve.py @@ -15,6 +15,7 @@ GraphQLSchema, GraphQLString, ) +from graphql.execution import ExecutionResult from promise import Promise # from graphql.execution.base import ResolveInfo @@ -112,6 +113,40 @@ def resolver(source, info, **args): ] +def test_handles_resolved_extensions_with_data(): + # type: () -> None + def resolver(source, info, **args): + # type: (Optional[str], ResolveInfo, **Any) -> ExecutionResult + extensions = info.extensions or {} + extensions["test_extensions"] = extensions.get("test_extensions", {}) + extensions["test_extensions"].update({"foo": "bar"}) + return ExecutionResult(data="foobar", extensions=extensions) + + schema = _test_schema(GraphQLField(GraphQLString, resolver=resolver)) + + result = graphql(schema, "{ test }", None) + assert not result.errors + assert result.data == {"test": "foobar"} + assert result.extensions == {"test_extensions": {"foo": "bar"}} + + +def test_handles_resolved_extensions_with_errors(): + # type: () -> None + def resolver(source, info, **args): + # type: (Optional[str], ResolveInfo, **Any) -> ExecutionResult + extensions = info.extensions or {} + extensions["errors"] = extensions.get("errors", {}) + extensions["errors"].update({"test": {"foo": "bar"}}) + return ExecutionResult(errors=[Exception()], extensions=extensions) + + schema = _test_schema(GraphQLField(GraphQLString, resolver=resolver)) + + result = graphql(schema, "{ test }", None) + assert len(result.errors) == 1 + assert result.data == {"test": None} + assert result.extensions == {"errors": {"test": {"foo": "bar"}}} + + def test_handles_resolved_promises(): # type: () -> None def resolver(source, info, **args): @@ -125,6 +160,23 @@ def resolver(source, info, **args): assert result.data == {"test": "foo"} +def test_handles_resolved_promises_extensions(): + # type: () -> None + def resolver(source, info, **args): + # type: (Optional[Any], ResolveInfo, **Any) -> Promise + extensions = info.extensions or {} + extensions["test_extensions"] = extensions.get("test_extensions", {}) + extensions["test_extensions"].update({"foo": "bar"}) + return Promise.resolve(ExecutionResult(data="foobar", extensions=extensions)) + + schema = _test_schema(GraphQLField(GraphQLString, resolver=resolver)) + + result = graphql(schema, "{ test }", None) + assert not result.errors + assert result.data == {"test": "foobar"} + assert result.extensions == {"test_extensions": {"foo": "bar"}} + + def test_handles_resolved_custom_promises(): # type: () -> None def resolver(source, info, **args): From de408a4fbd5ec52323ec22d025e219a002024453 Mon Sep 17 00:00:00 2001 From: nickharris Date: Fri, 7 Sep 2018 14:44:10 -0600 Subject: [PATCH 13/14] added extensions to ExecutionResult.to_dict --- graphql/execution/base.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/graphql/execution/base.py b/graphql/execution/base.py index f0759199..33f449d0 100644 --- a/graphql/execution/base.py +++ b/graphql/execution/base.py @@ -60,6 +60,9 @@ def to_dict(self, format_error=None, dict_class=OrderedDict): if not self.invalid: response["data"] = self.data + if self.extensions: + response["extensions"] = self.extensions + return response From 760850c1ad8891ae0f5f2735492da33fad3cc450 Mon Sep 17 00:00:00 2001 From: nikordaris Date: Tue, 11 Sep 2018 14:05:09 -0600 Subject: [PATCH 14/14] fixed update_extensions shallow merge --- graphql/execution/utils.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/graphql/execution/utils.py b/graphql/execution/utils.py index e7876e53..13622e39 100644 --- a/graphql/execution/utils.py +++ b/graphql/execution/utils.py @@ -157,8 +157,9 @@ def report_error(self, error, traceback=None): def update_extensions(self, extensions): # type: (Dict[str, Any]) -> None - self.extensions = deepcopy(extensions) if self.extensions else {} - self.extensions.update(extensions) + if extensions: + self.extensions = self.extensions or {} + self.extensions.update(extensions) def get_sub_fields(self, return_type, field_asts): # type: (GraphQLObjectType, List[Field]) -> DefaultOrderedDict