Skip to content

Commit 63f90a2

Browse files
authored
Merge pull request #4438 from RonnyPfannschmidt/fix-4386-raises-partial-object
fix #4386 - restructure construction and partial state of ExceptionInfo
2 parents f987b36 + 2eaf3db commit 63f90a2

File tree

12 files changed

+112
-59
lines changed

12 files changed

+112
-59
lines changed

changelog/4386.feature.rst

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Restructure ExceptionInfo object construction and ensure incomplete instances have a ``repr``/``str``.

src/_pytest/_code/code.py

+68-22
Original file line numberDiff line numberDiff line change
@@ -391,40 +391,84 @@ def recursionindex(self):
391391
)
392392

393393

394+
@attr.s(repr=False)
394395
class ExceptionInfo(object):
395396
""" wraps sys.exc_info() objects and offers
396397
help for navigating the traceback.
397398
"""
398399

399-
_striptext = ""
400400
_assert_start_repr = (
401401
"AssertionError(u'assert " if _PY2 else "AssertionError('assert "
402402
)
403403

404-
def __init__(self, tup=None, exprinfo=None):
405-
import _pytest._code
404+
_excinfo = attr.ib()
405+
_striptext = attr.ib(default="")
406+
_traceback = attr.ib(default=None)
407+
408+
@classmethod
409+
def from_current(cls, exprinfo=None):
410+
"""returns an ExceptionInfo matching the current traceback
411+
412+
.. warning::
413+
414+
Experimental API
415+
416+
417+
:param exprinfo: a text string helping to determine if we should
418+
strip ``AssertionError`` from the output, defaults
419+
to the exception message/``__str__()``
420+
"""
421+
tup = sys.exc_info()
422+
_striptext = ""
423+
if exprinfo is None and isinstance(tup[1], AssertionError):
424+
exprinfo = getattr(tup[1], "msg", None)
425+
if exprinfo is None:
426+
exprinfo = py.io.saferepr(tup[1])
427+
if exprinfo and exprinfo.startswith(cls._assert_start_repr):
428+
_striptext = "AssertionError: "
429+
430+
return cls(tup, _striptext)
431+
432+
@classmethod
433+
def for_later(cls):
434+
"""return an unfilled ExceptionInfo
435+
"""
436+
return cls(None)
437+
438+
@property
439+
def type(self):
440+
"""the exception class"""
441+
return self._excinfo[0]
442+
443+
@property
444+
def value(self):
445+
"""the exception value"""
446+
return self._excinfo[1]
447+
448+
@property
449+
def tb(self):
450+
"""the exception raw traceback"""
451+
return self._excinfo[2]
452+
453+
@property
454+
def typename(self):
455+
"""the type name of the exception"""
456+
return self.type.__name__
457+
458+
@property
459+
def traceback(self):
460+
"""the traceback"""
461+
if self._traceback is None:
462+
self._traceback = Traceback(self.tb, excinfo=ref(self))
463+
return self._traceback
406464

407-
if tup is None:
408-
tup = sys.exc_info()
409-
if exprinfo is None and isinstance(tup[1], AssertionError):
410-
exprinfo = getattr(tup[1], "msg", None)
411-
if exprinfo is None:
412-
exprinfo = py.io.saferepr(tup[1])
413-
if exprinfo and exprinfo.startswith(self._assert_start_repr):
414-
self._striptext = "AssertionError: "
415-
self._excinfo = tup
416-
#: the exception class
417-
self.type = tup[0]
418-
#: the exception instance
419-
self.value = tup[1]
420-
#: the exception raw traceback
421-
self.tb = tup[2]
422-
#: the exception type name
423-
self.typename = self.type.__name__
424-
#: the exception traceback (_pytest._code.Traceback instance)
425-
self.traceback = _pytest._code.Traceback(self.tb, excinfo=ref(self))
465+
@traceback.setter
466+
def traceback(self, value):
467+
self._traceback = value
426468

