Skip to content

Commit

Permalink
Improved traceback tracking without mutation the exception
Browse files Browse the repository at this point in the history
  • Loading branch information
syrusakbary committed Apr 13, 2017
1 parent 3c70aec commit 4e9457f
Show file tree
Hide file tree
Showing 2 changed files with 43 additions and 23 deletions.
50 changes: 27 additions & 23 deletions promise/promise.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from sys import version_info, exc_info
from threading import Event, RLock

from six import reraise as raise_
from six import reraise
from typing import (List, Any, Callable, Dict, Iterator, Optional, # flake8: noqa
Union)

Expand Down Expand Up @@ -58,7 +58,8 @@ def make_self_resolution_error():


_error_obj = {
'e': None
'e': None,
't': None
} # type: Dict[str, Union[None, Exception]]


Expand All @@ -67,10 +68,8 @@ def try_catch(handler, *args, **kwargs):
return handler(*args, **kwargs)
except Exception as e:
tb = exc_info()[2]
if tb is not None and not hasattr(e, '__traceback__'):
e.__traceback__ = tb
_error_obj['e'] = e
# print traceback.format_exc()
_error_obj['t'] = tb
return _error_obj


Expand Down Expand Up @@ -100,6 +99,7 @@ class Promise(object):
_promise0 = None # type: Promise
_future = None # type: Future
_event_instance = None # type: Event
_traceback = None
# _trace = None
_is_waiting = False

Expand Down Expand Up @@ -184,7 +184,7 @@ def _resolve_callback(self, value):
elif promise._state == STATE_FULFILLED:
self._fulfill(promise._value())
elif promise._state == STATE_REJECTED:
self._reject(promise._reason())
self._reject(promise._reason(), promise._target()._traceback)

def _settled_value(self, _raise=False):
assert not self._is_following
Expand All @@ -194,7 +194,7 @@ def _settled_value(self, _raise=False):
elif self._state == STATE_REJECTED:
if _raise:
raise_val = self._fulfillment_handler0
raise_(type(raise_val), raise_val, getattr(raise_val, '__traceback__', None))
reraise(type(raise_val), raise_val, self._traceback)
return self._fulfillment_handler0

def _fulfill(self, value):
Expand All @@ -211,9 +211,10 @@ def _fulfill(self, value):
else:
async_instance.settle_promises(self)

def _reject(self, reason):
def _reject(self, reason, traceback=None):
self._state = STATE_REJECTED
self._fulfillment_handler0 = reason
self._traceback = traceback

if self._is_final:
assert self._length == 0
Expand All @@ -234,12 +235,12 @@ def _ensure_possible_rejection_handled(self):
# async_instance.invoke_later(self._notify_unhandled_rejection, self)
pass

def _reject_callback(self, reason, synchronous=False):
def _reject_callback(self, reason, synchronous=False, traceback=None):
assert isinstance(reason, Exception), "A promise was rejected with a non-error: {}".format(reason)
# trace = ensure_error_object(reason)
# has_stack = trace is reason
# self._attach_extratrace(trace, synchronous and has_stack)
self._reject(reason)
self._reject(reason, traceback)

def _clear_callback_data_index_at(self, index):
assert not self._is_following
Expand All @@ -254,16 +255,16 @@ def _fulfill_promises(self, length, value):
handler = self._fulfillment_handler_at(i)
promise = self._promise_at(i)
self._clear_callback_data_index_at(i)
self._settle_promise(promise, handler, value)
self._settle_promise(promise, handler, value, None)

def _reject_promises(self, length, reason):
for i in range(1, length):
handler = self._rejection_handler_at(i)
promise = self._promise_at(i)
self._clear_callback_data_index_at(i)
self._settle_promise(promise, handler, reason)
self._settle_promise(promise, handler, reason, None)

def _settle_promise(self, promise, handler, value):
def _settle_promise(self, promise, handler, value, traceback):
assert not self._is_following
is_promise = isinstance(promise, self.__class__)
async_guaranteed = self._is_async_guaranteed
Expand All @@ -280,12 +281,12 @@ def _settle_promise(self, promise, handler, value):
if self._state == STATE_FULFILLED:
promise._fulfill(value)
else:
promise._reject(value)
promise._reject(value, self._traceback)

def _settle_promise0(self, handler, value):
def _settle_promise0(self, handler, value, traceback):
promise = self._promise0
self._promise0 = None
self._settle_promise(promise, handler, value)
self._settle_promise(promise, handler, value, traceback)

def _settle_promise_from_handler(self, handler, value, promise):
# promise._push_context()
Expand All @@ -294,7 +295,7 @@ def _settle_promise_from_handler(self, handler, value, promise):
# promise_created = promise._pop_context()

if x == _error_obj:
promise._reject_callback(x['e'], False)
promise._reject_callback(x['e'], False, x['t'])
# if isinstance(x, PromiseError):
# promise._reject_callback(x.e, False)
else:
Expand Down Expand Up @@ -390,11 +391,12 @@ def _settle_promises(self):
if length > 0:
if self._state == STATE_REJECTED:
reason = self._fulfillment_handler0
self._settle_promise0(self._rejection_handler0, reason)
traceback = self._traceback
self._settle_promise0(self._rejection_handler0, reason, traceback)
self._reject_promises(length, reason)
else:
value = self._rejection_handler0
self._settle_promise0(self._fulfillment_handler0, value)
self._settle_promise0(self._fulfillment_handler0, value, None)
self._fulfill_promises(length, value)

self._length = 0
Expand All @@ -412,18 +414,18 @@ def reject(reason):
self._reject_callback(reason, synchronous)

error = None
traceback = None
try:
executor(resolve, reject)
except Exception as e:
# import traceback
traceback = exc_info()[2]
error = e
# print traceback.format_exc()

synchronous = False
# self._pop_context()

if error is not None:
self._reject_callback(error, True)
self._reject_callback(error, True, traceback)

def _event(self):
if not self._event_instance:
Expand Down Expand Up @@ -525,15 +527,17 @@ def _then(self, did_fulfill=None, did_reject=None):
if state == STATE_PENDING:
target._add_callbacks(did_fulfill, did_reject, promise)
else:
traceback = None
if state == STATE_FULFILLED:
value = target._rejection_handler0
handler = did_fulfill
elif state == STATE_REJECTED:
value = target._fulfillment_handler0
traceback = target._traceback
handler = did_reject
# target._rejection_is_unhandled = False
async_instance.invoke(
partial(target._settle_promise, promise, handler, value),
partial(target._settle_promise, promise, handler, value, traceback),
context=target._trace,
# target._settle_promise instead?
# settler,
Expand Down
16 changes: 16 additions & 0 deletions tests/test_extra.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,25 @@ def throws(v):
def test_thrown_exceptions_have_stacktrace():
def throws(v):
assert False

p3 = Promise.resolve('a').then(throws)
with raises(AssertionError) as assert_exc:
p3.get()

assert assert_exc.traceback[-1].path.strpath == __file__


def test_thrown_exceptions_preserve_stacktrace():
def throws(v):
assert False

def after_throws(v):
pass

p3 = Promise.resolve('a').then(throws).then(after_throws)
with raises(AssertionError) as assert_exc:
p3.get()

assert assert_exc.traceback[-1].path.strpath == __file__


Expand Down

0 comments on commit 4e9457f

Please sign in to comment.