Skip to content

Commit

Permalink
Add traceback to errors from failed promises (#240)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Cito authored Jun 4, 2019
1 parent 0cddba7 commit 8f736b7
Show file tree
Hide file tree
Showing 2 changed files with 50 additions and 16 deletions.
12 changes: 9 additions & 3 deletions graphql/error/located_error.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
)

extensions = (
getattr(original_error, "extensions", None) if original_error else None
Expand Down
54 changes: 41 additions & 13 deletions graphql/error/tests/test_base.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -46,28 +50,52 @@ 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)",
),
("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)"),
("<lambda>", "return Promise(lambda resolve, reject: resolve(fail()))"),
("fail", 'raise Exception("Failed")'),
]

assert str(exc_info.value) == "Failed"

0 comments on commit 8f736b7

Please sign in to comment.