Merge pull request #4438 from RonnyPfannschmidt/fix-4386-raises-partial-object
fix #4386 - restructure construction and partial state of ExceptionInfo
This commit is contained in:
commit
63f90a2bcd
|
@ -0,0 +1 @@
|
|||
Restructure ExceptionInfo object construction and ensure incomplete instances have a ``repr``/``str``.
|
|
@ -391,40 +391,84 @@ 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:
|
||||
@classmethod
|
||||
def from_current(cls, exprinfo=None):
|
||||
"""returns an ExceptionInfo matching the current traceback
|
||||
|
||||
.. warning::
|
||||
|
||||
Experimental API
|
||||
|
||||
|
||||
:param exprinfo: a 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(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))
|
||||
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 +557,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)
|
||||
|
|
|
@ -164,7 +164,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:
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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 = (
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -561,18 +561,14 @@ 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)
|
||||
|
||||
|
||||
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")
|
||||
|
||||
|
@ -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")
|
||||
|
||||
|
||||
|
|
Loading…
Reference in New Issue