From 51cef393accd911315864f89cc8f8d4f85742aeb Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Sat, 15 Jun 2019 18:54:03 +0200 Subject: [PATCH] Merge pull request #5404 from Zac-HD/helpful-mock-unwrapper Emit warning for broken object --- changelog/5404.bugfix.rst | 2 ++ src/_pytest/doctest.py | 16 +++++++++++++--- testing/test_doctest.py | 25 +++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 changelog/5404.bugfix.rst 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 95a80bb4fa2..659d24aeebc 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -8,6 +8,7 @@ import platform import sys import traceback +import warnings from contextlib import contextmanager import pytest @@ -17,6 +18,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" @@ -374,10 +376,18 @@ 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: + try: + if stop is None or stop is _is_mocked: + return real_unwrap(obj, stop=_is_mocked) 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 25a35c3c136..04e6e0c74a1 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -3,11 +3,14 @@ from __future__ import division from __future__ import print_function +import inspect import sys import textwrap import pytest from _pytest.compat import MODULE_NOT_FOUND_ERROR +from _pytest.doctest import _is_mocked +from _pytest.doctest import _patch_unwrap_mock_aware from _pytest.doctest import DoctestItem from _pytest.doctest import DoctestModule from _pytest.doctest import DoctestTextfile @@ -1237,3 +1240,25 @@ 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.skipif(not hasattr(inspect, "unwrap")) +@pytest.mark.parametrize( # pragma: no branch (lambdas are not called) + "stop", [None, _is_mocked, lambda f: None, lambda f: False, lambda f: True] +) +def test_warning_on_unwrap_of_broken_object(stop): + bad_instance = Broken() + assert inspect.unwrap.__module__ == "inspect" + with _patch_unwrap_mock_aware(): + assert inspect.unwrap.__module__ != "inspect" + with pytest.warns( + pytest.PytestWarning, match="^Got KeyError.* when unwrapping" + ): + with pytest.raises(KeyError): + inspect.unwrap(bad_instance, stop=stop) + assert inspect.unwrap.__module__ == "inspect"