Skip to content

Commit

Permalink
Merge pull request #3780 from nicoddemus/mock-integration-fix
Browse files Browse the repository at this point in the history
Fix issue where fixtures would lose the decorated functionality
  • Loading branch information
nicoddemus committed Aug 9, 2018
2 parents 5d3c512 + 67106f0 commit 4d8903f
Show file tree
Hide file tree
Showing 5 changed files with 79 additions and 4 deletions.
19 changes: 19 additions & 0 deletions src/_pytest/compat.py
Expand Up @@ -228,12 +228,31 @@ def ascii_escaped(val):
return val.encode("unicode-escape")


class _PytestWrapper(object):
"""Dummy wrapper around a function object for internal use only.
Used to correctly unwrap the underlying function object
when we are creating fixtures, because we wrap the function object ourselves with a decorator
to issue warnings when the fixture function is called directly.
"""

def __init__(self, obj):
self.obj = obj


def get_real_func(obj):
""" gets the real function object of the (possibly) wrapped object by
functools.wraps or functools.partial.
"""
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 isinstance(new_obj, _PytestWrapper):
obj = new_obj.obj
break
new_obj = getattr(obj, "__wrapped__", None)
if new_obj is None:
break
Expand Down
8 changes: 5 additions & 3 deletions src/_pytest/fixtures.py
Expand Up @@ -31,6 +31,7 @@
safe_getattr,
FuncargnamesCompatAttr,
get_real_method,
_PytestWrapper,
)
from _pytest.deprecated import FIXTURE_FUNCTION_CALL, RemovedInPytest4Warning
from _pytest.outcomes import fail, TEST_OUTCOME
Expand Down Expand Up @@ -954,9 +955,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 +980,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__ = _PytestWrapper(function)

return result


Expand Down
7 changes: 7 additions & 0 deletions testing/acceptance_test.py
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
@@ -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: 31 additions & 1 deletion testing/test_compat.py
@@ -1,8 +1,11 @@
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
from _pytest.compat import is_generator, get_real_func, safe_getattr, _PytestWrapper
from _pytest.outcomes import OutcomeException


Expand Down Expand Up @@ -38,6 +41,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__ = _PytestWrapper(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 4d8903f

Please sign in to comment.