Merge pull request #3780 from nicoddemus/mock-integration-fix

Fix issue where fixtures would lose the decorated functionality
This commit is contained in:
Bruno Oliveira 2018-08-09 12:26:09 -03:00 committed by GitHub
commit 4d8903fd0b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 79 additions and 4 deletions

View File

@ -228,12 +228,31 @@ else:
return val.encode("unicode-escape") 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): def get_real_func(obj):
""" gets the real function object of the (possibly) wrapped object by """ gets the real function object of the (possibly) wrapped object by
functools.wraps or functools.partial. functools.wraps or functools.partial.
""" """
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 isinstance(new_obj, _PytestWrapper):
obj = new_obj.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

@ -31,6 +31,7 @@ from _pytest.compat import (
safe_getattr, safe_getattr,
FuncargnamesCompatAttr, FuncargnamesCompatAttr,
get_real_method, get_real_method,
_PytestWrapper,
) )
from _pytest.deprecated import FIXTURE_FUNCTION_CALL, RemovedInPytest4Warning from _pytest.deprecated import FIXTURE_FUNCTION_CALL, RemovedInPytest4Warning
from _pytest.outcomes import fail, TEST_OUTCOME from _pytest.outcomes import fail, TEST_OUTCOME
@ -954,9 +955,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 +980,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__ = _PytestWrapper(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,8 +1,11 @@
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, _PytestWrapper
from _pytest.outcomes import OutcomeException from _pytest.outcomes import OutcomeException
@ -38,6 +41,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__ = _PytestWrapper(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+"
) )