diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/zope/exceptions/exceptionformatter.py | 42 | ||||
-rw-r--r-- | src/zope/exceptions/log.py | 8 | ||||
-rw-r--r-- | src/zope/exceptions/tests/test_exceptionformatter.py | 189 | ||||
-rw-r--r-- | src/zope/exceptions/tests/test_log.py | 31 |
4 files changed, 166 insertions, 104 deletions
diff --git a/src/zope/exceptions/exceptionformatter.py b/src/zope/exceptions/exceptionformatter.py index fabea9f..47e0f41 100644 --- a/src/zope/exceptions/exceptionformatter.py +++ b/src/zope/exceptions/exceptionformatter.py @@ -46,7 +46,12 @@ class TextExceptionFormatter(object): return limit def formatSupplementLine(self, line): - return ' - %s' % line + result = ' - %s' % line + if not isinstance(result, str): + # Must be an Python 2, and must be a unicode `line` + # and we upconverted the result to a unicode + result = result.encode('utf-8') + return result def formatSourceURL(self, url): return [self.formatSupplementLine(url)] @@ -110,13 +115,13 @@ class TextExceptionFormatter(object): co = f.f_code filename = co.co_filename name = co.co_name - locals = f.f_locals # XXX shadowing normal builtins deliberately? - globals = f.f_globals # XXX shadowing normal builtins deliberately? + f_locals = f.f_locals + f_globals = f.f_globals if self.with_filenames: s = ' File "%s", line %d' % (filename, lineno) else: - modname = globals.get('__name__', filename) + modname = f_globals.get('__name__', filename) s = ' Module %s, line %d' % (modname, lineno) s = s + ', in %s' % name @@ -130,13 +135,13 @@ class TextExceptionFormatter(object): result.append(" " + self.escape(line.strip())) # Output a traceback supplement, if any. - if '__traceback_supplement__' in locals: + if '__traceback_supplement__' in f_locals: # Use the supplement defined in the function. - tbs = locals['__traceback_supplement__'] - elif '__traceback_supplement__' in globals: + tbs = f_locals['__traceback_supplement__'] + elif '__traceback_supplement__' in f_globals: # Use the supplement defined in the module. # This is used by Scripts (Python). - tbs = globals['__traceback_supplement__'] + tbs = f_globals['__traceback_supplement__'] else: tbs = None if tbs is not None: @@ -151,7 +156,7 @@ class TextExceptionFormatter(object): # else just swallow the exception. try: - tbi = locals.get('__traceback_info__', None) + tbi = f_locals.get('__traceback_info__', None) if tbi is not None: result.append(self.formatTracebackInfo(tbi)) except: #pragma: no cover @@ -240,13 +245,23 @@ class HTMLExceptionFormatter(TextExceptionFormatter): line_sep = '<br />\r\n' def escape(self, s): + if not isinstance(s, str): + try: + s = str(s) + except UnicodeError: + if hasattr(s, 'encode'): + # We probably got a unicode string on + # Python 2. + s = s.encode('utf-8') + else: # pragma: no cover + raise return escape(s, quote=False) def getPrefix(self): return '<p>Traceback (most recent call last):</p>\r\n<ul>' def formatSupplementLine(self, line): - return '<b>%s</b>' % self.escape(str(line)) + return '<b>%s</b>' % self.escape(line) def formatSupplementInfo(self, info): info = self.escape(info) @@ -255,7 +270,7 @@ class HTMLExceptionFormatter(TextExceptionFormatter): return info def formatTracebackInfo(self, tbi): - s = self.escape(str(tbi)) + s = self.escape(tbi) s = s.replace('\n', self.line_sep) return '__traceback_info__: %s' % (s, ) @@ -275,6 +290,9 @@ def format_exception(t, v, tb, limit=None, as_html=False, Similar to 'traceback.format_exception', but adds supplemental information to the traceback and accepts two options, 'as_html' and 'with_filenames'. + + The result is a list of native strings; on Python 2 they are UTF-8 + encoded if need be. """ if as_html: fmt = HTMLExceptionFormatter(limit, with_filenames) @@ -291,7 +309,7 @@ def print_exception(t, v, tb, limit=None, file=None, as_html=False, information to the traceback and accepts two options, 'as_html' and 'with_filenames'. """ - if file is None: #pragma: no cover + if file is None: # pragma: no cover file = sys.stderr lines = format_exception(t, v, tb, limit, as_html, with_filenames) for line in lines: diff --git a/src/zope/exceptions/log.py b/src/zope/exceptions/log.py index 735429a..574ccd3 100644 --- a/src/zope/exceptions/log.py +++ b/src/zope/exceptions/log.py @@ -15,13 +15,11 @@ """ import logging -try: - from StringIO import StringIO -except ImportError: #pragma: no cover Python3 - from io import StringIO +import io from zope.exceptions.exceptionformatter import print_exception +Buffer = io.StringIO if bytes is not str else io.BytesIO class Formatter(logging.Formatter): @@ -30,7 +28,7 @@ class Formatter(logging.Formatter): Uses zope.exceptions.exceptionformatter to generate the traceback. """ - sio = StringIO() + sio = Buffer() print_exception(ei[0], ei[1], ei[2], file=sio, with_filenames=True) s = sio.getvalue() if s.endswith("\n"): diff --git a/src/zope/exceptions/tests/test_exceptionformatter.py b/src/zope/exceptions/tests/test_exceptionformatter.py index 5856583..9046950 100644 --- a/src/zope/exceptions/tests/test_exceptionformatter.py +++ b/src/zope/exceptions/tests/test_exceptionformatter.py @@ -14,7 +14,6 @@ """ExceptionFormatter tests. """ import unittest -import doctest import sys @@ -53,7 +52,6 @@ class TextExceptionFormatterTests(unittest.TestCase): self.assertEqual(fmt.getLimit(), 200) def test_getLimit_sys_has_limit(self): - import sys fmt = self._makeOne() with _Monkey(sys, tracebacklimit=15): self.assertEqual(fmt.getLimit(), 15) @@ -152,6 +150,17 @@ class TextExceptionFormatterTests(unittest.TestCase): self.assertEqual(fmt.formatTracebackInfo('XYZZY'), ' - __traceback_info__: XYZZY') + def test_formatTracebackInfo_unicode(self): + __traceback_info__ = u"Have a Snowman: \u2603" + fmt = self._makeOne() + + result = fmt.formatTracebackInfo(__traceback_info__) + expected = ' - __traceback_info__: Have a Snowman: ' + # utf-8 encoded on Python 2, unicode on Python 3 + expected += '\xe2\x98\x83' if bytes is str else u'\u2603' + self.assertIsInstance(result, str) + self.assertEqual(result, expected) + def test_formatLine_no_tb_no_f(self): fmt = self._makeOne() self.assertRaises(ValueError, fmt.formatLine, None, None) @@ -168,24 +177,26 @@ class TextExceptionFormatterTests(unittest.TestCase): tb.tb_frame = f = DummyFrame() lines = fmt.formatLine(tb).splitlines() self.assertEqual(len(lines), 1) - self.assertEqual(lines[0], - ' File "%s", line %d, in %s' - % (f.f_code.co_filename, - tb.tb_lineno, - f.f_code.co_name, - )) + self.assertEqual( + lines[0], + ' File "%s", line %d, in %s' + % (f.f_code.co_filename, + tb.tb_lineno, + f.f_code.co_name,) + ) def test_formatLine_w_f_bogus_linecache_w_filenames(self): fmt = self._makeOne(with_filenames=True) f = DummyFrame() lines = fmt.formatLine(f=f).splitlines() self.assertEqual(len(lines), 1) - self.assertEqual(lines[0], - ' File "%s", line %d, in %s' - % (f.f_code.co_filename, - f.f_lineno, - f.f_code.co_name, - )) + self.assertEqual( + lines[0], + ' File "%s", line %d, in %s' + % (f.f_code.co_filename, + f.f_lineno, + f.f_code.co_name,) + ) def test_formatLine_w_tb_bogus_linecache_wo_filenames(self): fmt = self._makeOne(with_filenames=False) @@ -194,25 +205,27 @@ class TextExceptionFormatterTests(unittest.TestCase): f.f_globals['__name__'] = 'dummy.filename' lines = fmt.formatLine(tb).splitlines() self.assertEqual(len(lines), 1) - self.assertEqual(lines[0], - ' Module dummy.filename, line %d, in %s' - % (tb.tb_lineno, - f.f_code.co_name, - )) + self.assertEqual( + lines[0], + ' Module dummy.filename, line %d, in %s' + % (tb.tb_lineno, + f.f_code.co_name,) + ) def test_formatLine_w_f_real_linecache_w_filenames(self): - import sys fmt = self._makeOne(with_filenames=True) - f = sys._getframe(); lineno = f.f_lineno + f = sys._getframe() + lineno = f.f_lineno result = fmt.formatLine(f=f) lines = result.splitlines() self.assertEqual(len(lines), 2) - self.assertEqual(lines[0], - ' File "%s", line %d, in %s' - % (f.f_code.co_filename, - lineno + 1, - f.f_code.co_name, - )) + self.assertEqual( + lines[0], + ' File "%s", line %d, in %s' + % (f.f_code.co_filename, + lineno + 1, + f.f_code.co_name,) + ) self.assertEqual(lines[1], ' result = fmt.formatLine(f=f)') @@ -257,7 +270,7 @@ class TextExceptionFormatterTests(unittest.TestCase): err = ValueError('testing') self.assertEqual(fmt.formatExceptionOnly(ValueError, err), ''.join( - traceback.format_exception_only(ValueError, err))) + traceback.format_exception_only(ValueError, err))) def test_formatLastLine(self): fmt = self._makeOne() @@ -272,7 +285,7 @@ class TextExceptionFormatterTests(unittest.TestCase): self.assertEqual(lines[0], 'Traceback (most recent call last):\n') self.assertEqual(lines[1], ''.join( - traceback.format_exception_only(ValueError, err))) + traceback.format_exception_only(ValueError, err))) def test_formatException_non_empty_tb_stack(self): import traceback @@ -287,7 +300,7 @@ class TextExceptionFormatterTests(unittest.TestCase): 'in dummy_function\n') self.assertEqual(lines[2], ''.join( - traceback.format_exception_only(ValueError, err))) + traceback.format_exception_only(ValueError, err))) def test_formatException_deep_tb_stack_with_limit(self): import traceback @@ -336,12 +349,12 @@ class TextExceptionFormatterTests(unittest.TestCase): 'in dummy_function\n') self.assertEqual(lines[4], ''.join( - traceback.format_exception_only(ValueError, err))) + traceback.format_exception_only(ValueError, err))) def test_extractStack_wo_frame(self): - import sys fmt = self._makeOne() - f = sys._getframe(); lineno = f.f_lineno + f = sys._getframe() + lineno = f.f_lineno lines = fmt.extractStack() # rather don't assert this here # self.assertEqual(len(lines), 10) @@ -351,9 +364,9 @@ class TextExceptionFormatterTests(unittest.TestCase): ' lines = fmt.extractStack()\n' % (lineno + 1)) def test_extractStack_wo_frame_w_limit(self): - import sys fmt = self._makeOne(limit=2) - f = sys._getframe(); lineno = f.f_lineno + f = sys._getframe() + lineno = f.f_lineno lines = fmt.extractStack() self.assertEqual(len(lines), 3) self.assertEqual(lines[-1], ' Module ' @@ -417,7 +430,7 @@ class TextExceptionFormatterTests(unittest.TestCase): def _makeTBs(self, count): prev = None - for i in range(count): + for _i in range(count): tb = DummyTB() tb.tb_lineno = 14 tb.tb_frame = DummyFrame() @@ -429,7 +442,7 @@ class TextExceptionFormatterTests(unittest.TestCase): def _makeFrames(self, count): prev = None - for i in range(count): + for _i in range(count): f = DummyFrame() f.f_lineno = 17 if prev is not None: @@ -467,7 +480,7 @@ class HTMLExceptionFormatterTests(unittest.TestCase): def test_escape_w_markup(self): fmt = self._makeOne() self.assertEqual(fmt.escape('<span>XXX & YYY<span>'), - '<span>XXX & YYY<span>') + '<span>XXX & YYY<span>') def test_getPrefix(self): fmt = self._makeOne() @@ -506,12 +519,13 @@ class HTMLExceptionFormatterTests(unittest.TestCase): tb = DummyTB() tb.tb_frame = f = DummyFrame() result = fmt.formatLine(tb) - self.assertEqual(result, - '<li> File "%s", line %d, in %s</li>' - % (f.f_code.co_filename, - tb.tb_lineno, - f.f_code.co_name, - )) + self.assertEqual( + result, + '<li> File "%s", line %d, in %s</li>' + % (f.f_code.co_filename, + tb.tb_lineno, + f.f_code.co_name,) + ) def test_formatLastLine(self): fmt = self._makeOne() @@ -521,7 +535,6 @@ class HTMLExceptionFormatterTests(unittest.TestCase): class Test_format_exception(unittest.TestCase): def _callFUT(self, as_html=False): - import sys from zope.exceptions.exceptionformatter import format_exception t, v, b = sys.exc_info() try: @@ -633,23 +646,33 @@ class Test_format_exception(unittest.TestCase): pass try: raise TypeError(C()) - except: + except TypeError: s = self._callFUT(True) - self.assertTrue(s.find('<') >= 0, s) - self.assertTrue(s.find('>') >= 0, s) + self.assertIn('<', s) + self.assertIn('>', s) def test_multiline_exception(self): try: exec('syntax error\n') - except Exception: + except SyntaxError: s = self._callFUT(False) lines = s.splitlines()[-3:] self.assertEqual(lines[0], ' syntax error') self.assertTrue(lines[1].endswith(' ^')) #PyPy has a shorter prefix self.assertEqual(lines[2], 'SyntaxError: invalid syntax') + def test_traceback_info_non_ascii(self): + __traceback_info__ = u"Have a Snowman: \u2603" + try: + raise TypeError() + except TypeError: + s = self._callFUT(True) + + self.assertIsInstance(s, str) + self.assertIn('Have a Snowman', s) + + def test_recursion_failure(self): - import sys from zope.exceptions.exceptionformatter import TextExceptionFormatter class FormatterException(Exception): @@ -676,16 +699,38 @@ class Test_format_exception(unittest.TestCase): self.assertTrue('FormatterException: Formatter failed' in s.splitlines()[-1]) + def test_format_exception_as_html(self): + # Test for format_exception (as_html=True) + from zope.exceptions.exceptionformatter import format_exception + from textwrap import dedent + import re + try: + exec('import') + except SyntaxError: + result = ''.join(format_exception(*sys.exc_info(), as_html=True)) + expected = dedent("""\ + <p>Traceback (most recent call last):</p> + <ul> + <li> Module zope.exceptions.tests.test_exceptionformatter, line ABC, in test_format_exception_as_html<br /> + exec('import')</li> + </ul><p> File "<string>", line 1<br /> + import<br /> + ^<br /> + SyntaxError: invalid syntax<br /> + </p>""") + # HTML formatter uses Windows line endings for some reason. + result = result.replace('\r\n', '\n') + result = re.sub(r'line \d\d\d,', 'line ABC,', result) + self.maxDiff = None + self.assertEqual(expected, result) + class Test_print_exception(unittest.TestCase): def _callFUT(self, as_html=False): - try: - from StringIO import StringIO - except ImportError: - from io import StringIO - buf = StringIO() - import sys + import io + buf = io.StringIO() if bytes is not str else io.BytesIO() + from zope.exceptions.exceptionformatter import print_exception t, v, b = sys.exc_info() try: @@ -718,7 +763,6 @@ class Test_print_exception(unittest.TestCase): class Test_extract_stack(unittest.TestCase): def _callFUT(self, as_html=False): - import sys from zope.exceptions.exceptionformatter import extract_stack f = sys.exc_info()[2].tb_frame try: @@ -752,7 +796,7 @@ class Test_extract_stack(unittest.TestCase): def test_traceback_info_html(self): try: - __traceback_info__ = "Adam & Eve" + __traceback_info__ = u"Adam & Eve" raise ExceptionForTesting except ExceptionForTesting: s = self._callFUT(True) @@ -761,7 +805,7 @@ class Test_extract_stack(unittest.TestCase): def test_traceback_supplement_text(self): try: __traceback_supplement__ = (TestingTracebackSupplement, - "You're one in a million") + u"You're one in a million") raise ExceptionForTesting except ExceptionForTesting: s = self._callFUT(False) @@ -803,7 +847,7 @@ class Test_extract_stack(unittest.TestCase): self.assertTrue(s.find('test_noinput') >= 0) -class ExceptionForTesting (Exception): +class ExceptionForTesting(Exception): pass @@ -860,30 +904,7 @@ class _Monkey(object): delattr(self.module, key) -def doctest_format_exception_as_html(): - """Test for format_exception (as_html=True) - - >>> from zope.exceptions.exceptionformatter import format_exception - >>> try: - ... exec('import 2 + 2') - ... except: - ... print(''.join(format_exception(*sys.exc_info(), as_html=True))) - <p>Traceback (most recent call last):</p> - <ul> - <li> Module zope.exceptions.tests.test_exceptionformatter, line 2, in <module><br /> - exec('import 2 + 2')</li> - </ul><p> File "<string>", line 1<br /> - import 2 + 2<br /> - ^<br /> - SyntaxError: invalid syntax<br /> - </p> - - """ def test_suite(): - return unittest.TestSuite([ - unittest.defaultTestLoader.loadTestsFromName(__name__), - doctest.DocTestSuite( - optionflags=doctest.NORMALIZE_WHITESPACE), - ]) + return unittest.defaultTestLoader.loadTestsFromName(__name__) diff --git a/src/zope/exceptions/tests/test_log.py b/src/zope/exceptions/tests/test_log.py index d63385a..9e01eea 100644 --- a/src/zope/exceptions/tests/test_log.py +++ b/src/zope/exceptions/tests/test_log.py @@ -28,7 +28,6 @@ class FormatterTests(unittest.TestCase): def test_simple_exception(self): import traceback tb = DummyTB() - tb.tb_frame = DummyFrame() exc = ValueError('testing') fmt = self._makeOne() result = fmt.formatException((ValueError, exc, tb)) @@ -38,14 +37,40 @@ class FormatterTests(unittest.TestCase): self.assertEqual(lines[1], ' File "dummy/filename.py", line 14, ' 'in dummy_function') self.assertEqual(lines[2], - traceback.format_exception_only( - ValueError, exc)[0][:-1]) #trailing \n + traceback.format_exception_only( + ValueError, exc)[0][:-1]) #trailing \n + + def test_unicode_traceback_info(self): + import traceback + __traceback_info__ = u"Have a Snowman: \u2603" + tb = DummyTB() + tb.tb_frame.f_locals['__traceback_info__'] = __traceback_info__ + exc = ValueError('testing') + fmt = self._makeOne() + result = fmt.formatException((ValueError, exc, tb)) + self.assertIsInstance(result, str) + lines = result.splitlines() + self.assertEqual(len(lines), 4) + self.assertEqual(lines[0], 'Traceback (most recent call last):') + self.assertEqual(lines[1], ' File "dummy/filename.py", line 14, ' + 'in dummy_function') + expected = ' - __traceback_info__: Have a Snowman: ' + # utf-8 encoded on Python 2, unicode on Python 3 + expected += '\xe2\x98\x83' if bytes is str else u'\u2603' + + self.assertEqual(lines[2], + expected) + self.assertEqual(lines[3], + traceback.format_exception_only( + ValueError, exc)[0][:-1]) #trailing \n class DummyTB(object): tb_lineno = 14 tb_next = None + def __init__(self): + self.tb_frame = DummyFrame() class DummyFrame(object): f_lineno = 137 |