Fix issue where fixtures would lose the decorated functionality

Fix #3774
This commit is contained in:
Bruno Oliveira 2018-08-04 15:14:00 -03:00
parent a76cc8f8c4
commit ef8ec01e39
5 changed files with 67 additions and 3 deletions

View File

@ -234,6 +234,13 @@ def get_real_func(obj):
""" """
start_obj = obj start_obj = obj
for i in range(100): 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) new_obj = getattr(obj, "__wrapped__", None)
if new_obj is None: if new_obj is None:
break break

View File

@ -954,9 +954,6 @@ def _ensure_immutable_ids(ids):
def wrap_function_to_warning_if_called_directly(function, fixture_marker): 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 """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. 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) is_yield_function = is_generator(function)
msg = FIXTURE_FUNCTION_CALL.format(name=fixture_marker.name or function.__name__) 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: if six.PY2:
result.__wrapped__ = function 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 return result

View File

@ -1044,3 +1044,10 @@ def test_frame_leak_on_failing_test(testdir):
) )
result = testdir.runpytest_subprocess() result = testdir.runpytest_subprocess()
result.stdout.fnmatch_lines(["*1 failed, 1 passed in*"]) 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*")

View File

@ -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"

View File

@ -1,5 +1,8 @@
from __future__ import absolute_import, division, print_function from __future__ import absolute_import, division, print_function
import sys import sys
from functools import wraps
import six
import pytest import pytest
from _pytest.compat import is_generator, get_real_func, safe_getattr from _pytest.compat import is_generator, get_real_func, safe_getattr
@ -26,6 +29,8 @@ def test_real_func_loop_limit():
return "<Evil left={left}>".format(left=self.left) return "<Evil left={left}>".format(left=self.left)
def __getattr__(self, attr): def __getattr__(self, attr):
if attr == "__pytest_wrapped__":
raise AttributeError
if not self.left: if not self.left:
raise RuntimeError("its over") raise RuntimeError("its over")
self.left -= 1 self.left -= 1
@ -38,6 +43,33 @@ def test_real_func_loop_limit():
print(res) 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( @pytest.mark.skipif(
sys.version_info < (3, 4), reason="asyncio available in Python 3.4+" sys.version_info < (3, 4), reason="asyncio available in Python 3.4+"
) )