From 8c7326fb13cc85573c52d8876047b4765d0000a9 Mon Sep 17 00:00:00 2001 From: Christoph Zwerschke Date: Fri, 5 Apr 2019 16:00:19 +0200 Subject: [PATCH] Add traceback to errors from failed promises The stack attribute of GraphQLLocatedErrors was not set for errors that resulted from asynchronous resolvers. We add a unit test for this problem and a solution that works with Python >= 3. In Python 2 this problem is not yet solved, and may be never solved, because dealing with stack traces is much more difficult there. --- graphql/error/located_error.py | 12 +++++-- graphql/error/tests/test_base.py | 54 ++++++++++++++++++++++++-------- 2 files changed, 50 insertions(+), 16 deletions(-) diff --git a/graphql/error/located_error.py b/graphql/error/located_error.py index fe84e495..9c538863 100644 --- a/graphql/error/located_error.py +++ b/graphql/error/located_error.py @@ -26,9 +26,15 @@ def __init__( else: message = "An unknown error occurred." - stack = original_error and getattr(original_error, "stack", None) - if not stack: - stack = sys.exc_info()[2] + stack = ( + original_error + and ( + getattr(original_error, "stack", None) + # unfortunately, this is only available in Python 3: + or getattr(original_error, "__traceback__", None) + ) + or sys.exc_info()[2] + ) super(GraphQLLocatedError, self).__init__( message=message, nodes=nodes, stack=stack, path=path diff --git a/graphql/error/tests/test_base.py b/graphql/error/tests/test_base.py index e53f152a..3ddc091c 100644 --- a/graphql/error/tests/test_base.py +++ b/graphql/error/tests/test_base.py @@ -1,6 +1,10 @@ +import sys + import pytest import traceback +from promise import Promise + from graphql.execution import execute from graphql.language.parser import parse from graphql.type import GraphQLField, GraphQLObjectType, GraphQLSchema, GraphQLString @@ -46,13 +50,10 @@ def resolver(context, *_): extracted = traceback.extract_tb(exc_info.tb) formatted_tb = [row[2:] for row in extracted] - if formatted_tb[2][0] == "reraise": - formatted_tb[2:] = formatted_tb[3:] + formatted_tb = [tb for tb in formatted_tb if tb[0] != "reraise"] assert formatted_tb == [ ("test_reraise", "result.errors[0].reraise()"), - ("reraise", "six.reraise(type(self), self, self.stack)"), - # ('reraise', 'raise value.with_traceback(tb)'), ( "resolve_or_error", "return executor.execute(resolve_fn, source, info, **args)", @@ -60,14 +61,41 @@ def resolver(context, *_): ("execute", "return fn(*args, **kwargs)"), ("resolver", 'raise Exception("Failed")'), ] - # assert formatted_tb == [ - # ('test_reraise', 'result.errors[0].reraise()'), - # ('reraise', 'six.reraise(type(self), self, self.stack)'), - # ('on_complete_resolver', 'result = __resolver(*args, **kwargs)'), - # # ('reraise', 'raise value.with_traceback(tb)'), - # # ('resolve_or_error', 'return executor.execute(resolve_fn, source, info, **args)'), - # # ('execute', 'return fn(*args, **kwargs)'), - # ('resolver', "raise Exception('Failed')") - # ] + + assert str(exc_info.value) == "Failed" + + +@pytest.mark.skipif(sys.version_info < (3,), reason="this works only with Python 3") +def test_reraise_from_promise(): + # type: () -> None + ast = parse("query Example { a }") + + def fail(): + raise Exception("Failed") + + def resolver(context, *_): + # type: (Optional[Any], *ResolveInfo) -> None + return Promise(lambda resolve, reject: resolve(fail())) + + Type = GraphQLObjectType( + "Type", {"a": GraphQLField(GraphQLString, resolver=resolver)} + ) + + result = execute(GraphQLSchema(Type), ast) + with pytest.raises(Exception) as exc_info: + result.errors[0].reraise() + + extracted = traceback.extract_tb(exc_info.tb) + formatted_tb = [row[2:] for row in extracted] + formatted_tb = [tb for tb in formatted_tb if tb[0] != "reraise"] + + print(formatted_tb) + + assert formatted_tb == [ + ("test_reraise_from_promise", "result.errors[0].reraise()"), + ("_resolve_from_executor", "executor(resolve, reject)"), + ("", "return Promise(lambda resolve, reject: resolve(fail()))"), + ("fail", 'raise Exception("Failed")'), + ] assert str(exc_info.value) == "Failed"