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:
Bruno Oliveira 2018-11-23 12:41:14 -02:00 committed by GitHub
commit 63f90a2bcd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 112 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,84 @@ co_equal = compile(
) )
@attr.s(repr=False)
class ExceptionInfo(object): class ExceptionInfo(object):
""" wraps sys.exc_info() objects and offers """ wraps sys.exc_info() objects and offers
help for navigating the traceback. help for navigating the traceback.
""" """
_striptext = ""
_assert_start_repr = ( _assert_start_repr = (
"AssertionError(u'assert " if _PY2 else "AssertionError('assert " "AssertionError(u'assert " if _PY2 else "AssertionError('assert "
) )
def __init__(self, tup=None, exprinfo=None): _excinfo = attr.ib()
import _pytest._code _striptext = attr.ib(default="")
_traceback = attr.ib(default=None)
if tup is None: @classmethod
tup = sys.exc_info() def from_current(cls, exprinfo=None):
if exprinfo is None and isinstance(tup[1], AssertionError): """returns an ExceptionInfo matching the current traceback
exprinfo = getattr(tup[1], "msg", None)
if exprinfo is None: .. warning::
exprinfo = py.io.saferepr(tup[1])
if exprinfo and exprinfo.startswith(self._assert_start_repr): Experimental API
self._striptext = "AssertionError: "
self._excinfo = tup
#: the exception class :param exprinfo: a text string helping to determine if we should
self.type = tup[0] strip ``AssertionError`` from the output, defaults
#: the exception instance to the exception message/``__str__()``
self.value = tup[1] """
#: the exception raw traceback tup = sys.exc_info()
self.tb = tup[2] _striptext = ""
#: the exception type name if exprinfo is None and isinstance(tup[1], AssertionError):
self.typename = self.type.__name__ exprinfo = getattr(tup[1], "msg", None)
#: the exception traceback (_pytest._code.Traceback instance) if exprinfo is None:
self.traceback = _pytest._code.Traceback(self.tb, excinfo=ref(self)) 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): def __repr__(self):
if self._excinfo is None:
return "<ExceptionInfo for raises contextmanager>"
return "<ExceptionInfo %s tblen=%d>" % (self.typename, len(self.traceback)) return "<ExceptionInfo %s tblen=%d>" % (self.typename, len(self.traceback))
def exconly(self, tryshort=False): def exconly(self, tryshort=False):
@ -513,6 +557,8 @@ class ExceptionInfo(object):
return fmt.repr_excinfo(self) return fmt.repr_excinfo(self)
def __str__(self): def __str__(self):
if self._excinfo is None:
return repr(self)
entry = self.traceback[-1] entry = self.traceback[-1]
loc = ReprFileLocation(entry.path, entry.lineno + 1, self.exconly()) loc = ReprFileLocation(entry.path, entry.lineno + 1, self.exconly())
return str(loc) return str(loc)

View File

@ -164,7 +164,7 @@ def assertrepr_compare(config, op, left, right):
explanation = [ explanation = [
u"(pytest_assertion plugin: representation of details failed. " u"(pytest_assertion plugin: representation of details failed. "
u"Probably an object has a faulty __repr__.)", 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: if not explanation:

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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