diff --git a/changelog/5404.bugfix.rst b/changelog/5404.bugfix.rst new file mode 100644 index 00000000000..2187bed8b32 --- /dev/null +++ b/changelog/5404.bugfix.rst @@ -0,0 +1,2 @@ +Emit a warning when attempting to unwrap a broken object raises an exception, +for easier debugging (`#5080 `__). diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index afb7ede4cc7..f40b59635e9 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -3,6 +3,7 @@ import platform import sys import traceback +import warnings from contextlib import contextmanager import pytest @@ -12,6 +13,7 @@ from _pytest.compat import safe_getattr from _pytest.fixtures import FixtureRequest from _pytest.outcomes import Skipped +from _pytest.warning_types import PytestWarning DOCTEST_REPORT_CHOICE_NONE = "none" DOCTEST_REPORT_CHOICE_CDIFF = "cdiff" @@ -368,10 +370,21 @@ def _patch_unwrap_mock_aware(): else: def _mock_aware_unwrap(obj, stop=None): - if stop is None: - return real_unwrap(obj, stop=_is_mocked) - else: - return real_unwrap(obj, stop=lambda obj: _is_mocked(obj) or stop(obj)) + try: + if stop is None or stop is _is_mocked: + return real_unwrap(obj, stop=_is_mocked) + else: + return real_unwrap( + obj, stop=lambda obj: _is_mocked(obj) or stop(obj) + ) + except Exception as e: + warnings.warn( + "Got %r when unwrapping %r. This is usually caused " + "by a violation of Python's object protocol; see e.g. " + "https://github.com/pytest-dev/pytest/issues/5080" % (e, obj), + PytestWarning, + ) + raise inspect.unwrap = _mock_aware_unwrap try: diff --git a/testing/test_doctest.py b/testing/test_doctest.py index bf0405546bf..2c66bf6fe6a 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -1,7 +1,9 @@ +import inspect import textwrap import pytest from _pytest.compat import MODULE_NOT_FOUND_ERROR +from _pytest.doctest import _patch_unwrap_mock_aware from _pytest.doctest import DoctestItem from _pytest.doctest import DoctestModule from _pytest.doctest import DoctestTextfile @@ -1224,3 +1226,19 @@ class Example(object): ) result = testdir.runpytest("--doctest-modules") result.stdout.fnmatch_lines(["* 1 passed *"]) + + +class Broken: + def __getattr__(self, _): + raise KeyError("This should be an AttributeError") + + +@pytest.mark.parametrize( + "stop", [None, lambda f: None, lambda f: False, lambda f: True] +) +def test_warning_on_unwrap_of_broken_object(stop): + bad_instance = Broken() + with _patch_unwrap_mock_aware(): + with pytest.raises(KeyError): + with pytest.warns(pytest.PytestWarning): + inspect.unwrap(bad_instance, stop=stop)