Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix issue #138 (targeted to features branch) #1486

Merged
merged 4 commits into from
Apr 3, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,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 @@ -37,11 +37,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 @@ -594,12 +594,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 @@ -618,21 +642,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 @@ -385,6 +386,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 @@ -398,6 +401,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 @@ -430,6 +435,7 @@ class Traceback(_pytest._code.Traceback):

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

Expand All @@ -447,10 +453,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 @@ -637,6 +648,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 @@ -727,8 +741,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 @@ -909,3 +928,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")