Skip to content

Commit fbc5ba0

Browse files
committed
Fix issue #138
1 parent 3884398 commit fbc5ba0

File tree

3 files changed

+150
-11
lines changed

3 files changed

+150
-11
lines changed

_pytest/_code/code.py

Lines changed: 59 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -594,12 +594,36 @@ def repr_traceback(self, excinfo):
594594
break
595595
return ReprTraceback(entries, extraline, style=self.style)
596596

597+
597598
def repr_excinfo(self, excinfo):
598-
reprtraceback = self.repr_traceback(excinfo)
599-
reprcrash = excinfo._getreprcrash()
600-
return ReprExceptionInfo(reprtraceback, reprcrash)
599+
if sys.version_info[0] < 3:
600+
reprtraceback = self.repr_traceback(excinfo)
601+
reprcrash = excinfo._getreprcrash()
602+
603+
return ReprExceptionInfo(reprtraceback, reprcrash)
604+
else:
605+
repr_chain = []
606+
e = excinfo.value
607+
descr = None
608+
while e is not None:
609+
reprtraceback = self.repr_traceback(excinfo)
610+
reprcrash = excinfo._getreprcrash()
611+
repr_chain += [(reprtraceback, reprcrash, descr)]
612+
if e.__cause__ is not None:
613+
e = e.__cause__
614+
excinfo = ExceptionInfo((type(e), e, e.__traceback__))
615+
descr = 'The above exception was the direct cause of the following exception:'
616+
elif e.__context__ is not None:
617+
e = e.__context__
618+
excinfo = ExceptionInfo((type(e), e, e.__traceback__))
619+
descr = 'During handling of the above exception, another exception occurred:'
620+
else:
621+
e = None
622+
repr_chain.reverse()
623+
return ExceptionChainRepr(repr_chain)
624+
601625

602-
class TerminalRepr:
626+
class TerminalRepr(object):
603627
def __str__(self):
604628
s = self.__unicode__()
605629
if sys.version_info[0] < 3:
@@ -618,21 +642,47 @@ def __repr__(self):
618642
return "<%s instance at %0x>" %(self.__class__, id(self))
619643

620644

621-
class ReprExceptionInfo(TerminalRepr):
622-
def __init__(self, reprtraceback, reprcrash):
623-
self.reprtraceback = reprtraceback
624-
self.reprcrash = reprcrash
645+
class ExceptionRepr(TerminalRepr):
646+
def __init__(self):
625647
self.sections = []
626648

627649
def addsection(self, name, content, sep="-"):
628650
self.sections.append((name, content, sep))
629651

630652
def toterminal(self, tw):
631-
self.reprtraceback.toterminal(tw)
632653
for name, content, sep in self.sections:
633654
tw.sep(sep, name)
634655
tw.line(content)
635656

657+
658+
class ExceptionChainRepr(ExceptionRepr):
659+
def __init__(self, chain):
660+
super(ExceptionChainRepr, self).__init__()
661+
self.chain = chain
662+
# reprcrash and reprtraceback of the outermost (the newest) exception
663+
# in the chain
664+
self.reprtraceback = chain[-1][0]
665+
self.reprcrash = chain[-1][1]
666+
667+
def toterminal(self, tw):
668+
for element in self.chain:
669+
element[0].toterminal(tw)
670+
if element[2] is not None:
671+
tw.line("")
672+
tw.line(element[2], yellow=True)
673+
super(ExceptionChainRepr, self).toterminal(tw)
674+
675+
676+
class ReprExceptionInfo(ExceptionRepr):
677+
def __init__(self, reprtraceback, reprcrash):
678+
super(ReprExceptionInfo, self).__init__()
679+
self.reprtraceback = reprtraceback
680+
self.reprcrash = reprcrash
681+
682+
def toterminal(self, tw):
683+
self.reprtraceback.toterminal(tw)
684+
super(ReprExceptionInfo, self).toterminal(tw)
685+
636686
class ReprTraceback(TerminalRepr):
637687
entrysep = "_ "
638688

