fix #4386 - restructure construction and partial state of ExceptionInfo

This commit is contained in:
Ronny Pfannschmidt 2018-11-22 12:20:14 +01:00
parent 6e85febf20
commit 88bf01a31e
12 changed files with 113 additions and 59 deletions

View File

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

View File

@ -391,40 +391,85 @@ co_equal = compile(
)
@attr.s(repr=False)
class ExceptionInfo(object):
""" wraps sys.exc_info() objects and offers
help for navigating the traceback.
"""
_striptext = ""
_assert_start_repr = (
"AssertionError(u'assert " if _PY2 else "AssertionError('assert "
)
def __init__(self, tup=None, exprinfo=None):
import _pytest._code
_excinfo = attr.ib()
_striptext = attr.ib(default="")
_traceback = attr.ib(default=None)
if tup is None:
tup = sys.exc_info()
if exprinfo is None and isinstance(tup[1], AssertionError):
exprinfo = getattr(tup[1], "msg", None)
if exprinfo is None:
exprinfo = py.io.saferepr(tup[1])
if exprinfo and exprinfo.startswith(self._assert_start_repr):
self._striptext = "AssertionError: "
self._excinfo = tup
#: the exception class
self.type = tup[0]
#: the exception instance
self.value = tup[1]
#: the exception raw traceback
self.tb = tup[2]
#: the exception type name
self.typename = self.type.__name__
#: the exception traceback (_pytest._code.Traceback instance)
self.traceback = _pytest._code.Traceback(self.tb, excinfo=ref(self))
@classmethod
def from_current(cls, exprinfo=None):
"""returns a exceptioninfo matching the current traceback
.. warning::
experimental api
:param exprinfo: an text string helping to determine if we should
strip assertionerror from the output, defaults
to the exception message/__str__()
"""
tup = sys.exc_info()
_striptext = ""
if exprinfo is None and isinstance(tup[1], AssertionError):
exprinfo = getattr(tup[1], "msg", None)
if exprinfo is None:
exprinfo = py.io.saferepr(tup[1])
if exprinfo and exprinfo.startswith(cls._assert_start_repr):
_striptext = "AssertionError: "
return cls(tup, _striptext)
@classmethod
def for_later(cls):
"""return an unfilled ExceptionInfo
"""
return cls(None)
@property
def type(self):
"""the exception class"""
return self._excinfo[0]
@property
def value(self):
"""the exception value"""
return self._excinfo[1]
@property
def tb(self):
"""the exception raw traceback"""
return self._excinfo[2]
@property
def typename(self):
"""the type name of the exception"""
return self.type.__name__
@property
def traceback(self):
"""the traceback"""
if self._traceback is None:
self._traceback = Traceback(self.tb, excinfo=ref(self))
return self._traceback
@traceback.setter
def traceback(self, value):
self._traceback = value
def __repr__(self):
if self._excinfo is None:
return "<ExceptionInfo for raises contextmanager>"
return "<ExceptionInfo %s tblen=%d>" % (self.typename, len(self.traceback))
def exconly(self, tryshort=False):
@ -513,6 +558,8 @@ class ExceptionInfo(object):
return fmt.repr_excinfo(self)
def __str__(self):
if self._excinfo is None:
return repr(self)
entry = self.traceback[-1]
loc = ReprFileLocation(entry.path, entry.lineno + 1, self.exconly())
return str(loc)

View File

@ -155,7 +155,7 @@ def assertrepr_compare(config, op, left, right):
explanation = [
u"(pytest_assertion plugin: representation of details failed. "
u"Probably an object has a faulty __repr__.)",
six.text_type(_pytest._code.ExceptionInfo()),
six.text_type(_pytest._code.ExceptionInfo.from_current()),
]
if not explanation:

View File

@ -188,7 +188,7 @@ def wrap_session(config, doit):
except Failed:
session.exitstatus = EXIT_TESTSFAILED
except KeyboardInterrupt:
excinfo = _pytest._code.ExceptionInfo()
excinfo = _pytest._code.ExceptionInfo.from_current()
exitstatus = EXIT_INTERRUPTED
if initstate <= 2 and isinstance(excinfo.value, exit.Exception):
sys.stderr.write("{}: {}\n".format(excinfo.typename, excinfo.value.msg))
@ -197,7 +197,7 @@ def wrap_session(config, doit):
config.hook.pytest_keyboard_interrupt(excinfo=excinfo)
session.exitstatus = exitstatus
except: # noqa
excinfo = _pytest._code.ExceptionInfo()
excinfo = _pytest._code.ExceptionInfo.from_current()
config.notify_exception(excinfo, config.option)
session.exitstatus = EXIT_INTERNALERROR
if excinfo.errisinstance(SystemExit):

View File

