Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix #4386 - restructure construction and partial state of ExceptionInfo #4438

Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog/4386.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Restructure ExceptionInfo object construction and ensure incomplete instances have a ``repr``/``str``.
91 changes: 69 additions & 22 deletions src/_pytest/_code/code.py
Original file line number Diff line number Diff line change
Expand Up @@ -391,40 +391,85 @@ 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):
import _pytest._code
_excinfo = attr.ib()
_striptext = attr.ib(default="")
_traceback = attr.ib(default=None)

@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

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))
@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):
Expand Down Expand Up @@ -513,6 +558,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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
6 changes: 5 additions & 1 deletion src/_pytest/unittest.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ def _addexcinfo(self, rawexcinfo):
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:
Expand All @@ -136,7 +140,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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Loading