summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJason Madden <jamadden@gmail.com>2017-09-11 11:28:00 -0500
committerJason Madden <jamadden@gmail.com>2017-09-11 11:28:00 -0500
commitcc9f097936226845a89d879d49f38894dd032d03 (patch)
tree3643ca6bc8d5d22d2c8927e0cb3686c5698075cf
parentd97f1b37d541554ad85e15dd824a84d3e3883f4c (diff)
downloadzope-exceptions-issue1.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--.coveragerc7
-rw-r--r--.travis.yml6
-rw-r--r--CHANGES.rst8
-rw-r--r--setup.py4
-rw-r--r--src/zope/exceptions/exceptionformatter.py42
-rw-r--r--src/zope/exceptions/log.py8
-rw-r--r--src/zope/exceptions/tests/test_exceptionformatter.py189
-rw-r--r--src/zope/exceptions/tests/test_log.py31
-rw-r--r--tox.ini15
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)
diff --git a/setup.py b/setup.py
index bc2e2d0..bdfdafa 100644
--- a/setup.py
+++ b/setup.py
@@ -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>'),
- '&lt;span&gt;XXX &amp; YYY&lt;span&gt;')
+ '&lt;span&gt;XXX &amp; YYY&lt;span&gt;')
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('&lt;') >= 0, s)
- self.assertTrue(s.find('&gt;') >= 0, s)
+ self.assertIn('&lt;', s)
+ self.assertIn('&gt;', 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 "&lt;string&gt;", 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 &lt;module&gt;<br />
- exec('import 2 + 2')</li>
- </ul><p> File "&lt;string&gt;", 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
diff --git a/tox.ini b/tox.ini
index ce87117..c9423d8 100644
--- a/tox.ini
+++ b/tox.ini
@@ -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]