427469
def __repr__(self):
470+
if self._excinfo is None:
471+
return "<ExceptionInfo for raises contextmanager>"
428472
return "<ExceptionInfo %s tblen=%d>" % (self.typename, len(self.traceback))
429473

430474
def exconly(self, tryshort=False):
@@ -513,6 +557,8 @@ def getrepr(
513557
return fmt.repr_excinfo(self)
514558

515559
def __str__(self):
560+
if self._excinfo is None:
561+
return repr(self)
516562
entry = self.traceback[-1]
517563
loc = ReprFileLocation(entry.path, entry.lineno + 1, self.exconly())
518564
return str(loc)

src/_pytest/assertion/util.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,7 @@ def isiterable(obj):
164164
explanation = [
165165
u"(pytest_assertion plugin: representation of details failed. "
166166
u"Probably an object has a faulty __repr__.)",
167-
six.text_type(_pytest._code.ExceptionInfo()),
167+
six.text_type(_pytest._code.ExceptionInfo.from_current()),
168168
]
169169

170170
if not explanation:

src/_pytest/main.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ def wrap_session(config, doit):
188188
except Failed:
189189
session.exitstatus = EXIT_TESTSFAILED
190190
except KeyboardInterrupt:
191-
excinfo = _pytest._code.ExceptionInfo()
191+
excinfo = _pytest._code.ExceptionInfo.from_current()
192192
exitstatus = EXIT_INTERRUPTED
193193
if initstate <= 2 and isinstance(excinfo.value, exit.Exception):
194194
sys.stderr.write("{}: {}\n".format(excinfo.typename, excinfo.value.msg))
@@ -197,7 +197,7 @@ def wrap_session(config, doit):
197197
config.hook.pytest_keyboard_interrupt(excinfo=excinfo)
198198
session.exitstatus = exitstatus
199199
except: # noqa
200-
excinfo = _pytest._code.ExceptionInfo()
200+
excinfo = _pytest._code.ExceptionInfo.from_current()
201201
config.notify_exception(excinfo, config.option)
202202
session.exitstatus = EXIT_INTERNALERROR
203203
if excinfo.errisinstance(SystemExit):

src/_pytest/python.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -450,7 +450,7 @@ def _importtestmodule(self):
450450
mod = self.fspath.pyimport(ensuresyspath=importmode)
451451
except SyntaxError:
452452
raise self.CollectError(
453-
_pytest._code.ExceptionInfo().getrepr(style="short")
453+
_pytest._code.ExceptionInfo.from_current().getrepr(style="short")
454454
)
455455
except self.fspath.ImportMismatchError:
456456
e = sys.exc_info()[1]
@@ -466,7 +466,7 @@ def _importtestmodule(self):
466466
except ImportError:
467467
from _pytest._code.code import ExceptionInfo
468468

469-
exc_info = ExceptionInfo()
469+
exc_info = ExceptionInfo.from_current()
470470
if self.config.getoption("verbose") < 2:
471471
exc_info.traceback = exc_info.traceback.filter(filter_traceback)
472472
exc_repr = (

src/_pytest/python_api.py

+3-3
Original file line numberDiff line numberDiff line change
@@ -684,13 +684,13 @@ def raises(expected_exception, *args, **kwargs):
684684
# XXX didn't mean f_globals == f_locals something special?
685685
# this is destroyed here ...
686686
except expected_exception:
687-
return _pytest._code.ExceptionInfo()
687+
return _pytest._code.ExceptionInfo.from_current()
688688
else:
689689
func = args[0]
690690
try:
691691
func(*args[1:], **kwargs)
692692
except expected_exception:
693-
return _pytest._code.ExceptionInfo()
693+
return _pytest._code.ExceptionInfo.from_current()
694694
fail(message)
695695

696696

@@ -705,7 +705,7 @@ def __init__(self, expected_exception, message, match_expr):
705705
self.excinfo = None
706706

707707
def __enter__(self):
708-
self.excinfo = object.__new__(_pytest._code.ExceptionInfo)
708+
self.excinfo = _pytest._code.ExceptionInfo.for_later()
709709
return self.excinfo
710710

711711
def __exit__(self, *tp):

src/_pytest/runner.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -211,12 +211,12 @@ def __init__(self, func, when, treat_keyboard_interrupt_as_exception=False):
211211
self.result = func()
212212
except KeyboardInterrupt:
213213
if treat_keyboard_interrupt_as_exception:
214-
self.excinfo = ExceptionInfo()
214+
self.excinfo = ExceptionInfo.from_current()
215215
else:
216216
self.stop = time()
217217
raise
218218
except: # noqa
219-
self.excinfo = ExceptionInfo()
219+
self.excinfo = ExceptionInfo.from_current()
220220
self.stop = time()
221221

222222
def __repr__(self):

src/_pytest/unittest.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,10 @@ def _addexcinfo(self, rawexcinfo):
115115
rawexcinfo = getattr(rawexcinfo, "_rawexcinfo", rawexcinfo)
116116
try:
117117
excinfo = _pytest._code.ExceptionInfo(rawexcinfo)
118+
# invoke the attributes to trigger storing the traceback
119+
# trial causes some issue there
120+
excinfo.value
121+
excinfo.traceback
118122
except TypeError:
119123
try:
120124
try:
@@ -136,7 +140,7 @@ def _addexcinfo(self, rawexcinfo):
136140
except KeyboardInterrupt:
137141
raise
138142
except fail.Exception:
139-
excinfo = _pytest._code.ExceptionInfo()
143+
excinfo = _pytest._code.ExceptionInfo.from_current()
140144
self.__dict__.setdefault("_excinfo", []).append(excinfo)
141145

142146
def addError(self, testcase, rawexcinfo):

testing/code/test_code.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -169,7 +169,7 @@ def test_bad_getsource(self):
169169
else:
170170
assert False
171171
except AssertionError:
172-
exci = _pytest._code.ExceptionInfo()
172+
exci = _pytest._code.ExceptionInfo.from_current()
173173
assert exci.getrepr()
174174

175175

@@ -181,7 +181,7 @@ def test_getsource(self):
181181
else:
182182
assert False
183183
except AssertionError:
184-
exci = _pytest._code.ExceptionInfo()
184+
exci = _pytest._code.ExceptionInfo.from_current()
185185
entry = exci.traceback[0]
186186
source = entry.getsource()
187187
assert len(source) == 6

testing/code/test_excinfo.py

+17-11
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ def test_excinfo_simple():
7171
try:
7272
raise ValueError
7373
except ValueError:
74-
info = _pytest._code.ExceptionInfo()
74+
info = _pytest._code.ExceptionInfo.from_current()
7575
assert info.type == ValueError
7676

7777

@@ -85,7 +85,7 @@ def f():
8585
try:
8686
f()
8787
except ValueError:
88-
excinfo = _pytest._code.ExceptionInfo()
88+
excinfo = _pytest._code.ExceptionInfo.from_current()
8989
linenumbers = [
9090
_pytest._code.getrawcode(f).co_firstlineno - 1 + 4,
9191
_pytest._code.getrawcode(f).co_firstlineno - 1 + 1,
@@ -126,7 +126,7 @@ def setup_method(self, method):
126126
try:
127127
h()
128128
except ValueError:
129-
self.excinfo = _pytest._code.ExceptionInfo()
129+
self.excinfo = _pytest._code.ExceptionInfo.from_current()
130130

131131
def test_traceback_entries(self):
132132
tb = self.excinfo.traceback
@@ -163,7 +163,7 @@ def xyz():
163163
try:
164164
exec(source.compile())
165165
except NameError:
166-
tb = _pytest._code.ExceptionInfo().traceback
166+
tb = _pytest._code.ExceptionInfo.from_current().traceback
167167
print(tb[-1].getsource())
168168
s = str(tb[-1].getsource())
169169
assert s.startswith("def xyz():\n try:")
@@ -356,6 +356,12 @@ def test_excinfo_str():
356356
assert len(s.split(":")) >= 3 # on windows it's 4
357357

358358

359+
def test_excinfo_for_later():
360+
e = ExceptionInfo.for_later()
361+
assert "for raises" in repr(e)
362+
assert "for raises" in str(e)
363+
364+
359365
def test_excinfo_errisinstance():
360366
excinfo = pytest.raises(ValueError, h)
361367
assert excinfo.errisinstance(ValueError)
@@ -365,7 +371,7 @@ def test_excinfo_no_sourcecode():
365371
try:
366372
exec("raise ValueError()")
367373
except ValueError:
368-
excinfo = _pytest._code.ExceptionInfo()
374+
excinfo = _pytest._code.ExceptionInfo.from_current()
369375
s = str(excinfo.traceback[-1])
370376
assert s == " File '<string>':1 in <module>\n ???\n"
371377

@@ -390,7 +396,7 @@ def test_entrysource_Queue_example():
390396
try:
391397
queue.Queue().get(timeout=0.001)
392398
except queue.Empty:
393-
excinfo = _pytest._code.ExceptionInfo()
399+
excinfo = _pytest._code.ExceptionInfo.from_current()
394400
entry = excinfo.traceback[-1]
395401
source = entry.getsource()
396402
assert source is not None
@@ -402,7 +408,7 @@ def test_codepath_Queue_example():
402408
try:
403409
queue.Queue().get(timeout=0.001)
404410
except queue.Empty:
405-
excinfo = _pytest._code.ExceptionInfo()
411+
excinfo = _pytest._code.ExceptionInfo.from_current()
406412
entry = excinfo.traceback[-1]
407413
path = entry.path
408414
assert isinstance(path, py.path.local)
@@ -453,7 +459,7 @@ def excinfo_from_exec(self, source):
453459
except KeyboardInterrupt:
454460
raise
455461
except: # noqa
456-
return _pytest._code.ExceptionInfo()
462+
return _pytest._code.ExceptionInfo.from_current()
457463
assert 0, "did not raise"
458464

459465
def test_repr_source(self):
@@ -491,7 +497,7 @@ def test_repr_source_not_existing(self):
491497
try:
492498
exec(co)
493499
except ValueError:
494-
excinfo = _pytest._code.ExceptionInfo()
500+
excinfo = _pytest._code.ExceptionInfo.from_current()
495501
repr = pr.repr_excinfo(excinfo)
496502
assert repr.reprtraceback.reprentries[1].lines[0] == "> ???"
497503
if sys.version_info[0] >= 3:
@@ -510,7 +516,7 @@ def test_repr_many_line_source_not_existing(self):
510516
try:
511517
exec(co)
512518
except ValueError:
513-
excinfo = _pytest._code.ExceptionInfo()
519+
excinfo = _pytest._code.ExceptionInfo.from_current()
514520
repr = pr.repr_excinfo(excinfo)
515521
assert repr.reprtraceback.reprentries[1].lines[0] == "> ???"
516522
if sys.version_info[0] >= 3:
@@ -1340,7 +1346,7 @@ def test_repr_traceback_with_unicode(style, encoding):
13401346
try:
13411347
raise RuntimeError(msg)
13421348
except RuntimeError:
1343-
e_info = ExceptionInfo()
1349+
e_info = ExceptionInfo.from_current()
13441350
formatter = FormattedExcinfo(style=style)
13451351
repr_traceback = formatter.repr_traceback(e_info)
13461352
assert repr_traceback is not None

testing/test_resultlog.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ def test_internal_exception(self, style):
151151
try:
152152
raise ValueError
153153
except ValueError:
154-
excinfo = _pytest._code.ExceptionInfo()
154+
excinfo = _pytest._code.ExceptionInfo.from_current()
155155
reslog = ResultLog(None, py.io.TextIO())
156156
reslog.pytest_internalerror(excinfo.getrepr(style=style))
157157
entry = reslog.logfile.getvalue()

0 commit comments

Comments
 (0)