diff --git a/src/_pytest/compat.py b/src/_pytest/compat.py index 52051ff23..57ad4fdd8 100644 --- a/src/_pytest/compat.py +++ b/src/_pytest/compat.py @@ -234,6 +234,13 @@ def get_real_func(obj): """ start_obj = obj for i in range(100): + # __pytest_wrapped__ is set by @pytest.fixture when wrapping the fixture function + # to trigger a warning if it gets called directly instead of by pytest: we don't + # want to unwrap further than this otherwise we lose useful wrappings like @mock.patch (#3774) + new_obj = getattr(obj, "__pytest_wrapped__", None) + if new_obj is not None: + obj = new_obj + break new_obj = getattr(obj, "__wrapped__", None) if new_obj is None: break diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 818c5b81f..0d63b151f 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -954,9 +954,6 @@ def _ensure_immutable_ids(ids): def wrap_function_to_warning_if_called_directly(function, fixture_marker): """Wrap the given fixture function so we can issue warnings about it being called directly, instead of used as an argument in a test function. - - The warning is emitted only in Python 3, because I didn't find a reliable way to make the wrapper function - keep the original signature, and we probably will drop Python 2 in Pytest 4 anyway. """ is_yield_function = is_generator(function) msg = FIXTURE_FUNCTION_CALL.format(name=fixture_marker.name or function.__name__) @@ -982,6 +979,10 @@ def wrap_function_to_warning_if_called_directly(function, fixture_marker): if six.PY2: result.__wrapped__ = function + # keep reference to the original function in our own custom attribute so we don't unwrap + # further than this point and lose useful wrappings like @mock.patch (#3774) + result.__pytest_wrapped__ = function + return result diff --git a/testing/acceptance_test.py b/testing/acceptance_test.py index 6f12cc335..bc4e3bed8 100644 --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -1044,3 +1044,10 @@ def test_frame_leak_on_failing_test(testdir): ) result = testdir.runpytest_subprocess() result.stdout.fnmatch_lines(["*1 failed, 1 passed in*"]) + + +def test_fixture_mock_integration(testdir): + """Test that decorators applied to fixture are left working (#3774)""" + p = testdir.copy_example("acceptance/fixture_mock_integration.py") + result = testdir.runpytest(p) + result.stdout.fnmatch_lines("*1 passed*") diff --git a/testing/example_scripts/acceptance/fixture_mock_integration.py b/testing/example_scripts/acceptance/fixture_mock_integration.py new file mode 100644 index 000000000..51f46f82c --- /dev/null +++ b/testing/example_scripts/acceptance/fixture_mock_integration.py @@ -0,0 +1,17 @@ +"""Reproduces issue #3774""" + +import mock + +import pytest + +config = {"mykey": "ORIGINAL"} + + +@pytest.fixture(scope="function") +@mock.patch.dict(config, {"mykey": "MOCKED"}) +def my_fixture(): + return config["mykey"] + + +def test_foobar(my_fixture): + assert my_fixture == "MOCKED" diff --git a/testing/test_compat.py b/testing/test_compat.py index 399b0d342..0663fa149 100644 --- a/testing/test_compat.py +++ b/testing/test_compat.py @@ -1,5 +1,8 @@ from __future__ import absolute_import, division, print_function import sys +from functools import wraps + +import six import pytest from _pytest.compat import is_generator, get_real_func, safe_getattr @@ -26,6 +29,8 @@ def test_real_func_loop_limit(): return "".format(left=self.left) def __getattr__(self, attr): + if attr == "__pytest_wrapped__": + raise AttributeError if not self.left: raise RuntimeError("its over") self.left -= 1 @@ -38,6 +43,33 @@ def test_real_func_loop_limit(): print(res) +def test_get_real_func(): + """Check that get_real_func correctly unwraps decorators until reaching the real function""" + + def decorator(f): + @wraps(f) + def inner(): + pass + + if six.PY2: + inner.__wrapped__ = f + return inner + + def func(): + pass + + wrapped_func = decorator(decorator(func)) + assert get_real_func(wrapped_func) is func + + wrapped_func2 = decorator(decorator(wrapped_func)) + assert get_real_func(wrapped_func2) is func + + # special case for __pytest_wrapped__ attribute: used to obtain the function up until the point + # a function was wrapped by pytest itself + wrapped_func2.__pytest_wrapped__ = wrapped_func + assert get_real_func(wrapped_func2) is wrapped_func + + @pytest.mark.skipif( sys.version_info < (3, 4), reason="asyncio available in Python 3.4+" )