@ -450,7 +450,7 @@ class Module(nodes.File, PyCollector):
mod = self.fspath.pyimport(ensuresyspath=importmode)
except SyntaxError:
raise self.CollectError(
_pytest._code.ExceptionInfo().getrepr(style="short")
_pytest._code.ExceptionInfo.from_current().getrepr(style="short")
)
except self.fspath.ImportMismatchError:
e = sys.exc_info()[1]
@ -466,7 +466,7 @@ class Module(nodes.File, PyCollector):
except ImportError:
from _pytest._code.code import ExceptionInfo
exc_info = ExceptionInfo()
exc_info = ExceptionInfo.from_current()
if self.config.getoption("verbose") < 2:
exc_info.traceback = exc_info.traceback.filter(filter_traceback)
exc_repr = (

View File

@ -684,13 +684,13 @@ def raises(expected_exception, *args, **kwargs):
# XXX didn't mean f_globals == f_locals something special?
# this is destroyed here ...
except expected_exception:
return _pytest._code.ExceptionInfo()
return _pytest._code.ExceptionInfo.from_current()
else:
func = args[0]
try:
func(*args[1:], **kwargs)
except expected_exception:
return _pytest._code.ExceptionInfo()
return _pytest._code.ExceptionInfo.from_current()
fail(message)
@ -705,7 +705,7 @@ class RaisesContext(object):
self.excinfo = None
def __enter__(self):
self.excinfo = object.__new__(_pytest._code.ExceptionInfo)
self.excinfo = _pytest._code.ExceptionInfo.for_later()
return self.excinfo
def __exit__(self, *tp):

View File

@ -211,12 +211,12 @@ class CallInfo(object):
self.result = func()
except KeyboardInterrupt:
if treat_keyboard_interrupt_as_exception:
self.excinfo = ExceptionInfo()
self.excinfo = ExceptionInfo.from_current()
else:
self.stop = time()
raise
except: # noqa
self.excinfo = ExceptionInfo()
self.excinfo = ExceptionInfo.from_current()
self.stop = time()
def __repr__(self):

View File

@ -115,6 +115,10 @@ class TestCaseFunction(Function):
rawexcinfo = getattr(rawexcinfo, "_rawexcinfo", rawexcinfo)
try:
excinfo = _pytest._code.ExceptionInfo(rawexcinfo)
# invoke the attributes to trigger storing the traceback
# trial causes some issue there
excinfo.value
excinfo.traceback
except TypeError:
try:
try:
@ -136,7 +140,7 @@ class TestCaseFunction(Function):
except KeyboardInterrupt:
raise
except fail.Exception:
excinfo = _pytest._code.ExceptionInfo()
excinfo = _pytest._code.ExceptionInfo.from_current()
self.__dict__.setdefault("_excinfo", []).append(excinfo)
def addError(self, testcase, rawexcinfo):

View File

@ -169,7 +169,7 @@ class TestExceptionInfo(object):
else:
assert False
except AssertionError:
exci = _pytest._code.ExceptionInfo()
exci = _pytest._code.ExceptionInfo.from_current()
assert exci.getrepr()
@ -181,7 +181,7 @@ class TestTracebackEntry(object):
else:
assert False
except AssertionError:
exci = _pytest._code.ExceptionInfo()
exci = _pytest._code.ExceptionInfo.from_current()
entry = exci.traceback[0]
source = entry.getsource()
assert len(source) == 6

View File

@ -71,7 +71,7 @@ def test_excinfo_simple():
try:
raise ValueError
except ValueError:
info = _pytest._code.ExceptionInfo()
info = _pytest._code.ExceptionInfo.from_current()
assert info.type == ValueError
@ -85,7 +85,7 @@ def test_excinfo_getstatement():
try:
f()
except ValueError:
excinfo = _pytest._code.ExceptionInfo()
excinfo = _pytest._code.ExceptionInfo.from_current()
linenumbers = [
_pytest._code.getrawcode(f).co_firstlineno - 1 + 4,
_pytest._code.getrawcode(f).co_firstlineno - 1 + 1,
@ -126,7 +126,7 @@ class TestTraceback_f_g_h(object):
try:
h()
except ValueError:
self.excinfo = _pytest._code.ExceptionInfo()
self.excinfo = _pytest._code.ExceptionInfo.from_current()
def test_traceback_entries(self):
tb = self.excinfo.traceback
@ -163,7 +163,7 @@ class TestTraceback_f_g_h(object):
try:
exec(source.compile())
except NameError:
tb = _pytest._code.ExceptionInfo().traceback
tb = _pytest._code.ExceptionInfo.from_current().traceback
print(tb[-1].getsource())
s = str(tb[-1].getsource())
assert s.startswith("def xyz():\n try:")
@ -356,6 +356,12 @@ def test_excinfo_str():
assert len(s.split(":")) >= 3 # on windows it's 4
def test_excinfo_for_later():
e = ExceptionInfo.for_later()
assert "for raises" in repr(e)
assert "for raises" in str(e)
def test_excinfo_errisinstance():
excinfo = pytest.raises(ValueError, h)
assert excinfo.errisinstance(ValueError)
@ -365,7 +371,7 @@ def test_excinfo_no_sourcecode():
try:
exec("raise ValueError()")
except ValueError:
excinfo = _pytest._code.ExceptionInfo()
excinfo = _pytest._code.ExceptionInfo.from_current()
s = str(excinfo.traceback[-1])
assert s == " File '<string>':1 in <module>\n ???\n"
@ -390,7 +396,7 @@ def test_entrysource_Queue_example():
try:
queue.Queue().get(timeout=0.001)
except queue.Empty:
excinfo = _pytest._code.ExceptionInfo()
excinfo = _pytest._code.ExceptionInfo.from_current()
entry = excinfo.traceback[-1]
source = entry.getsource()
assert source is not None
@ -402,7 +408,7 @@ def test_codepath_Queue_example():
try:
queue.Queue().get(timeout=0.001)
except queue.Empty:
excinfo = _pytest._code.ExceptionInfo()
excinfo = _pytest._code.ExceptionInfo.from_current()
entry = excinfo.traceback[-1]
path = entry.path
assert isinstance(path, py.path.local)
@ -453,7 +459,7 @@ class TestFormattedExcinfo(object):
except KeyboardInterrupt:
raise
except: # noqa
return _pytest._code.ExceptionInfo()
return _pytest._code.ExceptionInfo.from_current()
assert 0, "did not raise"
def test_repr_source(self):
@ -491,7 +497,7 @@ class TestFormattedExcinfo(object):
try:
exec(co)
except ValueError:
excinfo = _pytest._code.ExceptionInfo()
excinfo = _pytest._code.ExceptionInfo.from_current()
repr = pr.repr_excinfo(excinfo)
assert repr.reprtraceback.reprentries[1].lines[0] == "> ???"
if sys.version_info[0] >= 3:
@ -510,7 +516,7 @@ raise ValueError()
try:
exec(co)
except ValueError:
excinfo = _pytest._code.ExceptionInfo()
excinfo = _pytest._code.ExceptionInfo.from_current()
repr = pr.repr_excinfo(excinfo)
assert repr.reprtraceback.reprentries[1].lines[0] == "> ???"
if sys.version_info[0] >= 3:
@ -1340,7 +1346,7 @@ def test_repr_traceback_with_unicode(style, encoding):
try:
raise RuntimeError(msg)
except RuntimeError:
e_info = ExceptionInfo()
e_info = ExceptionInfo.from_current()
formatter = FormattedExcinfo(style=style)
repr_traceback = formatter.repr_traceback(e_info)
assert repr_traceback is not None

View File

@ -151,7 +151,7 @@ class TestWithFunctionIntegration(object):
try:
raise ValueError
except ValueError:
excinfo = _pytest._code.ExceptionInfo()
excinfo = _pytest._code.ExceptionInfo.from_current()
reslog = ResultLog(None, py.io.TextIO())
reslog.pytest_internalerror(excinfo.getrepr(style=style))
entry = reslog.logfile.getvalue()

View File

@ -561,20 +561,16 @@ def test_outcomeexception_passes_except_Exception():
def test_pytest_exit():
try:
with pytest.raises(pytest.exit.Exception) as excinfo:
pytest.exit("hello")
except pytest.exit.Exception:
excinfo = _pytest._code.ExceptionInfo()
assert excinfo.errisinstance(KeyboardInterrupt)
assert excinfo.errisinstance(KeyboardInterrupt)
def test_pytest_fail():
try:
with pytest.raises(pytest.fail.Exception) as excinfo:
pytest.fail("hello")
except pytest.fail.Exception:
excinfo = _pytest._code.ExceptionInfo()
s = excinfo.exconly(tryshort=True)
assert s.startswith("Failed")
s = excinfo.exconly(tryshort=True)
assert s.startswith("Failed")
def test_pytest_exit_msg(testdir):
@ -683,7 +679,7 @@ def test_exception_printing_skip():
try:
pytest.skip("hello")
except pytest.skip.Exception:
excinfo = _pytest._code.ExceptionInfo()
excinfo = _pytest._code.ExceptionInfo.from_current()
s = excinfo.exconly(tryshort=True)
assert s.startswith("Skipped")
@ -718,7 +714,7 @@ def test_importorskip(monkeypatch):
mod2 = pytest.importorskip("hello123", minversion="1.3")
assert mod2 == mod
except pytest.skip.Exception:
print(_pytest._code.ExceptionInfo())
print(_pytest._code.ExceptionInfo.from_current())
pytest.fail("spurious skip")
@ -740,7 +736,7 @@ def test_importorskip_dev_module(monkeypatch):
pytest.importorskip('mockmodule1', minversion='0.14.0')""",
)
except pytest.skip.Exception:
print(_pytest._code.ExceptionInfo())
print(_pytest._code.ExceptionInfo.from_current())
pytest.fail("spurious skip")