Deprecate calling fixture functions directly

This will now issue a RemovedInPytest4Warning when the user calls
a fixture function directly, instead of requesting it from test
functions as is expected

Fix #3661
This commit is contained in:
Bruno Oliveira 2018-07-22 11:10:44 -03:00
parent f8749eeb5c
commit 011f88f7e7
11 changed files with 103 additions and 42 deletions

View File

@ -0,0 +1,3 @@
Calling a fixture function directly, as opposed to request them in a test function, now issues a ``RemovedInPytest4Warning``. It will be changed into an error in pytest ``4.0``.
This is a great source of confusion to new users, which will often call the fixture functions and request them from test functions interchangeably, which breaks the fixture resolution model.

View File

@ -84,6 +84,7 @@ def iscoroutinefunction(func):
def getlocation(function, curdir):
function = get_real_func(function)
fn = py.path.local(inspect.getfile(function))
lineno = function.__code__.co_firstlineno
if fn.relto(curdir):

View File

@ -22,6 +22,12 @@ FUNCARG_PREFIX = (
"Please remove the prefix and use the @pytest.fixture decorator instead."
)
FIXTURE_FUNCTION_CALL = (
"Fixture {name} called directly. Fixtures are not meant to be called directly, "
"are created automatically when test functions request them as parameters. "
"See https://docs.pytest.org/en/latest/fixture.html for more information."
)
CFG_PYTEST_SECTION = (
"[pytest] section in {filename} files is deprecated, use [tool:pytest] instead."
)

View File

