diff --git a/graphene/types/schema.py b/graphene/types/schema.py index 55f0bf93..a941c245 100644 --- a/graphene/types/schema.py +++ b/graphene/types/schema.py @@ -28,6 +28,8 @@ from graphql import ( GraphQLString, Undefined, ) +from graphql.execution import ExecutionContext +from graphql.execution.values import get_argument_values from ..utils.str_converters import to_camel_case from ..utils.get_unbound_function import get_unbound_function @@ -394,6 +396,57 @@ class TypeMap(dict): return type_ +class UnforgivingExecutionContext(ExecutionContext): + """An execution context which doesn't swallow exceptions. + + The only difference between this execution context and the one it inherits from is + that ``except Exception`` is commented out within ``resolve_field_value_or_error``. + By removing that exception handling, only ``GraphQLError``'s are caught. + """ + + def resolve_field_value_or_error( + self, field_def, field_nodes, resolve_fn, source, info, + ): + """Resolve field to a value or an error. + + Isolates the "ReturnOrAbrupt" behavior to not de-opt the resolve_field() + method. Returns the result of resolveFn or the abrupt-return Error object. + + For internal use only. + """ + try: + # Build a dictionary of arguments from the field.arguments AST, using the + # variables scope to fulfill any variable references. + args = get_argument_values(field_def, field_nodes[0], self.variable_values) + + # Note that contrary to the JavaScript implementation, we pass the context + # value as part of the resolve info. + result = resolve_fn(source, info, **args) + if self.is_awaitable(result): + # noinspection PyShadowingNames + async def await_result(): + try: + return await result + except GraphQLError as error: + return error + # except Exception as error: + # return GraphQLError(str(error), original_error=error) + + # Yes, this is commented out code. It's been intentionally + # _not_ removed to show what has changed from the original + # implementation. + + return await_result() + return result + except GraphQLError as error: + return error + # except Exception as error: + # return GraphQLError(str(error), original_error=error) + + # Yes, this is commented out code. It's been intentionally _not_ + # removed to show what has changed from the original implementation. + + class Schema: """Schema Definition. @@ -481,6 +534,8 @@ class Schema: request_string, an operation name must be provided for the result to be provided. middleware (List[SupportsGraphQLMiddleware]): Supply request level middleware as defined in `graphql-core`. + execution_context_class (ExecutionContext, optional): The execution context class + to use when resolving queries and mutations. Returns: :obj:`ExecutionResult` containing any data and errors for the operation. diff --git a/graphene/types/tests/test_schema.py b/graphene/types/tests/test_schema.py index fe4739c9..d3fa774f 100644 --- a/graphene/types/tests/test_schema.py +++ b/graphene/types/tests/test_schema.py @@ -1,12 +1,13 @@ from graphql.type import GraphQLObjectType, GraphQLSchema -from pytest import raises +from graphql import GraphQLError +from pytest import raises, fixture from graphene.tests.utils import dedent from ..field import Field from ..objecttype import ObjectType from ..scalars import String -from ..schema import Schema +from ..schema import Schema, UnforgivingExecutionContext class MyOtherType(ObjectType): @@ -68,3 +69,53 @@ def test_schema_requires_query_type(): assert len(result.errors) == 1 error = result.errors[0] assert error.message == "Query root type must be provided." + + +class TestUnforgivingExecutionContext: + @fixture + def schema(self): + class MyQuery(ObjectType): + sanity_field = String() + expected_error_field = String() + unexpected_error_field = String() + + @staticmethod + def resolve_sanity_field(obj, info): + return "not an error" + + @staticmethod + def resolve_expected_error_field(obj, info): + raise GraphQLError("expected error") + + @staticmethod + def resolve_unexpected_error_field(obj, info): + raise ValueError("unexpected error") + + schema = Schema(query=MyQuery) + return schema + + def test_sanity_check(self, schema): + # this should pass with no errors (sanity check) + result = schema.execute( + "query { sanityField }", + execution_context_class=UnforgivingExecutionContext, + ) + assert not result.errors + assert result.data == {"sanityField": "not an error"} + + def test_graphql_error(self, schema): + result = schema.execute( + "query { expectedErrorField }", + execution_context_class=UnforgivingExecutionContext, + ) + assert len(result.errors) == 1 + assert result.errors[0].message == "expected error" + assert result.data == {"expectedErrorField": None} + + def test_unexpected_error(self, schema): + with raises(ValueError): + # no result, but the exception should be propagated + schema.execute( + "query { unexpectedErrorField }", + execution_context_class=UnforgivingExecutionContext, + )