_pytest/runner.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,9 +491,12 @@ def importorskip(modname, minversion=None):
491491
"""
492492
__tracebackhide__ = True
493493
compile(modname, '', 'eval') # to catch syntaxerrors
494+
should_skip = False
494495
try:
495496
__import__(modname)
496497
except ImportError:
498+
should_skip = True
499+
if should_skip:
497500
skip("could not import %r" %(modname,))
498501
mod = sys.modules[modname]
499502
if minversion is None:

testing/code/test_excinfo.py

Lines changed: 88 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
import _pytest
44
import py
55
import pytest
6-
from _pytest._code.code import FormattedExcinfo, ReprExceptionInfo
6+
from _pytest._code.code import (FormattedExcinfo, ReprExceptionInfo,
7+
ExceptionChainRepr)
78

89
queue = py.builtin._tryimport('queue', 'Queue')
910

@@ -385,6 +386,8 @@ def test_repr_source_not_existing(self):
385386
excinfo = _pytest._code.ExceptionInfo()
386387
repr = pr.repr_excinfo(excinfo)
387388
assert repr.reprtraceback.reprentries[1].lines[0] == "> ???"
389+
if py.std.sys.version_info[0] >= 3:
390+
assert repr.chain[0][0].reprentries[1].lines[0] == "> ???"
388391

389392
def test_repr_many_line_source_not_existing(self):
390393
pr = FormattedExcinfo()
@@ -398,6 +401,8 @@ def test_repr_many_line_source_not_existing(self):
398401
excinfo = _pytest._code.ExceptionInfo()
399402
repr = pr.repr_excinfo(excinfo)
400403
assert repr.reprtraceback.reprentries[1].lines[0] == "> ???"
404+
if py.std.sys.version_info[0] >= 3:
405+
assert repr.chain[0][0].reprentries[1].lines[0] == "> ???"
401406

402407
def test_repr_source_failing_fullsource(self):
403408
pr = FormattedExcinfo()
@@ -430,6 +435,7 @@ class Traceback(_pytest._code.Traceback):
430435

431436
class FakeExcinfo(_pytest._code.ExceptionInfo):
432437
typename = "Foo"
438+
value = Exception()
433439
def __init__(self):
434440
pass
435441

@@ -447,10 +453,15 @@ class FakeRawTB(object):
447453
fail = IOError() # noqa
448454
repr = pr.repr_excinfo(excinfo)
449455
assert repr.reprtraceback.reprentries[0].lines[0] == "> ???"
456+
if py.std.sys.version_info[0] >= 3:
457+
assert repr.chain[0][0].reprentries[0].lines[0] == "> ???"
458+
450459

451460
fail = py.error.ENOENT # noqa
452461
repr = pr.repr_excinfo(excinfo)
453462
assert repr.reprtraceback.reprentries[0].lines[0] == "> ???"
463+
if py.std.sys.version_info[0] >= 3:
464+
assert repr.chain[0][0].reprentries[0].lines[0] == "> ???"
454465

455466

456467
def test_repr_local(self):
@@ -637,6 +648,9 @@ def entry():
637648
repr = p.repr_excinfo(excinfo)
638649
assert repr.reprtraceback
639650
assert len(repr.reprtraceback.reprentries) == len(reprtb.reprentries)
651+
if py.std.sys.version_info[0] >= 3:
652+
assert repr.chain[0][0]
653+
assert len(repr.chain[0][0].reprentries) == len(reprtb.reprentries)
640654
assert repr.reprcrash.path.endswith("mod.py")
641655
assert repr.reprcrash.message == "ValueError: 0"
642656

@@ -727,8 +741,13 @@ def entry():
727741
for style in ("short", "long", "no"):
728742
for showlocals in (True, False):
729743
repr = excinfo.getrepr(style=style, showlocals=showlocals)
730-
assert isinstance(repr, ReprExceptionInfo)
744+
if py.std.sys.version_info[0] < 3:
745+
assert isinstance(repr, ReprExceptionInfo)
731746
assert repr.reprtraceback.style == style
747+
if py.std.sys.version_info[0] >= 3:
748+
assert isinstance(repr, ExceptionChainRepr)
749+
for repr in repr.chain:
750+
assert repr[0].style == style
732751

733752
def test_reprexcinfo_unicode(self):
734753
from _pytest._code.code import TerminalRepr
@@ -909,3 +928,70 @@ def i():
909928
assert tw.lines[14] == "E ValueError"
910929
assert tw.lines[15] == ""
911930
assert tw.lines[16].endswith("mod.py:9: ValueError")
931+
932+
@pytest.mark.skipif("sys.version_info[0] < 3")
933+
def test_exc_chain_repr(self, importasmod):
934+
mod = importasmod("""
935+
class Err(Exception):
936+
pass
937+
def f():
938+
try:
939+
g()
940+
except Exception as e:
941+
raise Err() from e
942+
finally:
943+
h()
944+
def g():
945+
raise ValueError()
946+
947+
def h():
948+
raise AttributeError()
949+
""")
950+
excinfo = pytest.raises(AttributeError, mod.f)
951+
r = excinfo.getrepr(style="long")
952+
tw = TWMock()
953+
r.toterminal(tw)
954+
for line in tw.lines: print (line)
955+
assert tw.lines[0] == ""
956+
assert tw.lines[1] == " def f():"
957+
assert tw.lines[2] == " try:"
958+
assert tw.lines[3] == "> g()"
959+
assert tw.lines[4] == ""
960+
assert tw.lines[5].endswith("mod.py:6: ")
961+
assert tw.lines[6] == ("_ ", None)
962+
assert tw.lines[7] == ""
963+
assert tw.lines[8] == " def g():"
964+
assert tw.lines[9] == "> raise ValueError()"
965+
assert tw.lines[10] == "E ValueError"
966+
assert tw.lines[11] == ""
967+
assert tw.lines[12].endswith("mod.py:12: ValueError")
968+
assert tw.lines[13] == ""
969+
assert tw.lines[14] == "The above exception was the direct cause of the following exception:"
970+
assert tw.lines[15] == ""
971+
assert tw.lines[16] == " def f():"
972+
assert tw.lines[17] == " try:"
973+
assert tw.lines[18] == " g()"
974+
assert tw.lines[19] == " except Exception as e:"
975+
assert tw.lines[20] == "> raise Err() from e"
976+
assert tw.lines[21] == "E test_exc_chain_repr0.mod.Err"
977+
assert tw.lines[22] == ""
978+
assert tw.lines[23].endswith("mod.py:8: Err")
979+
assert tw.lines[24] == ""
980+
assert tw.lines[25] == "During handling of the above exception, another exception occurred:"
981+
assert tw.lines[26] == ""
982+
assert tw.lines[27] == " def f():"
983+
assert tw.lines[28] == " try:"
984+
assert tw.lines[29] == " g()"
985+
assert tw.lines[30] == " except Exception as e:"
986+
assert tw.lines[31] == " raise Err() from e"
987+
assert tw.lines[32] == " finally:"
988+
assert tw.lines[33] == "> h()"
989+
assert tw.lines[34] == ""
990+
assert tw.lines[35].endswith("mod.py:10: ")
991+
assert tw.lines[36] == ('_ ', None)
992+
assert tw.lines[37] == ""
993+
assert tw.lines[38] == " def h():"
994+
assert tw.lines[39] == "> raise AttributeError()"
995+
assert tw.lines[40] == "E AttributeError"
996+
assert tw.lines[41] == ""
997+
assert tw.lines[42].endswith("mod.py:15: AttributeError")

0 commit comments

Comments
 (0)