From 1a27866edbcdfebf3918cb271be8597744c54316 Mon Sep 17 00:00:00 2001 From: Ronny Pfannschmidt Date: Thu, 22 Nov 2018 12:20:14 +0100 Subject: [PATCH] fix #4386 - restructure construction and partial state of ExceptionInfo --- src/_pytest/_code/code.py | 69 ++++++++++++++++++++++++++++++----- src/_pytest/assertion/util.py | 2 +- src/_pytest/main.py | 4 +- src/_pytest/python.py | 4 +- src/_pytest/python_api.py | 6 +-- src/_pytest/runner.py | 4 +- src/_pytest/unittest.py | 3 +- testing/code/test_code.py | 4 +- testing/code/test_excinfo.py | 28 ++++++++------ testing/test_resultlog.py | 2 +- testing/test_runner.py | 20 ++++------ 11 files changed, 100 insertions(+), 46 deletions(-) diff --git a/src/_pytest/_code/code.py b/src/_pytest/_code/code.py index d06e24f006c..5331374ee1b 100644 --- a/src/_pytest/_code/code.py +++ b/src/_pytest/_code/code.py @@ -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: @@ -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 "" return "" % (self.typename, len(self.traceback)) def exconly(self, tryshort=False): @@ -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) diff --git a/src/_pytest/assertion/util.py b/src/_pytest/assertion/util.py index 451e454952b..15561f2935f 100644 --- a/src/_pytest/assertion/util.py +++ b/src/_pytest/assertion/util.py @@ -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: diff --git a/src/_pytest/main.py b/src/_pytest/main.py index df4f1c8fbf5..851b08ae39c 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -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): diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 8912ca060a4..8c8de8e752d 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -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] @@ -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 = ( diff --git a/src/_pytest/python_api.py b/src/_pytest/python_api.py index 805cd85ad41..f895fb8a8e8 100644 --- a/src/_pytest/python_api.py +++ b/src/_pytest/python_api.py @@ -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 @@ 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): diff --git a/src/_pytest/runner.py b/src/_pytest/runner.py index 86298a7aa3f..9ea1a07cd5d 100644 --- a/src/_pytest/runner.py +++ b/src/_pytest/runner.py @@ -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): diff --git a/src/_pytest/unittest.py b/src/_pytest/unittest.py index a38a60d8e68..645fec554fd 100644 --- a/src/_pytest/unittest.py +++ b/src/_pytest/unittest.py @@ -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: @@ -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): diff --git a/testing/code/test_code.py b/testing/code/test_code.py index 20ca0bfce1f..df9f109ef5c 100644 --- a/testing/code/test_code.py +++ b/testing/code/test_code.py @@ -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() @@ -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 diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index c8f4c904d37..b4d64313c60 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -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 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, @@ -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 @@ -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:") @@ -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 '':1 in \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 @@ 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): @@ -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: @@ -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: @@ -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 diff --git a/testing/test_resultlog.py b/testing/test_resultlog.py index 36f584e573d..cb7b0cd3ce7 100644 --- a/testing/test_resultlog.py +++ b/testing/test_resultlog.py @@ -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() diff --git a/testing/test_runner.py b/testing/test_runner.py index c081920a502..2d047af70b8 100644 --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -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 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") @@ -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")