diff options
author | Lars Wirzenius <liw@liw.fi> | 2014-12-28 20:44:43 +0200 |
---|---|---|
committer | Lars Wirzenius <liw@liw.fi> | 2014-12-28 20:44:43 +0200 |
commit | 4b82cdec0cceb61abb6d1c933212e35b38f26e29 (patch) | |
tree | bd976feda0e2383f0054b0751eeff29f335bf99e | |
parent | 80a754e894de068a1d5c71cbd7607a9db0b6589d (diff) | |
download | obnam-4b82cdec0cceb61abb6d1c933212e35b38f26e29.tar.gz |
Add support for long error messages
-rw-r--r-- | obnamlib/structurederror.py | 30 | ||||
-rw-r--r-- | obnamlib/structurederror_tests.py | 24 |
2 files changed, 50 insertions, 4 deletions
diff --git a/obnamlib/structurederror.py b/obnamlib/structurederror.py index cebaecc9..81c4d5a4 100644 --- a/obnamlib/structurederror.py +++ b/obnamlib/structurederror.py @@ -17,6 +17,7 @@ import hashlib +import textwrap class StructuredError(Exception): @@ -44,6 +45,13 @@ class StructuredError(Exception): translations, though that is not currently implemented. The ID will also make translated log files more greppable. + The msg attribute is a format string. It can be arbitrarily long. + The __str__ method returns the first line only, but the full + message can be retrieved using the formatted() method. The + convention is to have the first line be a short summary of the + problem, and have the full message provide additional, helpful + information to the user. + The format string uses syntax according to the str.format specification (not the old % interpolation), in order to ease eventual migration to Python 3. @@ -96,17 +104,33 @@ class StructuredError(Exception): return 'R{0}X'.format(hash.upper()) def _format_msg(self, template): + # In case template is a docstring, remove leading whitespace + # from lines. + lines = template.splitlines(True) + if len(lines) == 0: + dedented = '' + else: + dedented = (textwrap.dedent(lines[0]) + + textwrap.dedent(''.join(lines[1:]))) + try: - formatted_msg = template.format(**self.kwargs) + formatted_msg = dedented.format(**self.kwargs) except KeyError as e: # If there were any errors in the formatting of the message, # report them here. We do NOT want replace the actual error # message, because that would hide information from the user. # We do want to know there was an error, though. formatted_msg = '{0} (PROGRAMMING ERROR: {1} {2})'.format( - template, repr(e), repr(self.kwargs)) + dedented, repr(e), repr(self.kwargs)) return '{0}: {1}'.format(self.id, formatted_msg) - def __str__(self): + def formatted(self): + '''Return the full formatted message.''' return self._format_msg(self.msg) + + def __str__(self): + full = self.formatted() + lines = full.splitlines() + assert lines + return lines[0] diff --git a/obnamlib/structurederror_tests.py b/obnamlib/structurederror_tests.py index ae43a7e6..ef49daca 100644 --- a/obnamlib/structurederror_tests.py +++ b/obnamlib/structurederror_tests.py @@ -23,7 +23,11 @@ import obnamlib class FirstError(obnamlib.StructuredError): - msg = 'first with parameter foo set to {foo}' + msg = '''first with parameter foo set to {foo} + + This is some explanatory text. + + ''' class SecondError(obnamlib.StructuredError): @@ -31,6 +35,12 @@ class SecondError(obnamlib.StructuredError): msg = 'second' +class EmptyError(obnamlib.StructuredError): + + msg = '' + + + class StructuredErrorTests(unittest.TestCase): def test_ids_differ_between_classes(self): @@ -49,3 +59,15 @@ class StructuredErrorTests(unittest.TestCase): def test_returns_error_string_even_with_lacking_keywords(self): first = FirstError() self.assertTrue(first.id in str(first)) + + def test_str_returns_first_line_only(self): + first = FirstError() + self.assertNotIn('\n', str(first)) + + def test_formatted_returns_full_message(self): + first = FirstError() + self.assertIn('\n', first.formatted()) + + def test_handles_empty_message_string(self): + empty = EmptyError() + self.assertTrue(str(empty).rstrip().endswith(':')) |