Merge pull request #1486 from roolebo/fix-issue-138

Fix issue #138 - support chained exceptions
This commit is contained in:
Ronny Pfannschmidt 2016-04-03 19:21:57 +02:00
commit 0f7aeafe7c
5 changed files with 155 additions and 12 deletions

View File

@ -75,6 +75,7 @@ Piotr Banaszkiewicz
Punyashloka Biswal
Ralf Schmitt
Raphael Pierzina
Roman Bolshakov
Ronny Pfannschmidt
Ross Lawley
Ryan Wooden

View File

@ -98,11 +98,13 @@
* Fix (`#649`_): parametrized test nodes cannot be specified to run on the command line.
* Fix (`#138`_): better reporting for python 3.3+ chained exceptions
.. _#1437: https://github.com/pytest-dev/pytest/issues/1437
.. _#469: https://github.com/pytest-dev/pytest/issues/469
.. _#1431: https://github.com/pytest-dev/pytest/pull/1431
.. _#649: https://github.com/pytest-dev/pytest/issues/649
.. _#138: https://github.com/pytest-dev/pytest/issues/138
.. _@asottile: https://github.com/asottile

View File

@ -608,12 +608,36 @@ class FormattedExcinfo(object):
break
return ReprTraceback(entries, extraline, style=self.style)
def repr_excinfo(self, excinfo):
if sys.version_info[0] < 3:
reprtraceback = self.repr_traceback(excinfo)
reprcrash = excinfo._getreprcrash()
return ReprExceptionInfo(reprtraceback, reprcrash)
class TerminalRepr:
return ReprExceptionInfo(reprtraceback, reprcrash)
else:
repr_chain = []
e = excinfo.value
descr = None
while e is not None:
reprtraceback = self.repr_traceback(excinfo)
reprcrash = excinfo._getreprcrash()
repr_chain += [(reprtraceback, reprcrash, descr)]
if e.__cause__ is not None:
e = e.__cause__
excinfo = ExceptionInfo((type(e), e, e.__traceback__))
descr = 'The above exception was the direct cause of the following exception:'
elif e.__context__ is not None:
e = e.__context__
excinfo = ExceptionInfo((type(e), e, e.__traceback__))
descr = 'During handling of the above exception, another exception occurred:'
else:
e = None
repr_chain.reverse()
return ExceptionChainRepr(repr_chain)
class TerminalRepr(object):
def __str__(self):
s = self.__unicode__()
if sys.version_info[0] < 3:
@ -632,21 +656,47 @@ class TerminalRepr:
return "<%s instance at %0x>" %(self.__class__, id(self))
class ReprExceptionInfo(TerminalRepr):
def __init__(self, reprtraceback, reprcrash):
self.reprtraceback = reprtraceback
self.reprcrash = reprcrash
class ExceptionRepr(TerminalRepr):
def __init__(self):
self.sections = []
def addsection(self, name, content, sep="-"):
self.sections.append((name, content, sep))
def toterminal(self, tw):
self.reprtraceback.toterminal(tw)
for name, content, sep in self.sections:
tw.sep(sep, name)
tw.line(content)
class ExceptionChainRepr(ExceptionRepr):
def __init__(self, chain):
super(ExceptionChainRepr, self).__init__()
self.chain = chain
# reprcrash and reprtraceback of the outermost (the newest) exception
# in the chain
self.reprtraceback = chain[-1][0]
self.reprcrash = chain[-1][1]
def toterminal(self, tw):
for element in self.chain:
element[0].toterminal(tw)
if element[2] is not None:
tw.line("")
tw.line(element[2], yellow=True)
super(ExceptionChainRepr, self).toterminal(tw)
class ReprExceptionInfo(ExceptionRepr):
def __init__(self, reprtraceback, reprcrash):
super(ReprExceptionInfo, self).__init__()
self.reprtraceback = reprtraceback
self.reprcrash = reprcrash
def toterminal(self, tw):
self.reprtraceback.toterminal(tw)
super(ReprExceptionInfo, self).toterminal(tw)
class ReprTraceback(TerminalRepr):
entrysep = "_ "

View File

