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
|
Punyashloka Biswal
|
||||||
Ralf Schmitt
|
Ralf Schmitt
|
||||||
Raphael Pierzina
|
Raphael Pierzina
|
||||||
|
Roman Bolshakov
|
||||||
Ronny Pfannschmidt
|
Ronny Pfannschmidt
|
||||||
Ross Lawley
|
Ross Lawley
|
||||||
Ryan Wooden
|
Ryan Wooden
|
||||||
|
|
|
@ -98,11 +98,13 @@
|
||||||
|
|
||||||
* Fix (`#649`_): parametrized test nodes cannot be specified to run on the command line.
|
* 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
|
.. _#1437: https://github.com/pytest-dev/pytest/issues/1437
|
||||||
.. _#469: https://github.com/pytest-dev/pytest/issues/469
|
.. _#469: https://github.com/pytest-dev/pytest/issues/469
|
||||||
.. _#1431: https://github.com/pytest-dev/pytest/pull/1431
|
.. _#1431: https://github.com/pytest-dev/pytest/pull/1431
|
||||||
.. _#649: https://github.com/pytest-dev/pytest/issues/649
|
.. _#649: https://github.com/pytest-dev/pytest/issues/649
|
||||||
|
.. _#138: https://github.com/pytest-dev/pytest/issues/138
|
||||||
|
|
||||||
.. _@asottile: https://github.com/asottile
|
.. _@asottile: https://github.com/asottile
|
||||||
|
|
||||||
|
|
|
@ -608,12 +608,36 @@ class FormattedExcinfo(object):
|
||||||
break
|
break
|
||||||
return ReprTraceback(entries, extraline, style=self.style)
|
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):
|
def __str__(self):
|
||||||
s = self.__unicode__()
|
s = self.__unicode__()
|
||||||
if sys.version_info[0] < 3:
|
if sys.version_info[0] < 3:
|
||||||
|
@ -632,21 +656,47 @@ class TerminalRepr:
|
||||||
return "<%s instance at %0x>" %(self.__class__, id(self))
|
return "<%s instance at %0x>" %(self.__class__, id(self))
|
||||||
|
|
||||||
|
|
||||||
class ReprExceptionInfo(TerminalRepr):
|
class ExceptionRepr(TerminalRepr):
|
||||||
def __init__(self, reprtraceback, reprcrash):
|
def __init__(self):
|
||||||
self.reprtraceback = reprtraceback
|
|
||||||
self.reprcrash = reprcrash
|
|
||||||
self.sections = []
|
self.sections = []
|
||||||
|
|
||||||
def addsection(self, name, content, sep="-"):
|
def addsection(self, name, content, sep="-"):
|
||||||
self.sections.append((name, content, sep))
|
self.sections.append((name, content, sep))
|
||||||
|
|
||||||
def toterminal(self, tw):
|
def toterminal(self, tw):
|
||||||
self.reprtraceback.toterminal(tw)
|
|
||||||
for name, content, sep in self.sections:
|
for name, content, sep in self.sections:
|
||||||
tw.sep(sep, name)
|
tw.sep(sep, name)
|
||||||
tw.line(content)
|
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):
|
class ReprTraceback(TerminalRepr):
|
||||||
entrysep = "_ "
|
entrysep = "_ "
|
||||||
|
|
||||||
|
|
|
@ -494,9 +494,13 @@ def importorskip(modname, minversion=None):
|
||||||
"""
|
"""
|
||||||
__tracebackhide__ = True
|
__tracebackhide__ = True
|
||||||
compile(modname, '', 'eval') # to catch syntaxerrors
|
compile(modname, '', 'eval') # to catch syntaxerrors
|
||||||
|
should_skip = False
|
||||||
try:
|
try:
|
||||||
__import__(modname)
|
__import__(modname)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
# Do not raise chained exception here(#1485)
|
||||||
|
should_skip = True
|
||||||
|
if should_skip:
|
||||||
skip("could not import %r" %(modname,))
|
skip("could not import %r" %(modname,))
|
||||||
mod = sys.modules[modname]
|
mod = sys.modules[modname]
|
||||||
if minversion is None:
|
if minversion is None:
|
||||||
|
|
|
@ -3,7 +3,8 @@
|
||||||
import _pytest
|
import _pytest
|
||||||
import py
|
import py
|
||||||
import pytest
|
import pytest
|
||||||
from _pytest._code.code import FormattedExcinfo, ReprExceptionInfo
|
from _pytest._code.code import (FormattedExcinfo, ReprExceptionInfo,
|
||||||
|
ExceptionChainRepr)
|
||||||
|
|
||||||
queue = py.builtin._tryimport('queue', 'Queue')
|
queue = py.builtin._tryimport('queue', 'Queue')
|
||||||
|
|
||||||
|
@ -404,6 +405,8 @@ class TestFormattedExcinfo:
|
||||||
excinfo = _pytest._code.ExceptionInfo()
|
excinfo = _pytest._code.ExceptionInfo()
|
||||||
repr = pr.repr_excinfo(excinfo)
|
repr = pr.repr_excinfo(excinfo)
|
||||||
assert repr.reprtraceback.reprentries[1].lines[0] == "> ???"
|
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):
|
def test_repr_many_line_source_not_existing(self):
|
||||||
pr = FormattedExcinfo()
|
pr = FormattedExcinfo()
|
||||||
|
@ -417,6 +420,8 @@ raise ValueError()
|
||||||
excinfo = _pytest._code.ExceptionInfo()
|
excinfo = _pytest._code.ExceptionInfo()
|
||||||
repr = pr.repr_excinfo(excinfo)
|
repr = pr.repr_excinfo(excinfo)
|
||||||
assert repr.reprtraceback.reprentries[1].lines[0] == "> ???"
|
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):
|
def test_repr_source_failing_fullsource(self):
|
||||||
pr = FormattedExcinfo()
|
pr = FormattedExcinfo()
|
||||||
|
@ -449,6 +454,7 @@ raise ValueError()
|
||||||
|
|
||||||
class FakeExcinfo(_pytest._code.ExceptionInfo):
|
class FakeExcinfo(_pytest._code.ExceptionInfo):
|
||||||
typename = "Foo"
|
typename = "Foo"
|
||||||
|
value = Exception()
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@ -466,10 +472,15 @@ raise ValueError()
|
||||||
fail = IOError() # noqa
|
fail = IOError() # noqa
|
||||||
repr = pr.repr_excinfo(excinfo)
|
repr = pr.repr_excinfo(excinfo)
|
||||||
assert repr.reprtraceback.reprentries[0].lines[0] == "> ???"
|
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
|
fail = py.error.ENOENT # noqa
|
||||||
repr = pr.repr_excinfo(excinfo)
|
repr = pr.repr_excinfo(excinfo)
|
||||||
assert repr.reprtraceback.reprentries[0].lines[0] == "> ???"
|
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):
|
def test_repr_local(self):
|
||||||
|
@ -656,6 +667,9 @@ raise ValueError()
|
||||||
repr = p.repr_excinfo(excinfo)
|
repr = p.repr_excinfo(excinfo)
|
||||||
assert repr.reprtraceback
|
assert repr.reprtraceback
|
||||||
assert len(repr.reprtraceback.reprentries) == len(reprtb.reprentries)
|
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.path.endswith("mod.py")
|
||||||
assert repr.reprcrash.message == "ValueError: 0"
|
assert repr.reprcrash.message == "ValueError: 0"
|
||||||
|
|
||||||
|
@ -746,8 +760,13 @@ raise ValueError()
|
||||||
for style in ("short", "long", "no"):
|
for style in ("short", "long", "no"):
|
||||||
for showlocals in (True, False):
|
for showlocals in (True, False):
|
||||||
repr = excinfo.getrepr(style=style, showlocals=showlocals)
|
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
|
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):
|
def test_reprexcinfo_unicode(self):
|
||||||
from _pytest._code.code import TerminalRepr
|
from _pytest._code.code import TerminalRepr
|
||||||
|
@ -928,3 +947,70 @@ raise ValueError()
|
||||||
assert tw.lines[14] == "E ValueError"
|
assert tw.lines[14] == "E ValueError"
|
||||||
assert tw.lines[15] == ""
|
assert tw.lines[15] == ""
|
||||||
assert tw.lines[16].endswith("mod.py:9: ValueError")
|
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