@ -6,6 +6,8 @@ import os
import sys
import warnings
from collections import OrderedDict, deque, defaultdict
import six
from more_itertools import flatten
import attr
@ -29,6 +31,7 @@ from _pytest.compat import (
safe_getattr,
FuncargnamesCompatAttr,
)
from _pytest.deprecated import FIXTURE_FUNCTION_CALL, RemovedInPytest4Warning
from _pytest.outcomes import fail, TEST_OUTCOME
FIXTURE_MSG = 'fixtures cannot have "pytest_funcarg__" prefix and be decorated with @pytest.fixture:\n{}'
@ -798,7 +801,7 @@ def call_fixture_func(fixturefunc, request, kwargs):
def _teardown_yield_fixture(fixturefunc, it):
"""Executes the teardown of a fixture function by advancing the iterator after the
yield and ensure the iteration ends (if not it means there is more than one yield in the function"""
yield and ensure the iteration ends (if not it means there is more than one yield in the function)"""
try:
next(it)
except StopIteration:
@ -928,6 +931,13 @@ def pytest_fixture_setup(fixturedef, request):
request._check_scope(argname, request.scope, fixdef.scope)
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)
my_cache_key = request.param_index
try:
@ -947,6 +957,44 @@ def _ensure_immutable_ids(ids):
return tuple(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__)
warning = RemovedInPytest4Warning(msg)
if is_yield_function:
@functools.wraps(function)
def result(*args, **kwargs):
__tracebackhide__ = True
__being_called_by_pytest = kwargs.pop("__being_called_by_pytest", False)
if not __being_called_by_pytest:
warnings.warn(warning, stacklevel=3)
for x in function(*args, **kwargs):
yield x
else:
@functools.wraps(function)
def result(*args, **kwargs):
__tracebackhide__ = True
__being_called_by_pytest = kwargs.pop("__being_called_by_pytest", False)
if not __being_called_by_pytest:
warnings.warn(warning, stacklevel=3)
return function(*args, **kwargs)
if six.PY2:
result.__wrapped__ = function
return result
@attr.s(frozen=True)
class FixtureFunctionMarker(object):
scope = attr.ib()
@ -964,6 +1012,8 @@ class FixtureFunctionMarker(object):
"fixture is being applied more than once to the same function"
)
function = wrap_function_to_warning_if_called_directly(function, self)
function._pytestfixturefunction = self
return function

View File

@ -669,6 +669,11 @@ class Testdir(object):
else:
raise LookupError("example is not found as a file or directory")
def run_example(self, name=None, *pytest_args):
"""Runs the given example and returns the results of the run"""
p = self.copy_example(name)
return self.runpytest(p, *pytest_args)
Session = Session
def getnode(self, config, arg):

View File

@ -1,4 +1,6 @@
from __future__ import absolute_import, division, print_function
import pytest
@ -263,3 +265,15 @@ def test_pytest_plugins_in_non_top_level_conftest_deprecated_no_false_positives(
str(PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST).splitlines()[0]
not in res.stderr.str()
)
# @pytest.mark.skipif(six.PY2, reason="We issue the warning in Python 3 only")
def test_call_fixture_function_deprecated():
"""Check if a warning is raised if a fixture function is called directly (#3661)"""
@pytest.fixture
def fix():
return 1
with pytest.deprecated_call():
assert fix() == 1

View File

@ -0,0 +1,7 @@
import pytest
@pytest.mark.parametrize("a", [r"qwe/\abc"])
def test_fixture(tmpdir, a):
tmpdir.check(dir=1)
assert tmpdir.listdir() == []

View File

@ -1456,6 +1456,7 @@ class TestFixtureManagerParseFactories(object):
testdir.makepyfile(
"""
import pytest
import six
@pytest.fixture
def hello(request):
@ -1468,9 +1469,11 @@ class TestFixtureManagerParseFactories(object):
faclist = fm.getfixturedefs("hello", item.nodeid)
print (faclist)
assert len(faclist) == 3
assert faclist[0].func(item._request) == "conftest"
assert faclist[1].func(item._request) == "module"
assert faclist[2].func(item._request) == "class"
kwargs = {'__being_called_by_pytest': True}
assert faclist[0].func(item._request, **kwargs) == "conftest"
assert faclist[1].func(item._request, **kwargs) == "module"
assert faclist[2].func(item._request, **kwargs) == "class"
"""
)
reprec = testdir.inline_run("-s")

View File

@ -12,7 +12,6 @@ from _pytest.assertion import truncate
PY3 = sys.version_info >= (3, 0)
@pytest.fixture
def mock_config():
class Config(object):
verbose = False
@ -768,15 +767,15 @@ def test_rewritten(testdir):
assert testdir.runpytest().ret == 0
def test_reprcompare_notin(mock_config):
detail = plugin.pytest_assertrepr_compare(
mock_config, "not in", "foo", "aaafoobbb"
)[1:]
def test_reprcompare_notin():
config = mock_config()
detail = plugin.pytest_assertrepr_compare(config, "not in", "foo", "aaafoobbb")[1:]
assert detail == ["'foo' is contained here:", " aaafoobbb", "? +++"]
def test_reprcompare_whitespaces(mock_config):
detail = plugin.pytest_assertrepr_compare(mock_config, "==", "\r\n", "\n")
def test_reprcompare_whitespaces():
config = mock_config()
detail = plugin.pytest_assertrepr_compare(config, "==", "\r\n", "\n")
assert detail == [
r"'\r\n' == '\n'",
r"Strings contain only whitespace, escaping them using repr()",

View File

@ -10,9 +10,7 @@ from _pytest.main import EXIT_NOTESTSCOLLECTED, EXIT_USAGEERROR
@pytest.fixture(scope="module", params=["global", "inpackage"])
def basedir(request, tmpdir_factory):
from _pytest.tmpdir import tmpdir
tmpdir = tmpdir(request, tmpdir_factory)
tmpdir = tmpdir_factory.mktemp("basedir", numbered=True)
tmpdir.ensure("adir/conftest.py").write("a=1 ; Directory = 3")
tmpdir.ensure("adir/b/conftest.py").write("b=2 ; a = 1.5")
if request.param == "inpackage":

View File

@ -3,35 +3,10 @@ import sys
import py
import pytest
from _pytest.tmpdir import tmpdir
def test_funcarg(testdir):
testdir.makepyfile(
"""
def pytest_generate_tests(metafunc):
metafunc.addcall(id='a')
metafunc.addcall(id='b')
def test_func(tmpdir): pass
"""
)
from _pytest.tmpdir import TempdirFactory
reprec = testdir.inline_run()
calls = reprec.getcalls("pytest_runtest_setup")
item = calls[0].item
config = item.config
tmpdirhandler = TempdirFactory(config)
item._initrequest()
p = tmpdir(item._request, tmpdirhandler)
assert p.check()
bn = p.basename.strip("0123456789")
assert bn.endswith("test_func_a_")
item.name = "qwe/\\abc"
p = tmpdir(item._request, tmpdirhandler)
assert p.check()
bn = p.basename.strip("0123456789")
assert bn == "qwe__abc"
def test_tmpdir_fixture(testdir):
results = testdir.run_example("tmpdir/tmpdir_fixture.py")
results.stdout.fnmatch_lines("*1 passed*")
def test_ensuretemp(recwarn):