Skip to content

Commit

Permalink
Merge pull request #1486 from roolebo/fix-issue-138
Browse files Browse the repository at this point in the history
Fix issue #138 - support chained exceptions
  • Loading branch information
RonnyPfannschmidt committed Apr 3, 2016
2 parents e3bc6fa + 89df701 commit 0f7aeaf
Show file tree
Hide file tree
Showing 5 changed files with 154 additions and 11 deletions.
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ Piotr Banaszkiewicz
Punyashloka Biswal
Ralf Schmitt
Raphael Pierzina
Roman Bolshakov
Ronny Pfannschmidt
Ross Lawley
Ryan Wooden
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
68 changes: 59 additions & 9 deletions _pytest/_code/code.py
Original file line number Diff line number Diff line change
Expand Up @@ -608,12 +608,36 @@ def repr_traceback(self, excinfo):
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)
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:
class TerminalRepr(object):
def __str__(self):
s = self.__unicode__()
if sys.version_info[0] < 3:
Expand All @@ -632,21 +656,47 @@ def __repr__(self):
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 = "_ "

Expand Down
4 changes: 4 additions & 0 deletions _pytest/runner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
90 changes: 88 additions & 2 deletions testing/code/test_excinfo.py
Original file line number Diff line number Diff line change
Expand Up @@ -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')

Expand Down Expand Up @@ -404,6 +405,8 @@ def test_repr_source_not_existing(self):
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()
Expand All @@ -417,6 +420,8 @@ def test_repr_many_line_source_not_existing(self):
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()
Expand Down Expand Up @@ -449,6 +454,7 @@ class Traceback(_pytest._code.Traceback):

class FakeExcinfo(_pytest._code.ExceptionInfo):
typename = "Foo"
value = Exception()
def __init__(self):
pass

Expand All @@ -466,10 +472,15 @@ class FakeRawTB(object):
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):
Expand Down Expand Up @@ -656,6 +667,9 @@ def entry():
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"

Expand Down Expand Up @@ -746,8 +760,13 @@ def entry():
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
Expand Down Expand Up @@ -928,3 +947,70 @@ def i():
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")

0 comments on commit 0f7aeaf

Please sign in to comment.