diff options
author | Jason Madden <jamadden@gmail.com> | 2017-09-11 11:28:00 -0500 |
---|---|---|
committer | Jason Madden <jamadden@gmail.com> | 2017-09-11 11:28:00 -0500 |
commit | cc9f097936226845a89d879d49f38894dd032d03 (patch) | |
tree | 3643ca6bc8d5d22d2c8927e0cb3686c5698075cf | |
parent | d97f1b37d541554ad85e15dd824a84d3e3883f4c (diff) | |
download | zope-exceptions-cc9f097936226845a89d879d49f38894dd032d03.tar.gz |
Fix non-ASCII supplement info under Python 2 and drop Py3.3.issue1
Fixes #1.
Fix the coverage environment using zope.testrunner (nose no longer
works, that same namespace path issue).
Also run the doctests on all supported versions. This requires
dropping Python 3.3 because sphinx needs 3.4+.
-rw-r--r-- | .coveragerc | 7 | ||||
-rw-r--r-- | .travis.yml | 6 | ||||
-rw-r--r-- | CHANGES.rst | 8 | ||||
-rw-r--r-- | setup.py | 4 | ||||
-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 | ||||
-rw-r--r-- | tox.ini | 15 |
9 files changed, 191 insertions, 119 deletions
diff --git a/.coveragerc b/.coveragerc index af40312..3b86679 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1,6 +1,11 @@ [run] -source = src +source = zope.exceptions [report] +precision = 2 exclude_lines = pragma: no cover + if __name__ == '__main__': + raise NotImplementedError + self.fail + raise AssertionError diff --git a/.travis.yml b/.travis.yml index 19a4c83..830860d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,17 +2,17 @@ language: python sudo: false python: - 2.7 - - 3.3 - 3.4 - 3.5 - 3.6 - - pypy-5.4.1 + - pypy install: - pip install -U pip setuptools - pip install -U coverage coveralls - - pip install -U -e .[test] + - pip install -U -e .[test,docs] script: - coverage run -m zope.testrunner --test-path=src + - coverage run -a -m sphinx -b doctest -d docs/_build/doctrees docs docs/_build/doctest notifications: email: false cache: pip diff --git a/CHANGES.rst b/CHANGES.rst index d919f5a..5d60d0f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -7,6 +7,12 @@ - Add support for Python 3.6. +- Drop support for Python 3.3. + +- Fix handling of unicode supplemental traceback information on + Python 2. Now such values are always encoded to UTF-8; previously + the results were undefined and depended on system encodings and the + values themselves. See `issue 1 <https://github.com/zopefoundation/zope.exceptions/issues/1>`_. 4.1.0 (2017-04-12) ================== @@ -76,7 +82,7 @@ 4.0.1 (2012-08-20) ================== -- Fixed optional dependency code for `zope.security` to work under Python 3.3. +- Fixed optional dependency code for `'zope.security`` to work under Python 3.3. 4.0.0.1 (2012-05-16) @@ -61,7 +61,6 @@ setup(name='zope.exceptions', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.3', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', @@ -72,7 +71,7 @@ setup(name='zope.exceptions', 'Topic :: Internet :: WWW/HTTP', 'Framework :: Zope3', ], - url='http://cheeseshop.python.org/pypi/zope.exceptions', + url='https://github.com/zopefoundation/zope.exceptions', license='ZPL 2.1', packages=find_packages('src'), package_dir={'': 'src'}, @@ -89,7 +88,6 @@ setup(name='zope.exceptions', zip_safe=False, extras_require={ 'docs': ['Sphinx', 'repoze.sphinx.autointerface'], - 'testing': ['nose', 'coverage'], 'test': tests_require, }, ) 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 @@ -1,22 +1,25 @@ [tox] envlist = - py27,py33,py34,py35,py36,pypy,pypy3,coverage,docs + py27,py34,py35,py36,pypy,pypy3,coverage,docs [testenv] commands = zope-testrunner --test-path=src [] + sphinx-build -b doctest -d {envdir}/.cache/doctrees docs {envdir}/.cache/doctest deps = - .[test] + .[test,docs] [testenv:coverage] usedevelop = true basepython = python2.7 commands = - nosetests --with-xunit --with-xcoverage + coverage run -m zope.testrunner --test-path=src [] + coverage run -a -m sphinx -b doctest -d {envdir}/.cache/doctrees docs {envdir}/.cache/doctest + coverage report --fail-under=100 deps = - .[test,testing] - nosexcover + {[testenv]deps} + coverage [testenv:docs] basepython = @@ -24,5 +27,3 @@ basepython = commands = sphinx-build -b html -d docs/_build/doctrees docs docs/_build/html sphinx-build -b doctest -d docs/_build/doctrees docs docs/_build/doctest -deps = - .[test,docs] |