diff --git a/taskflow/exceptions.py b/taskflow/exceptions.py index ae7baef4..069a2f2a 100644 --- a/taskflow/exceptions.py +++ b/taskflow/exceptions.py @@ -18,6 +18,7 @@ import os import sys import traceback +from oslo_utils import reflection import six @@ -76,35 +77,51 @@ class TaskFlowException(Exception): def cause(self): return self._cause - def pformat(self, indent=2, indent_text=" "): + 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 taskflow exception + any connected causes.""" if indent < 0: - raise ValueError("indent must be greater than or equal to zero") - return os.linesep.join(self._pformat(self, [], 0, - indent=indent, - indent_text=indent_text)) - - @classmethod - def _pformat(cls, excp, lines, current_indent, indent=2, indent_text=" "): - line_prefix = indent_text * current_indent - for line in traceback.format_exception_only(type(excp), excp): - # We'll add our own newlines on at the end of formatting. - # - # NOTE(harlowja): the reason we don't search for os.linesep is - # that the traceback module seems to only use '\n' (for some - # reason). - if line.endswith("\n"): - line = line[0:-1] - lines.append(line_prefix + line) - try: - cause = excp.cause - except AttributeError: - pass - else: - if cause is not None: - cls._pformat(cause, lines, current_indent + indent, - indent=indent, indent_text=indent_text) - return lines + 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 + while next_up is not None: + buf.write(os.linesep) + if isinstance(next_up, TaskFlowException): + 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) + active_indent += indent + try: + next_up = next_up.cause + except AttributeError: + next_up = None + return buf.getvalue() # Errors related to storage or operations on storage units. diff --git a/taskflow/tests/unit/test_exceptions.py b/taskflow/tests/unit/test_exceptions.py index d834ce7c..b3590653 100644 --- a/taskflow/tests/unit/test_exceptions.py +++ b/taskflow/tests/unit/test_exceptions.py @@ -56,6 +56,44 @@ class TestExceptions(test.TestCase): self.assertIsNotNone(capture.cause) self.assertIsInstance(capture.cause, IOError) + def test_pformat_str(self): + ex = None + try: + try: + try: + raise IOError("Didn't work") + except IOError: + exc.raise_with_cause(exc.TaskFlowException, + "It didn't go so well") + except exc.TaskFlowException: + exc.raise_with_cause(exc.TaskFlowException, "I Failed") + except exc.TaskFlowException as e: + ex = e + + self.assertIsNotNone(ex) + self.assertIsInstance(ex, exc.TaskFlowException) + self.assertIsInstance(ex.cause, exc.TaskFlowException) + self.assertIsInstance(ex.cause.cause, IOError) + + p_msg = ex.pformat() + p_str_msg = str(ex) + for msg in ["I Failed", "It didn't go so well", "Didn't work"]: + self.assertIn(msg, p_msg) + self.assertIn(msg, p_str_msg) + + def test_pformat_root_class(self): + ex = exc.TaskFlowException("Broken") + self.assertIn("TaskFlowException", + ex.pformat(show_root_class=True)) + self.assertNotIn("TaskFlowException", + ex.pformat(show_root_class=False)) + self.assertIn("Broken", + ex.pformat(show_root_class=True)) + + def test_invalid_pformat_indent(self): + ex = exc.TaskFlowException("Broken") + self.assertRaises(ValueError, ex.pformat, indent=-100) + @testtools.skipIf(not six.PY3, 'py3.x is not available') def test_raise_with_cause(self): capture = None