Retain nested causes where/when we can
On py3.x we should attempt to retain an exceptions causes (if any) so that they can be examined at a later time. This adds that change into gather and retain this information via a new failure causes property (that is lazily populated). This will aid in the change to traceback2 and the adjustments made in http://bugs.python.org/issue17911 so that we can in the future use its exception class instead. Change-Id: I8cadd7b70c2f638719155d96df4236fc40e18ccf
This commit is contained in:
parent
722d41b377
commit
1478f52c9a
@ -17,6 +17,8 @@
|
||||
import sys
|
||||
|
||||
import six
|
||||
from six.moves import cPickle as pickle
|
||||
import testtools
|
||||
|
||||
from taskflow import exceptions
|
||||
from taskflow import test
|
||||
@ -311,6 +313,101 @@ class NonAsciiExceptionsTestCase(test.TestCase):
|
||||
self.assertEqual(fail, copied)
|
||||
|
||||
|
||||
@testtools.skipIf(not six.PY3, 'this test only works on python 3.x')
|
||||
class FailureCausesTest(test.TestCase):
|
||||
|
||||
@classmethod
|
||||
def _raise_many(cls, messages):
|
||||
if not messages:
|
||||
return
|
||||
msg = messages.pop(0)
|
||||
e = RuntimeError(msg)
|
||||
try:
|
||||
cls._raise_many(messages)
|
||||
raise e
|
||||
except RuntimeError as e1:
|
||||
six.raise_from(e, e1)
|
||||
|
||||
def test_causes(self):
|
||||
f = None
|
||||
try:
|
||||
self._raise_many(["Still still not working",
|
||||
"Still not working", "Not working"])
|
||||
except RuntimeError:
|
||||
f = failure.Failure()
|
||||
|
||||
self.assertIsNotNone(f)
|
||||
self.assertEqual(2, len(f.causes))
|
||||
self.assertEqual("Still not working", f.causes[0].exception_str)
|
||||
self.assertEqual("Not working", f.causes[1].exception_str)
|
||||
|
||||
f = f.causes[0]
|
||||
self.assertEqual(1, len(f.causes))
|
||||
self.assertEqual("Not working", f.causes[0].exception_str)
|
||||
|
||||
f = f.causes[0]
|
||||
self.assertEqual(0, len(f.causes))
|
||||
|
||||
def test_causes_to_from_dict(self):
|
||||
f = None
|
||||
try:
|
||||
self._raise_many(["Still still not working",
|
||||
"Still not working", "Not working"])
|
||||
except RuntimeError:
|
||||
f = failure.Failure()
|
||||
|
||||
self.assertIsNotNone(f)
|
||||
d_f = f.to_dict()
|
||||
f = failure.Failure.from_dict(d_f)
|
||||
self.assertEqual(2, len(f.causes))
|
||||
self.assertEqual("Still not working", f.causes[0].exception_str)
|
||||
self.assertEqual("Not working", f.causes[1].exception_str)
|
||||
|
||||
f = f.causes[0]
|
||||
self.assertEqual(1, len(f.causes))
|
||||
self.assertEqual("Not working", f.causes[0].exception_str)
|
||||
|
||||
f = f.causes[0]
|
||||
self.assertEqual(0, len(f.causes))
|
||||
|
||||
def test_causes_pickle(self):
|
||||
f = None
|
||||
try:
|
||||
self._raise_many(["Still still not working",
|
||||
"Still not working", "Not working"])
|
||||
except RuntimeError:
|
||||
f = failure.Failure()
|
||||
|
||||
self.assertIsNotNone(f)
|
||||
p_f = pickle.dumps(f)
|
||||
f = pickle.loads(p_f)
|
||||
|
||||
self.assertEqual(2, len(f.causes))
|
||||
self.assertEqual("Still not working", f.causes[0].exception_str)
|
||||
self.assertEqual("Not working", f.causes[1].exception_str)
|
||||
|
||||
f = f.causes[0]
|
||||
self.assertEqual(1, len(f.causes))
|
||||
self.assertEqual("Not working", f.causes[0].exception_str)
|
||||
|
||||
f = f.causes[0]
|
||||
self.assertEqual(0, len(f.causes))
|
||||
|
||||
def test_causes_supress_context(self):
|
||||
f = None
|
||||
try:
|
||||
try:
|
||||
self._raise_many(["Still still not working",
|
||||
"Still not working", "Not working"])
|
||||
except RuntimeError as e:
|
||||
six.raise_from(e, None)
|
||||
except RuntimeError:
|
||||
f = failure.Failure()
|
||||
|
||||
self.assertIsNotNone(f)
|
||||
self.assertEqual([], list(f.causes))
|
||||
|
||||
|
||||
class ExcInfoUtilsTest(test.TestCase):
|
||||
def test_copy_none(self):
|
||||
result = failure._copy_exc_info(None)
|
||||
|
@ -152,8 +152,10 @@ class Failure(object):
|
||||
self._exception_str = exc.exception_message(self._exc_info[1])
|
||||
self._traceback_str = ''.join(
|
||||
traceback.format_tb(self._exc_info[2]))
|
||||
self._causes = kwargs.pop('causes', None)
|
||||
else:
|
||||
self._exc_info = exc_info # may be None
|
||||
self._causes = kwargs.pop('causes', None)
|
||||
self._exc_info = exc_info
|
||||
self._exception_str = kwargs.pop('exception_str')
|
||||
self._exc_type_names = tuple(kwargs.pop('exc_type_names', []))
|
||||
self._traceback_str = kwargs.pop('traceback_str', None)
|
||||
@ -172,7 +174,8 @@ class Failure(object):
|
||||
return True
|
||||
return (self._exc_type_names == other._exc_type_names
|
||||
and self.exception_str == other.exception_str
|
||||
and self.traceback_str == other.traceback_str)
|
||||
and self.traceback_str == other.traceback_str
|
||||
and self.causes == other.causes)
|
||||
|
||||
def matches(self, other):
|
||||
"""Checks if another object is equivalent to this object.
|
||||
@ -269,6 +272,66 @@ class Failure(object):
|
||||
return cls
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def _extract_causes_iter(cls, exc_val):
|
||||
seen = [exc_val]
|
||||
causes = [exc_val]
|
||||
while causes:
|
||||
exc_val = causes.pop()
|
||||
if exc_val is None:
|
||||
continue
|
||||
# See: https://www.python.org/dev/peps/pep-3134/ for why/what
|
||||
# these are...
|
||||
#
|
||||
# '__cause__' attribute for explicitly chained exceptions
|
||||
# '__context__' attribute for implicitly chained exceptions
|
||||
# '__traceback__' attribute for the traceback
|
||||
#
|
||||
# See: https://www.python.org/dev/peps/pep-0415/ for why/what
|
||||
# the '__suppress_context__' is/means/implies...
|
||||
supress_context = getattr(exc_val,
|
||||
'__suppress_context__', False)
|
||||
if supress_context:
|
||||
attr_lookups = ['__cause__']
|
||||
else:
|
||||
attr_lookups = ['__cause__', '__context__']
|
||||
nested_exc_val = None
|
||||
for attr_name in attr_lookups:
|
||||
attr_val = getattr(exc_val, attr_name, None)
|
||||
if attr_val is None:
|
||||
continue
|
||||
if attr_val not in seen:
|
||||
nested_exc_val = attr_val
|
||||
break
|
||||
if nested_exc_val is not None:
|
||||
exc_info = (
|
||||
type(nested_exc_val),
|
||||
nested_exc_val,
|
||||
getattr(nested_exc_val, '__traceback__', None),
|
||||
)
|
||||
seen.append(nested_exc_val)
|
||||
causes.append(nested_exc_val)
|
||||
yield cls(exc_info=exc_info)
|
||||
|
||||
@property
|
||||
def causes(self):
|
||||
"""Tuple of all *inner* failure *causes* of this failure.
|
||||
|
||||
NOTE(harlowja): Does **not** include the current failure (only
|
||||
returns connected causes of this failure, if any). This property
|
||||
is really only useful on 3.x or newer versions of python as older
|
||||
versions do **not** have associated causes (the tuple will **always**
|
||||
be empty on 2.x versions of python).
|
||||
|
||||
Refer to :pep:`3134` and :pep:`409` and :pep:`415` for what
|
||||
this is examining to find failure causes.
|
||||
"""
|
||||
if self._causes is not None:
|
||||
return self._causes
|
||||
else:
|
||||
self._causes = tuple(self._extract_causes_iter(self.exception))
|
||||
return self._causes
|
||||
|
||||
def __str__(self):
|
||||
return self.pformat()
|
||||
|
||||
@ -323,6 +386,10 @@ class Failure(object):
|
||||
self._exc_info = tuple(_fill_iter(dct['exc_info'], 3))
|
||||
else:
|
||||
self._exc_info = None
|
||||
causes = dct.get('causes')
|
||||
if causes is not None:
|
||||
causes = tuple(self.from_dict(d) for d in causes)
|
||||
self._causes = causes
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data):
|
||||
@ -332,6 +399,9 @@ class Failure(object):
|
||||
if version != cls.DICT_VERSION:
|
||||
raise ValueError('Invalid dict version of failure object: %r'
|
||||
% version)
|
||||
causes = data.get('causes')
|
||||
if causes is not None:
|
||||
data['causes'] = tuple(cls.from_dict(d) for d in causes)
|
||||
return cls(**data)
|
||||
|
||||
def to_dict(self):
|
||||
@ -341,6 +411,7 @@ class Failure(object):
|
||||
'traceback_str': self.traceback_str,
|
||||
'exc_type_names': list(self),
|
||||
'version': self.DICT_VERSION,
|
||||
'causes': [f.to_dict() for f in self.causes],
|
||||
}
|
||||
|
||||
def copy(self):
|
||||
@ -348,4 +419,5 @@ class Failure(object):
|
||||
return Failure(exc_info=_copy_exc_info(self.exc_info),
|
||||
exception_str=self.exception_str,
|
||||
traceback_str=self.traceback_str,
|
||||
exc_type_names=self._exc_type_names[:])
|
||||
exc_type_names=self._exc_type_names[:],
|
||||
causes=self._causes)
|
||||
|
Loading…
Reference in New Issue
Block a user