Merge pull request #3780 from nicoddemus/mock-integration-fix
Fix issue where fixtures would lose the decorated functionality
This commit is contained in:
commit
4d8903fd0b
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -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*")
|
||||||
|
|
|
@ -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"
|
|
@ -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+"
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue