Skip to content

Commit 0f7aeaf

Browse files
Merge pull request #1486 from roolebo/fix-issue-138
Fix issue #138 - support chained exceptions
2 parents e3bc6fa + 89df701 commit 0f7aeaf

File tree

5 files changed

+154
-11
lines changed

5 files changed

+154
-11
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ Piotr Banaszkiewicz
7575
Punyashloka Biswal
7676
Ralf Schmitt
7777
Raphael Pierzina
78+
Roman Bolshakov
7879
Ronny Pfannschmidt
7980
Ross Lawley
8081
Ryan Wooden

CHANGELOG.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,11 +98,13 @@
9898

9999
* Fix (`#649`_): parametrized test nodes cannot be specified to run on the command line.
100100

101+
* Fix (`#138`_): better reporting for python 3.3+ chained exceptions
101102

102103
.. _#1437: https://github.com/pytest-dev/pytest/issues/1437
103104
.. _#469: https://github.com/pytest-dev/pytest/issues/469
104105
.. _#1431: https://github.com/pytest-dev/pytest/pull/1431
105106
.. _#649: https://github.com/pytest-dev/pytest/issues/649
107+
.. _#138: https://github.com/pytest-dev/pytest/issues/138
106108

107109
.. _@asottile: https://github.com/asottile
108110

_pytest/_code/code.py

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

611+
611612
def repr_excinfo(self, excinfo):
612-
reprtraceback = self.repr_traceback(excinfo)
613-
reprcrash = excinfo._getreprcrash()
614-
return ReprExceptionInfo(reprtraceback, reprcrash)
613+
if sys.version_info[0] < 3:
614+
reprtraceback = self.repr_traceback(excinfo)
615+
reprcrash = excinfo._getreprcrash()
616+
617+
return ReprExceptionInfo(reprtraceback, reprcrash)
618+
else:
619+
repr_chain = []
620+
e = excinfo.value
621+
descr = None
622+
while e is not None:
623+
reprtraceback = self.repr_traceback(excinfo)
624+
reprcrash = excinfo._getreprcrash()
625+
repr_chain += [(reprtraceback, reprcrash, descr)]
626+
if e.__cause__ is not None:
627+
e = e.__cause__
628+
excinfo = ExceptionInfo((type(e), e, e.__traceback__))
629+
descr = 'The above exception was the direct cause of the following exception:'
630+
elif e.__context__ is not None:
631+
e = e.__context__
632+
excinfo = ExceptionInfo((type(e), e, e.__traceback__))
633+
descr = 'During handling of the above exception, another exception occurred:'
634+
else:
635+
e = None
636+
repr_chain.reverse()
637+
return ExceptionChainRepr(repr_chain)
638+
615639

616-
class TerminalRepr:
640+
class TerminalRepr(object):
617641
def __str__(self):
618642
s = self.__unicode__()
619643
if sys.version_info[0] < 3:
@@ -632,21 +656,47 @@ def __repr__(self):
632656
return "<%s instance at %0x>" %(self.__class__, id(self))
633657

634658

635-
class ReprExceptionInfo(TerminalRepr):
636-
def __init__(self, reprtraceback, reprcrash):
637-
self.reprtraceback = reprtraceback
638-
self.reprcrash = reprcrash
659+
class ExceptionRepr(TerminalRepr):
660+
def __init__(self):
639661
self.sections = []
640662

641663
def addsection(self, name, content, sep="-"):
642664
self.sections.append((name, content, sep))
643665

644666
def toterminal(self, tw):
645-
self.reprtraceback.toterminal(tw)
646667
for name, content, sep in self.sections:
647668
tw.sep(sep, name)
648669
tw.line(content)
649670

671+
672+
class ExceptionChainRepr(ExceptionRepr):
673+
def __init__(self, chain):
674+
super(ExceptionChainRepr, self).__init__()
675+
self.chain = chain
676+
# reprcrash and reprtraceback of the outermost (the newest) exception
677+
# in the chain
678+
self.reprtraceback = chain[-1][0]
679+
self.reprcrash = chain[-1][1]
680+
681+
def toterminal(self, tw):
682+
for element in self.chain:
683+
element[0].toterminal(tw)
684+
if element[2] is not None:
685+
tw.line("")
686+
tw.line(element[2], yellow=True)
687+
super(ExceptionChainRepr, self).toterminal(tw)
688+
689+
690+
class ReprExceptionInfo(ExceptionRepr):
691+
def __init__(self, reprtraceback, reprcrash):
692+
super(ReprExceptionInfo, self).__init__()
693+
self.reprtraceback = reprtraceback
694+
self.reprcrash = reprcrash
695+
696+
def toterminal(self, tw):
697+
self.reprtraceback.toterminal(tw)
698+
super(ReprExceptionInfo, self).toterminal(tw)
699+
650700
class ReprTraceback(TerminalRepr):
651701
entrysep = "_ "
652702

_pytest/runner.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -494,9 +494,13 @@ def importorskip(modname, minversion=None):
494494
"""
495495
__tracebackhide__ = True
496496
compile(modname, '', 'eval') # to catch syntaxerrors
497+
should_skip = False
497498
try:
498499
__import__(modname)
499500
except ImportError:
501+
# Do not raise chained exception here(#1485)
502+
should_skip = True
503+
if should_skip:
500504
skip("could not import %r" %(modname,))
501505
mod = sys.modules[modname]
502506
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

@@ -404,6 +405,8 @@ def test_repr_source_not_existing(self):
404405
excinfo = _pytest._code.ExceptionInfo()
405406
repr = pr.repr_excinfo(excinfo)
406407
assert repr.reprtraceback.reprentries[1].lines[0] == "> ???"
408+
if py.std.sys.version_info[0] >= 3:
409+
assert repr.chain[0][0].reprentries[1].lines[0] == "> ???"
407410

408411
def test_repr_many_line_source_not_existing(self):
409412
pr = FormattedExcinfo()
@@ -417,6 +420,8 @@ def test_repr_many_line_source_not_existing(self):
417420
excinfo = _pytest._code.ExceptionInfo()
418421
repr = pr.repr_excinfo(excinfo)
419422
assert repr.reprtraceback.reprentries[1].lines[0] == "> ???"
423+
if py.std.sys.version_info[0] >= 3:
424+
assert repr.chain[0][0].reprentries[1].lines[0] == "> ???"
420425

421426
def test_repr_source_failing_fullsource(self):
422427
pr = FormattedExcinfo()
@@ -449,6 +454,7 @@ class Traceback(_pytest._code.Traceback):
449454

450455
class FakeExcinfo(_pytest._code.ExceptionInfo):
451456
typename = "Foo"
457+
value = Exception()
452458
def __init__(self):
453459
pass
454460

@@ -466,10 +472,15 @@ class FakeRawTB(object):
466472
fail = IOError() # noqa
467473
repr = pr.repr_excinfo(excinfo)
468474
assert repr.reprtraceback.reprentries[0].lines[0] == "> ???"
475+
if py.std.sys.version_info[0] >= 3:
476+
assert repr.chain[0][0].reprentries[0].lines[0] == "> ???"
477+
469478

470479
fail = py.error.ENOENT # noqa
471480
repr = pr.repr_excinfo(excinfo)
472481
assert repr.reprtraceback.reprentries[0].lines[0] == "> ???"
482+
if py.std.sys.version_info[0] >= 3:
483+
assert repr.chain[0][0].reprentries[0].lines[0] == "> ???"
473484

474485

475486
def test_repr_local(self):
@@ -656,6 +667,9 @@ def entry():
656667
repr = p.repr_excinfo(excinfo)
657668
assert repr.reprtraceback
658669
assert len(repr.reprtraceback.reprentries) == len(reprtb.reprentries)
670+
if py.std.sys.version_info[0] >= 3:
671+
assert repr.chain[0][0]
672+
assert len(repr.chain[0][0].reprentries) == len(reprtb.reprentries)
659673
assert repr.reprcrash.path.endswith("mod.py")
660674
assert repr.reprcrash.message == "ValueError: 0"
661675

@@ -746,8 +760,13 @@ def entry():
746760
for style in ("short", "long", "no"):
747761
for showlocals in (True, False):
748762
repr = excinfo.getrepr(style=style, showlocals=showlocals)
749-
assert isinstance(repr, ReprExceptionInfo)
763+
if py.std.sys.version_info[0] < 3:
764+
assert isinstance(repr, ReprExceptionInfo)
750765
assert repr.reprtraceback.style == style
766+
if py.std.sys.version_info[0] >= 3:
767+
assert isinstance(repr, ExceptionChainRepr)
768+
for repr in repr.chain:
769+
assert repr[0].style == style
751770

752771
def test_reprexcinfo_unicode(self):
753772
from _pytest._code.code import TerminalRepr
@@ -928,3 +947,70 @@ def i():
928947
assert tw.lines[14] == "E ValueError"
929948
assert tw.lines[15] == ""
930949
assert tw.lines[16].endswith("mod.py:9: ValueError")
950+
951+
@pytest.mark.skipif("sys.version_info[0] < 3")
952+
def test_exc_chain_repr(self, importasmod):
953+
mod = importasmod("""
954+
class Err(Exception):
955+
pass
956+
def f():
957+
try:
958+
g()
959+
except Exception as e:
960+
raise Err() from e
961+
finally:
962+
h()
963+
def g():
964+
raise ValueError()
965+
966+
def h():
967+
raise AttributeError()
968+
""")
969+
excinfo = pytest.raises(AttributeError, mod.f)
970+
r = excinfo.getrepr(style="long")
971+
tw = TWMock()
972+
r.toterminal(tw)
973+
for line in tw.lines: print (line)
974+
assert tw.lines[0] == ""
975+
assert tw.lines[1] == " def f():"
976+
assert tw.lines[2] == " try:"
977+
assert tw.lines[3] == "> g()"
978+
assert tw.lines[4] == ""
979+
assert tw.lines[5].endswith("mod.py:6: ")
980+
assert tw.lines[6] == ("_ ", None)
981+
assert tw.lines[7] == ""
982+
assert tw.lines[8] == " def g():"
983+
assert tw.lines[9] == "> raise ValueError()"
984+
assert tw.lines[10] == "E ValueError"
985+
assert tw.lines[11] == ""
986+
assert tw.lines[12].endswith("mod.py:12: ValueError")
987+
assert tw.lines[13] == ""
988+
assert tw.lines[14] == "The above exception was the direct cause of the following exception:"
989+
assert tw.lines[15] == ""
990+
assert tw.lines[16] == " def f():"
991+
assert tw.lines[17] == " try:"
992+
assert tw.lines[18] == " g()"
993+
assert tw.lines[19] == " except Exception as e:"
994+
assert tw.lines[20] == "> raise Err() from e"
995+
assert tw.lines[21] == "E test_exc_chain_repr0.mod.Err"
996+
assert tw.lines[22] == ""
997+
assert tw.lines[23].endswith("mod.py:8: Err")
998+
assert tw.lines[24] == ""
999+
assert tw.lines[25] == "During handling of the above exception, another exception occurred:"
1000+
assert tw.lines[26] == ""
1001+
assert tw.lines[27] == " def f():"
1002+
assert tw.lines[28] == " try:"
1003+
assert tw.lines[29] == " g()"
1004+
assert tw.lines[30] == " except Exception as e:"
1005+
assert tw.lines[31] == " raise Err() from e"
1006+
assert tw.lines[32] == " finally:"
1007+
assert tw.lines[33] == "> h()"
1008+
assert tw.lines[34] == ""
1009+
assert tw.lines[35].endswith("mod.py:10: ")
1010+
assert tw.lines[36] == ('_ ', None)
1011+
assert tw.lines[37] == ""
1012+
assert tw.lines[38] == " def h():"
1013+
assert tw.lines[39] == "> raise AttributeError()"
1014+
assert tw.lines[40] == "E AttributeError"
1015+
assert tw.lines[41] == ""
1016+
assert tw.lines[42].endswith("mod.py:15: AttributeError")

0 commit comments

Comments
 (0)