@ -494,9 +494,13 @@ def importorskip(modname, minversion=None):
"""
__tracebackhide__ = True
compile(modname, '', 'eval') # to catch syntaxerrors
should_skip = False
try:
__import__(modname)
except ImportError:
# Do not raise chained exception here(#1485)
should_skip = True
if should_skip:
skip("could not import %r" %(modname,))
mod = sys.modules[modname]
if minversion is None:

View File

@ -3,7 +3,8 @@
import _pytest
import py
import pytest
from _pytest._code.code import FormattedExcinfo, ReprExceptionInfo
from _pytest._code.code import (FormattedExcinfo, ReprExceptionInfo,
ExceptionChainRepr)
queue = py.builtin._tryimport('queue', 'Queue')
@ -404,6 +405,8 @@ class TestFormattedExcinfo:
excinfo = _pytest._code.ExceptionInfo()
repr = pr.repr_excinfo(excinfo)
assert repr.reprtraceback.reprentries[1].lines[0] == "> ???"
if py.std.sys.version_info[0] >= 3:
assert repr.chain[0][0].reprentries[1].lines[0] == "> ???"
def test_repr_many_line_source_not_existing(self):
pr = FormattedExcinfo()
@ -417,6 +420,8 @@ raise ValueError()
excinfo = _pytest._code.ExceptionInfo()
repr = pr.repr_excinfo(excinfo)
assert repr.reprtraceback.reprentries[1].lines[0] == "> ???"
if py.std.sys.version_info[0] >= 3:
assert repr.chain[0][0].reprentries[1].lines[0] == "> ???"
def test_repr_source_failing_fullsource(self):
pr = FormattedExcinfo()
@ -449,6 +454,7 @@ raise ValueError()
class FakeExcinfo(_pytest._code.ExceptionInfo):
typename = "Foo"
value = Exception()
def __init__(self):
pass
@ -466,10 +472,15 @@ raise ValueError()
fail = IOError() # noqa
repr = pr.repr_excinfo(excinfo)
assert repr.reprtraceback.reprentries[0].lines[0] == "> ???"
if py.std.sys.version_info[0] >= 3:
assert repr.chain[0][0].reprentries[0].lines[0] == "> ???"
fail = py.error.ENOENT # noqa
repr = pr.repr_excinfo(excinfo)
assert repr.reprtraceback.reprentries[0].lines[0] == "> ???"
if py.std.sys.version_info[0] >= 3:
assert repr.chain[0][0].reprentries[0].lines[0] == "> ???"
def test_repr_local(self):
@ -656,6 +667,9 @@ raise ValueError()
repr = p.repr_excinfo(excinfo)
assert repr.reprtraceback
assert len(repr.reprtraceback.reprentries) == len(reprtb.reprentries)
if py.std.sys.version_info[0] >= 3:
assert repr.chain[0][0]
assert len(repr.chain[0][0].reprentries) == len(reprtb.reprentries)
assert repr.reprcrash.path.endswith("mod.py")
assert repr.reprcrash.message == "ValueError: 0"
@ -746,8 +760,13 @@ raise ValueError()
for style in ("short", "long", "no"):
for showlocals in (True, False):
repr = excinfo.getrepr(style=style, showlocals=showlocals)
if py.std.sys.version_info[0] < 3:
assert isinstance(repr, ReprExceptionInfo)
assert repr.reprtraceback.style == style
if py.std.sys.version_info[0] >= 3:
assert isinstance(repr, ExceptionChainRepr)
for repr in repr.chain:
assert repr[0].style == style
def test_reprexcinfo_unicode(self):
from _pytest._code.code import TerminalRepr
@ -928,3 +947,70 @@ raise ValueError()
assert tw.lines[14] == "E ValueError"
assert tw.lines[15] == ""
assert tw.lines[16].endswith("mod.py:9: ValueError")
@pytest.mark.skipif("sys.version_info[0] < 3")
def test_exc_chain_repr(self, importasmod):
mod = importasmod("""
class Err(Exception):
pass
def f():
try:
g()
except Exception as e:
raise Err() from e
finally:
h()
def g():
raise ValueError()
def h():
raise AttributeError()
""")
excinfo = pytest.raises(AttributeError, mod.f)
r = excinfo.getrepr(style="long")
tw = TWMock()
r.toterminal(tw)
for line in tw.lines: print (line)
assert tw.lines[0] == ""
assert tw.lines[1] == " def f():"
assert tw.lines[2] == " try:"
assert tw.lines[3] == "> g()"
assert tw.lines[4] == ""
assert tw.lines[5].endswith("mod.py:6: ")
assert tw.lines[6] == ("_ ", None)
assert tw.lines[7] == ""
assert tw.lines[8] == " def g():"
assert tw.lines[9] == "> raise ValueError()"
assert tw.lines[10] == "E ValueError"
assert tw.lines[11] == ""
assert tw.lines[12].endswith("mod.py:12: ValueError")
assert tw.lines[13] == ""
assert tw.lines[14] == "The above exception was the direct cause of the following exception:"
assert tw.lines[15] == ""
assert tw.lines[16] == " def f():"
assert tw.lines[17] == " try:"
assert tw.lines[18] == " g()"
assert tw.lines[19] == " except Exception as e:"
assert tw.lines[20] == "> raise Err() from e"
assert tw.lines[21] == "E test_exc_chain_repr0.mod.Err"
assert tw.lines[22] == ""
assert tw.lines[23].endswith("mod.py:8: Err")
assert tw.lines[24] == ""
assert tw.lines[25] == "During handling of the above exception, another exception occurred:"
assert tw.lines[26] == ""
assert tw.lines[27] == " def f():"
assert tw.lines[28] == " try:"
assert tw.lines[29] == " g()"
assert tw.lines[30] == " except Exception as e:"
assert tw.lines[31] == " raise Err() from e"
assert tw.lines[32] == " finally:"
assert tw.lines[33] == "> h()"
assert tw.lines[34] == ""
assert tw.lines[35].endswith("mod.py:10: ")
assert tw.lines[36] == ('_ ', None)
assert tw.lines[37] == ""
assert tw.lines[38] == " def h():"
assert tw.lines[39] == "> raise AttributeError()"
assert tw.lines[40] == "E AttributeError"
assert tw.lines[41] == ""
assert tw.lines[42].endswith("mod.py:15: AttributeError")