Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Re-create UnforgivingExecutionContext for graphql-core 3.1.5+ #1369

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions graphene/types/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
print_schema,
subscribe,
validate,
ExecutionContext,
ExecutionResult,
GraphQLArgument,
GraphQLBoolean,
Expand Down Expand Up @@ -54,6 +55,22 @@
introspection_query = get_introspection_query()
IntrospectionSchema = introspection_types["__Schema"]

class UnforgivingExecutionContext(ExecutionContext):
"""An execution context which doesn't swallow exceptions.
Instead it re-raises the original error, except for
GraphQLError, which is handled by graphql-core
"""

def handle_field_error(
self,
error: GraphQLError,
return_type,
) -> None:
if type(error.original_error) is GraphQLError:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In our project, I had to change this to isinstance(error.original_error, GraphQLError).

We have a few subclasses of GraphQLError to support transformations from different data types using OurGraphQLError.from_other_error(...). This makes it easier to support extension's within the errors.

super().handle_field_error(error, return_type)
else:
raise error.original_error


def assert_valid_root_type(type_):
if type_ is None:
Expand Down
118 changes: 115 additions & 3 deletions graphene/types/tests/test_schema.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
from graphql.type import GraphQLObjectType, GraphQLSchema
from pytest import raises

from graphql import GraphQLError
from pytest import fixture, mark, raises
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):
Expand Down Expand Up @@ -68,3 +68,115 @@ 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 ErrorFieldsMixin:
sanity_field = String()
expected_error_field = String()
unexpected_value_error_field = String()
unexpected_type_error_field = String()
unexpected_attribute_error_field = String()
unexpected_key_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_value_error_field(obj, info):
raise ValueError("unexpected error")

@staticmethod
def resolve_unexpected_type_error_field(obj, info):
raise TypeError("unexpected error")

@staticmethod
def resolve_unexpected_attribute_error_field(obj, info):
raise AttributeError("unexpected error")

@staticmethod
def resolve_unexpected_key_error_field(obj, info):
return {}["fails"]

class NestedObject(ErrorFieldsMixin, ObjectType):
pass

class MyQuery(ErrorFieldsMixin, ObjectType):
nested_object = Field(NestedObject)
nested_object_error = Field(NestedObject)

@staticmethod
def resolve_nested_object(obj, info):
return object()

@staticmethod
def resolve_nested_object_error(obj, info):
raise TypeError()

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_nested_sanity_check(self, schema):
# this should pass with no errors (sanity check)
result = schema.execute(
r"query { nestedObject { sanityField } }",
execution_context_class=UnforgivingExecutionContext,
)
assert not result.errors
assert result.data == {"nestedObject": {"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_nested_graphql_error(self, schema):
result = schema.execute(
r"query { nestedObject { expectedErrorField } }",
execution_context_class=UnforgivingExecutionContext,
)
assert len(result.errors) == 1
assert result.errors[0].message == "expected error"
assert result.data == {"nestedObject": {"expectedErrorField": None}}

@mark.parametrize(
"field,exception",
[
("unexpectedValueErrorField", ValueError),
("unexpectedTypeErrorField", TypeError),
("unexpectedAttributeErrorField", AttributeError),
("unexpectedKeyErrorField", KeyError),
("nestedObject { unexpectedValueErrorField }", ValueError),
("nestedObject { unexpectedTypeErrorField }", TypeError),
("nestedObject { unexpectedAttributeErrorField }", AttributeError),
("nestedObject { unexpectedKeyErrorField }", KeyError),
("nestedObjectError { __typename }", TypeError),
],
)
def test_unexpected_error(self, field, exception, schema):
with raises(exception):
# no result, but the exception should be propagated
schema.execute(
f"query {{ {field} }}",
execution_context_class=UnforgivingExecutionContext,
)