From eb7a4e32ad920b4cdd9c956763535fed194ae8a7 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 23 Oct 2019 06:59:18 +0200 Subject: [PATCH 1/2] saferepr: handle BaseExceptions This causes INTERNALERRORs with pytest-django, which uses `pytest.fail` (derived from `BaseException`) to prevent DB access, when pytest then tries to e.g. display the `repr()` for a Django `QuerySet` etc. Ref: https://github.com/pytest-dev/pytest-django/pull/776 --- changelog/6047.bugfix.rst | 1 + src/_pytest/_io/saferepr.py | 32 ++++++++++----- testing/code/test_excinfo.py | 4 +- testing/io/test_saferepr.py | 78 ++++++++++++++++++++++++++++++++++-- testing/test_session.py | 18 ++++----- 5 files changed, 110 insertions(+), 23 deletions(-) create mode 100644 changelog/6047.bugfix.rst diff --git a/changelog/6047.bugfix.rst b/changelog/6047.bugfix.rst new file mode 100644 index 00000000000..11a997f713a --- /dev/null +++ b/changelog/6047.bugfix.rst @@ -0,0 +1 @@ +BaseExceptions are handled in ``saferepr``, which includes ``pytest.fail.Exception`` etc. diff --git a/src/_pytest/_io/saferepr.py b/src/_pytest/_io/saferepr.py index 908fd2183cf..7fded872def 100644 --- a/src/_pytest/_io/saferepr.py +++ b/src/_pytest/_io/saferepr.py @@ -3,14 +3,24 @@ from typing import Any -def _format_repr_exception(exc: Exception, obj: Any) -> str: - exc_name = type(exc).__name__ +def _try_repr_or_str(obj): try: - exc_info = str(exc) - except Exception: - exc_info = "unknown" - return '<[{}("{}") raised in repr()] {} object at 0x{:x}>'.format( - exc_name, exc_info, obj.__class__.__name__, id(obj) + return repr(obj) + except (KeyboardInterrupt, SystemExit): + raise + except BaseException: + return '{}("{}")'.format(type(obj).__name__, obj) + + +def _format_repr_exception(exc: BaseException, obj: Any) -> str: + try: + exc_info = _try_repr_or_str(exc) + except (KeyboardInterrupt, SystemExit): + raise + except BaseException as exc: + exc_info = "unpresentable exception ({})".format(_try_repr_or_str(exc)) + return "<[{} raised in repr()] {} object at 0x{:x}>".format( + exc_info, obj.__class__.__name__, id(obj) ) @@ -35,14 +45,18 @@ def __init__(self, maxsize: int) -> None: def repr(self, x: Any) -> str: try: s = super().repr(x) - except Exception as exc: + except (KeyboardInterrupt, SystemExit): + raise + except BaseException as exc: s = _format_repr_exception(exc, x) return _ellipsize(s, self.maxsize) def repr_instance(self, x: Any, level: int) -> str: try: s = repr(x) - except Exception as exc: + except (KeyboardInterrupt, SystemExit): + raise + except BaseException as exc: s = _format_repr_exception(exc, x) return _ellipsize(s, self.maxsize) diff --git a/testing/code/test_excinfo.py b/testing/code/test_excinfo.py index b431bb66dfa..262d1d18422 100644 --- a/testing/code/test_excinfo.py +++ b/testing/code/test_excinfo.py @@ -584,7 +584,7 @@ def __repr__(self): reprlocals = p.repr_locals(loc) assert reprlocals.lines assert reprlocals.lines[0] == "__builtins__ = " - assert '[NotImplementedError("") raised in repr()]' in reprlocals.lines[1] + assert "[NotImplementedError() raised in repr()]" in reprlocals.lines[1] def test_repr_local_with_exception_in_class_property(self): class ExceptionWithBrokenClass(Exception): @@ -602,7 +602,7 @@ def __repr__(self): reprlocals = p.repr_locals(loc) assert reprlocals.lines assert reprlocals.lines[0] == "__builtins__ = " - assert '[ExceptionWithBrokenClass("") raised in repr()]' in reprlocals.lines[1] + assert "[ExceptionWithBrokenClass() raised in repr()]" in reprlocals.lines[1] def test_repr_local_truncated(self): loc = {"l": [i for i in range(10)]} diff --git a/testing/io/test_saferepr.py b/testing/io/test_saferepr.py index 86897b57c2f..db86ea4d586 100644 --- a/testing/io/test_saferepr.py +++ b/testing/io/test_saferepr.py @@ -1,3 +1,4 @@ +import pytest from _pytest._io.saferepr import saferepr @@ -40,9 +41,80 @@ class BrokenReprException(Exception): assert "TypeError" in s assert "TypeError" in saferepr(BrokenRepr("string")) - s2 = saferepr(BrokenRepr(BrokenReprException("omg even worse"))) - assert "NameError" not in s2 - assert "unknown" in s2 + try: + None() + except Exception as exc: + exp_exc = repr(exc) + obj = BrokenRepr(BrokenReprException("omg even worse")) + s2 = saferepr(obj) + assert s2 == ( + "<[unpresentable exception ({!s}) raised in repr()] BrokenRepr object at 0x{:x}>".format( + exp_exc, id(obj) + ) + ) + + +def test_baseexception(): + """Test saferepr() with BaseExceptions, which includes pytest outcomes.""" + + class RaisingOnStrRepr(BaseException): + def __init__(self, exc_types): + self.exc_types = exc_types + + def raise_exc(self, *args): + try: + self.exc_type = self.exc_types.pop(0) + except IndexError: + pass + if hasattr(self.exc_type, "__call__"): + raise self.exc_type(*args) + raise self.exc_type + + def __str__(self): + self.raise_exc("__str__") + + def __repr__(self): + self.raise_exc("__repr__") + + class BrokenObj: + def __init__(self, exc): + self.exc = exc + + def __repr__(self): + raise self.exc + + __str__ = __repr__ + + baseexc_str = BaseException("__str__") + obj = BrokenObj(RaisingOnStrRepr([BaseException])) + assert saferepr(obj) == ( + "<[unpresentable exception ({!r}) " + "raised in repr()] BrokenObj object at 0x{:x}>".format(baseexc_str, id(obj)) + ) + obj = BrokenObj(RaisingOnStrRepr([RaisingOnStrRepr([BaseException])])) + assert saferepr(obj) == ( + "<[{!r} raised in repr()] BrokenObj object at 0x{:x}>".format( + baseexc_str, id(obj) + ) + ) + + with pytest.raises(KeyboardInterrupt): + saferepr(BrokenObj(KeyboardInterrupt())) + + with pytest.raises(SystemExit): + saferepr(BrokenObj(SystemExit())) + + with pytest.raises(KeyboardInterrupt): + saferepr(BrokenObj(RaisingOnStrRepr([KeyboardInterrupt]))) + + with pytest.raises(SystemExit): + saferepr(BrokenObj(RaisingOnStrRepr([SystemExit]))) + + with pytest.raises(KeyboardInterrupt): + print(saferepr(BrokenObj(RaisingOnStrRepr([BaseException, KeyboardInterrupt])))) + + with pytest.raises(SystemExit): + saferepr(BrokenObj(RaisingOnStrRepr([BaseException, SystemExit]))) def test_buggy_builtin_repr(): diff --git a/testing/test_session.py b/testing/test_session.py index dbe0573760b..7b4eb817a14 100644 --- a/testing/test_session.py +++ b/testing/test_session.py @@ -102,15 +102,20 @@ def test_broken_repr(self, testdir): p = testdir.makepyfile( """ import pytest + + class reprexc(BaseException): + def __str__(self): + return "Ha Ha fooled you, I'm a broken repr()." + class BrokenRepr1(object): foo=0 def __repr__(self): - raise Exception("Ha Ha fooled you, I'm a broken repr().") + raise reprexc class TestBrokenClass(object): def test_explicit_bad_repr(self): t = BrokenRepr1() - with pytest.raises(Exception, match="I'm a broken repr"): + with pytest.raises(BaseException, match="broken repr"): repr(t) def test_implicit_bad_repr1(self): @@ -123,12 +128,7 @@ def test_implicit_bad_repr1(self): passed, skipped, failed = reprec.listoutcomes() assert (len(passed), len(skipped), len(failed)) == (1, 0, 1) out = failed[0].longrepr.reprcrash.message - assert ( - out.find( - """[Exception("Ha Ha fooled you, I'm a broken repr().") raised in repr()]""" - ) - != -1 - ) + assert out.find("<[reprexc() raised in repr()] BrokenRepr1") != -1 def test_broken_repr_with_showlocals_verbose(self, testdir): p = testdir.makepyfile( @@ -151,7 +151,7 @@ def test_repr_error(): assert repr_locals.lines assert len(repr_locals.lines) == 1 assert repr_locals.lines[0].startswith( - 'x = <[NotImplementedError("") raised in repr()] ObjWithErrorInRepr' + "x = <[NotImplementedError() raised in repr()] ObjWithErrorInRepr" ) def test_skip_file_by_conftest(self, testdir): From fee7c7b032b5995339375e8cfbaf8f9832aeb512 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Wed, 6 Nov 2019 14:10:20 +0100 Subject: [PATCH 2/2] py38: do not call None() directly MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Works around: _____ ERROR collecting testing/io/test_saferepr.py _____ src/_pytest/python.py:502: in _importtestmodule mod = self.fspath.pyimport(ensuresyspath=importmode) .venv38/lib/python3.8/site-packages/py/_path/local.py:701: in pyimport __import__(modname) :991: in _find_and_load ??? :975: in _find_and_load_unlocked ??? :671: in _load_unlocked ??? src/_pytest/assertion/rewrite.py:136: in exec_module source_stat, co = _rewrite_test(fn, self.config) src/_pytest/assertion/rewrite.py:288: in _rewrite_test co = compile(tree, fn, "exec", dont_inherit=True) E File "…/Vcs/pytest/testing/io/test_saferepr.py", line 45 E None() E ^ E SyntaxError: 'NoneType' object is not callable; perhaps you missed a comma? --- testing/io/test_saferepr.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/testing/io/test_saferepr.py b/testing/io/test_saferepr.py index db86ea4d586..e24d9b470b5 100644 --- a/testing/io/test_saferepr.py +++ b/testing/io/test_saferepr.py @@ -41,9 +41,10 @@ class BrokenReprException(Exception): assert "TypeError" in s assert "TypeError" in saferepr(BrokenRepr("string")) + none = None try: - None() - except Exception as exc: + none() + except BaseException as exc: exp_exc = repr(exc) obj = BrokenRepr(BrokenReprException("omg even worse")) s2 = saferepr(obj)