From d16e111b536f60d3b455b05d20a1849f62a77e8e Mon Sep 17 00:00:00 2001 From: Joshua Harlow Date: Mon, 23 Mar 2015 20:44:50 -0700 Subject: [PATCH] Make an attempt at having taskflow exceptions print causes better It is often quite useful to try to see what the contained causes are on versions of python that do not have the native support for this built-in so to make everyones life easier add basic support for traversing the cause list and printing out associated causes when we are able to. Change-Id: Ia0a7e13757a989722291bcc06599d04014706d8c --- taskflow/exceptions.py | 71 ++++++++++++++++---------- taskflow/tests/unit/test_exceptions.py | 38 ++++++++++++++ 2 files changed, 82 insertions(+), 27 deletions(-) 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