Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
fix pytest-dev#4386 - restructure construction and partial state of E…
…xceptionInfo
  • Loading branch information
RonnyPfannschmidt committed Nov 22, 2018
1 parent 6e85feb commit 1a27866
Show file tree
Hide file tree
Showing 11 changed files with 100 additions and 46 deletions.
69 changes: 60 additions & 9 deletions src/_pytest/_code/code.py
Expand Up @@ -391,17 +391,70 @@ def recursionindex(self):
)


@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):
_excinfo = attr.ib()
_striptext = attr.ib(default="")
_traceback = attr.ib(default=None)

@classmethod
def from_current(cls, exprinfo=None):
"""UNSTABLE API
"""
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 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):
return self.type.__name__

@property
def traceback(self):
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 other(self, tup=None, exprinfo=None):
import _pytest._code

if tup is None:
Expand All @@ -412,19 +465,15 @@ def __init__(self, tup=None, exprinfo=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))

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):
Expand Down Expand Up @@ -513,6 +562,8 @@ def getrepr(
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)
Expand Down
2 changes: 1 addition & 1 deletion src/_pytest/assertion/util.py
Expand Up @@ -155,7 +155,7 @@ def isiterable(obj):
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:
Expand Down
4 changes: 2 additions & 2 deletions src/_pytest/main.py
Expand Up @@ -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))
Expand All @@ -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):
Expand Down
4 changes: 2 additions & 2 deletions src/_pytest/python.py
Expand Up @@ -450,7 +450,7 @@ def _importtestmodule(self):
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]
Expand All @@ -466,7 +466,7 @@ def _importtestmodule(self):
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 = (
Expand Down
6 changes: 3 additions & 3 deletions src/_pytest/python_api.py
Expand Up @@ -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)


Expand All @@ -705,7 +705,7 @@ def __init__(self, expected_exception, message, match_expr):
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):
Expand Down
4 changes: 2 additions & 2 deletions src/_pytest/runner.py
Expand Up @@ -211,12 +211,12 @@ def __init__(self, func, when, treat_keyboard_interrupt_as_exception=False):
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):
Expand Down
3 changes: 2 additions & 1 deletion src/_pytest/unittest.py
Expand Up @@ -115,6 +115,7 @@ def _addexcinfo(self, rawexcinfo):
rawexcinfo = getattr(rawexcinfo, "_rawexcinfo", rawexcinfo)
try:
excinfo = _pytest._code.ExceptionInfo(rawexcinfo)
getattr(excinfo, "value")
except TypeError:
try:
try:
Expand All @@ -136,7 +137,7 @@ def _addexcinfo(self, rawexcinfo):
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):
Expand Down
4 changes: 2 additions & 2 deletions testing/code/test_code.py
Expand Up @@ -169,7 +169,7 @@ def test_bad_getsource(self):
else:
assert False
except AssertionError:
exci = _pytest._code.ExceptionInfo()
exci = _pytest._code.ExceptionInfo.from_current()
assert exci.getrepr()


Expand All @@ -181,7 +181,7 @@ def test_getsource(self):
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
Expand Down
28 changes: 17 additions & 11 deletions testing/code/test_excinfo.py
Expand Up @@ -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


Expand All @@ -85,7 +85,7 @@ def f():
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,
Expand Down Expand Up @@ -126,7 +126,7 @@ def setup_method(self, method):
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
Expand Down Expand Up @@ -163,7 +163,7 @@ def xyz():
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:")
Expand Down Expand Up @@ -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)
Expand All @@ -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"

Expand All @@ -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
Expand All @@ -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)
Expand Down Expand Up @@ -453,7 +459,7 @@ def excinfo_from_exec(self, source):
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):
Expand Down Expand Up @@ -491,7 +497,7 @@ def test_repr_source_not_existing(self):
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:
Expand All @@ -510,7 +516,7 @@ def test_repr_many_line_source_not_existing(self):
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:
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion testing/test_resultlog.py
Expand Up @@ -151,7 +151,7 @@ def test_internal_exception(self, style):
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()
Expand Down
20 changes: 8 additions & 12 deletions testing/test_runner.py
Expand Up @@ -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):
Expand Down Expand Up @@ -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")

Expand Down Expand Up @@ -718,7 +714,7 @@ def f():
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")


Expand All @@ -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")


Expand Down

0 comments on commit 1a27866

Please sign in to comment.