Merge pull request #3754 from nicoddemus/fix-function-call-warning

Refactor direct fixture call warning to avoid incompatibility with plugins
This commit is contained in:
Ronny Pfannschmidt 2018-08-02 07:17:15 +02:00 committed by GitHub
commit 33769d0328
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 32 additions and 19 deletions

View File

@ -0,0 +1 @@
Fix compatibility problem with plugins and the warning code issued by fixture functions when they are called directly.

View File

@ -249,6 +249,21 @@ def get_real_func(obj):
return obj return obj
def get_real_method(obj, holder):
"""
Attempts to obtain the real function object that might be wrapping ``obj``, while at the same time
returning a bound method to ``holder`` if the original object was a bound method.
"""
try:
is_method = hasattr(obj, "__func__")
obj = get_real_func(obj)
except Exception:
return obj
if is_method and hasattr(obj, "__get__") and callable(obj.__get__):
obj = obj.__get__(holder)
return obj
def getfslineno(obj): def getfslineno(obj):
# xxx let decorators etc specify a sane ordering # xxx let decorators etc specify a sane ordering
obj = get_real_func(obj) obj = get_real_func(obj)

View File

@ -30,6 +30,7 @@ from _pytest.compat import (
getfuncargnames, getfuncargnames,
safe_getattr, safe_getattr,
FuncargnamesCompatAttr, FuncargnamesCompatAttr,
get_real_method,
) )
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
@ -931,13 +932,6 @@ def pytest_fixture_setup(fixturedef, request):
request._check_scope(argname, request.scope, fixdef.scope) request._check_scope(argname, request.scope, fixdef.scope)
kwargs[argname] = result kwargs[argname] = result
# if function has been defined with @pytest.fixture, we want to
# pass the special __being_called_by_pytest parameter so we don't raise a warning
# this is an ugly hack, see #3720 for an opportunity to improve this
defined_using_fixture_decorator = hasattr(fixturedef.func, "_pytestfixturefunction")
if defined_using_fixture_decorator:
kwargs["__being_called_by_pytest"] = True
fixturefunc = resolve_fixture_function(fixturedef, request) fixturefunc = resolve_fixture_function(fixturedef, request)
my_cache_key = request.param_index my_cache_key = request.param_index
try: try:
@ -973,9 +967,7 @@ def wrap_function_to_warning_if_called_directly(function, fixture_marker):
@functools.wraps(function) @functools.wraps(function)
def result(*args, **kwargs): def result(*args, **kwargs):
__tracebackhide__ = True __tracebackhide__ = True
__being_called_by_pytest = kwargs.pop("__being_called_by_pytest", False) warnings.warn(warning, stacklevel=3)
if not __being_called_by_pytest:
warnings.warn(warning, stacklevel=3)
for x in function(*args, **kwargs): for x in function(*args, **kwargs):
yield x yield x
@ -984,9 +976,7 @@ def wrap_function_to_warning_if_called_directly(function, fixture_marker):
@functools.wraps(function) @functools.wraps(function)
def result(*args, **kwargs): def result(*args, **kwargs):
__tracebackhide__ = True __tracebackhide__ = True
__being_called_by_pytest = kwargs.pop("__being_called_by_pytest", False) warnings.warn(warning, stacklevel=3)
if not __being_called_by_pytest:
warnings.warn(warning, stacklevel=3)
return function(*args, **kwargs) return function(*args, **kwargs)
if six.PY2: if six.PY2:
@ -1279,9 +1269,9 @@ class FixtureManager(object):
# The attribute can be an arbitrary descriptor, so the attribute # The attribute can be an arbitrary descriptor, so the attribute
# access below can raise. safe_getatt() ignores such exceptions. # access below can raise. safe_getatt() ignores such exceptions.
obj = safe_getattr(holderobj, name, None) obj = safe_getattr(holderobj, name, None)
marker = getfixturemarker(obj)
# fixture functions have a pytest_funcarg__ prefix (pre-2.3 style) # fixture functions have a pytest_funcarg__ prefix (pre-2.3 style)
# or are "@pytest.fixture" marked # or are "@pytest.fixture" marked
marker = getfixturemarker(obj)
if marker is None: if marker is None:
if not name.startswith(self._argprefix): if not name.startswith(self._argprefix):
continue continue
@ -1303,6 +1293,15 @@ class FixtureManager(object):
name = marker.name name = marker.name
assert not name.startswith(self._argprefix), FIXTURE_MSG.format(name) assert not name.startswith(self._argprefix), FIXTURE_MSG.format(name)
# during fixture definition we wrap the original fixture function
# to issue a warning if called directly, so here we unwrap it in order to not emit the warning
# when pytest itself calls the fixture function
if six.PY2 and unittest:
# hack on Python 2 because of the unbound methods
obj = get_real_func(obj)
else:
obj = get_real_method(obj, holderobj)
fixture_def = FixtureDef( fixture_def = FixtureDef(
self, self,
nodeid, nodeid,

View File

@ -267,7 +267,6 @@ def test_pytest_plugins_in_non_top_level_conftest_deprecated_no_false_positives(
) )
# @pytest.mark.skipif(six.PY2, reason="We issue the warning in Python 3 only")
def test_call_fixture_function_deprecated(): def test_call_fixture_function_deprecated():
"""Check if a warning is raised if a fixture function is called directly (#3661)""" """Check if a warning is raised if a fixture function is called directly (#3661)"""

View File

@ -1470,10 +1470,9 @@ class TestFixtureManagerParseFactories(object):
print (faclist) print (faclist)
assert len(faclist) == 3 assert len(faclist) == 3
kwargs = {'__being_called_by_pytest': True} assert faclist[0].func(item._request) == "conftest"
assert faclist[0].func(item._request, **kwargs) == "conftest" assert faclist[1].func(item._request) == "module"
assert faclist[1].func(item._request, **kwargs) == "module" assert faclist[2].func(item._request) == "class"
assert faclist[2].func(item._request, **kwargs) == "class"
""" """
) )
reprec = testdir.inline_run("-s") reprec = testdir.inline_run("-s")