summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJenkins <jenkins@review.openstack.org>2015-08-22 17:04:42 +0000
committerGerrit Code Review <review@openstack.org>2015-08-22 17:04:42 +0000
commit4f1adeaf66163b87a92d3b622b57941308f11b8c (patch)
tree96a93cc6e7f10d071d0639f9477c4d6d7e65ebee
parent168ed1651060fe0aeeab496954cc7921faa262af (diff)
parenteba46f3a8e60d5dca81c9a42d5e20408319ec768 (diff)
downloadoslo-utils-2.4.0.tar.gz
Merge "Provide a common exception caused by base class"2.4.0
-rw-r--r--oslo_utils/excutils.py78
-rw-r--r--oslo_utils/tests/test_excutils.py35
2 files changed, 112 insertions, 1 deletions
diff --git a/oslo_utils/excutils.py b/oslo_utils/excutils.py
index a4eeace..dbaf0f6 100644
--- a/oslo_utils/excutils.py
+++ b/oslo_utils/excutils.py
@@ -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.
diff --git a/oslo_utils/tests/test_excutils.py b/oslo_utils/tests/test_excutils.py
index c2626a4..60f752d 100644
--- a/oslo_utils/tests/test_excutils.py
+++ b/oslo_utils/tests/test_excutils.py
@@ -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):