Merge "Provide a common exception caused by base class"

This commit is contained in:
Jenkins 2015-08-22 17:04:42 +00:00 committed by Gerrit Code Review
commit 4f1adeaf66
2 changed files with 112 additions and 1 deletions

View File

@ -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.

View File

@ -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):