Merge "Provide a common exception caused by base class"
This commit is contained in:
commit
4f1adeaf66
@ -18,6 +18,7 @@ Exception related utilities.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import traceback
|
||||
@ -25,6 +26,80 @@ import traceback
|
||||
import six
|
||||
|
||||
from oslo_utils._i18n import _LE
|
||||
from oslo_utils import reflection
|
||||
|
||||
|
||||
class CausedByException(Exception):
|
||||
"""Base class for exceptions which have associated causes.
|
||||
|
||||
NOTE(harlowja): in later versions of python we can likely remove the need
|
||||
to have a ``cause`` here as PY3+ have implemented :pep:`3134` which
|
||||
handles chaining in a much more elegant manner.
|
||||
|
||||
:param message: the exception message, typically some string that is
|
||||
useful for consumers to view when debugging or analyzing
|
||||
failures.
|
||||
:param cause: the cause of the exception being raised, when provided this
|
||||
should itself be an exception instance, this is useful for
|
||||
creating a chain of exceptions for versions of python where
|
||||
this is not yet implemented/supported natively.
|
||||
"""
|
||||
def __init__(self, message, cause=None):
|
||||
super(CausedByException, self).__init__(message)
|
||||
self.cause = cause
|
||||
|
||||
def __bytes__(self):
|
||||
return self.pformat().encode("utf8")
|
||||
|
||||
def __str__(self):
|
||||
return self.pformat()
|
||||
|
||||
def _get_message(self):
|
||||
# We must *not* call into the ``__str__`` method as that will
|
||||
# reactivate the pformat method, which will end up badly (and doesn't
|
||||
# look pretty at all); so be careful...
|
||||
return self.args[0]
|
||||
|
||||
def pformat(self, indent=2, indent_text=" ", show_root_class=False):
|
||||
"""Pretty formats a caused exception + any connected causes."""
|
||||
if indent < 0:
|
||||
raise ValueError("Provided 'indent' must be greater than"
|
||||
" or equal to zero instead of %s" % indent)
|
||||
buf = six.StringIO()
|
||||
if show_root_class:
|
||||
buf.write(reflection.get_class_name(self, fully_qualified=False))
|
||||
buf.write(": ")
|
||||
buf.write(self._get_message())
|
||||
active_indent = indent
|
||||
next_up = self.cause
|
||||
seen = []
|
||||
while next_up is not None and next_up not in seen:
|
||||
seen.append(next_up)
|
||||
buf.write(os.linesep)
|
||||
if isinstance(next_up, CausedByException):
|
||||
buf.write(indent_text * active_indent)
|
||||
buf.write(reflection.get_class_name(next_up,
|
||||
fully_qualified=False))
|
||||
buf.write(": ")
|
||||
buf.write(next_up._get_message())
|
||||
else:
|
||||
lines = traceback.format_exception_only(type(next_up), next_up)
|
||||
for i, line in enumerate(lines):
|
||||
buf.write(indent_text * active_indent)
|
||||
if line.endswith("\n"):
|
||||
# We'll add our own newlines on...
|
||||
line = line[0:-1]
|
||||
buf.write(line)
|
||||
if i + 1 != len(lines):
|
||||
buf.write(os.linesep)
|
||||
if not isinstance(next_up, CausedByException):
|
||||
# Don't go deeper into non-caused-by exceptions... as we
|
||||
# don't know if there exception 'cause' attributes are even
|
||||
# useable objects...
|
||||
break
|
||||
active_indent += indent
|
||||
next_up = getattr(next_up, 'cause', None)
|
||||
return buf.getvalue()
|
||||
|
||||
|
||||
def raise_with_cause(exc_cls, message, *args, **kwargs):
|
||||
@ -40,7 +115,8 @@ def raise_with_cause(exc_cls, message, *args, **kwargs):
|
||||
inspected/retained on py2.x to get *similar* information as would be
|
||||
automatically included/obtainable in py3.x.
|
||||
|
||||
:param exc_cls: the exception class to raise.
|
||||
:param exc_cls: the exception class to raise (typically one derived
|
||||
from :py:class:`.CausedByException` or equivalent).
|
||||
:param message: the text/str message that will be passed to
|
||||
the exceptions constructor as its first positional
|
||||
argument.
|
||||
|
@ -25,6 +25,41 @@ from oslo_utils import excutils
|
||||
mox = moxstubout.mox
|
||||
|
||||
|
||||
class Fail1(excutils.CausedByException):
|
||||
pass
|
||||
|
||||
|
||||
class Fail2(excutils.CausedByException):
|
||||
pass
|
||||
|
||||
|
||||
class CausedByTest(test_base.BaseTestCase):
|
||||
|
||||
def test_caused_by_explicit(self):
|
||||
e = self.assertRaises(Fail1,
|
||||
excutils.raise_with_cause,
|
||||
Fail1, "I was broken",
|
||||
cause=Fail2("I have been broken"))
|
||||
self.assertIsInstance(e.cause, Fail2)
|
||||
e_p = e.pformat()
|
||||
self.assertIn("I have been broken", e_p)
|
||||
self.assertIn("Fail2", e_p)
|
||||
|
||||
def test_caused_by_implicit(self):
|
||||
|
||||
def raises_chained():
|
||||
try:
|
||||
raise Fail2("I have been broken")
|
||||
except Fail2:
|
||||
excutils.raise_with_cause(Fail1, "I was broken")
|
||||
|
||||
e = self.assertRaises(Fail1, raises_chained)
|
||||
self.assertIsInstance(e.cause, Fail2)
|
||||
e_p = e.pformat()
|
||||
self.assertIn("I have been broken", e_p)
|
||||
self.assertIn("Fail2", e_p)
|
||||
|
||||
|
||||
class SaveAndReraiseTest(test_base.BaseTestCase):
|
||||
|
||||
def test_save_and_reraise_exception(self):
|
||||
|
Loading…
Reference in New Issue
Block a user