Merge pull request #1486 from roolebo/fix-issue-138
Fix issue #138 - support chained exceptions
This commit is contained in:
commit
0f7aeafe7c
1
AUTHORS
1
AUTHORS
|
@ -75,6 +75,7 @@ Piotr Banaszkiewicz
|
|||
Punyashloka Biswal
|
||||
Ralf Schmitt
|
||||
Raphael Pierzina
|
||||
Roman Bolshakov
|
||||
Ronny Pfannschmidt
|
||||
Ross Lawley
|
||||
Ryan Wooden
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -608,12 +608,36 @@ class FormattedExcinfo(object):
|
|||
break
|
||||
return ReprTraceback(entries, extraline, style=self.style)
|
||||
|
||||
def repr_excinfo(self, excinfo):
|
||||
reprtraceback = self.repr_traceback(excinfo)
|
||||
reprcrash = excinfo._getreprcrash()
|
||||
return ReprExceptionInfo(reprtraceback, reprcrash)
|
||||
|
||||
class TerminalRepr:
|
||||
def repr_excinfo(self, excinfo):
|
||||
if sys.version_info[0] < 3:
|
||||
reprtraceback = self.repr_traceback(excinfo)
|
||||
reprcrash = excinfo._getreprcrash()
|
||||
|
||||
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 = "_ "
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
assert isinstance(repr, ReprExceptionInfo)
|
||||
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")
|
||||
|
|
Loading…
Reference in New Issue