Skip to content

Commit

Permalink
Fix issue where fixtures would lose the decorated functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
nicoddemus committed Aug 4, 2018
1 parent a76cc8f commit ef8ec01
Show file tree
Hide file tree
Showing 5 changed files with 67 additions and 3 deletions.
7 changes: 7 additions & 0 deletions src/_pytest/compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 4 additions & 3 deletions src/_pytest/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__)
Expand All @@ -982,6 +979,10 @@ def result(*args, **kwargs):
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


Expand Down
7 changes: 7 additions & 0 deletions testing/acceptance_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -1044,3 +1044,10 @@ def test2():
)
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*")
17 changes: 17 additions & 0 deletions testing/example_scripts/acceptance/fixture_mock_integration.py
Original file line number Diff line number Diff line change
@@ -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"
32 changes: 32 additions & 0 deletions testing/test_compat.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -26,6 +29,8 @@ def __repr__(self):
return "<Evil left={left}>".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
Expand All @@ -38,6 +43,33 @@ def __getattr__(self, attr):
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+"
)
Expand Down

0 comments on commit ef8ec01

Please sign in to comment.