Skip to content

Commit 00edd81

Browse files
committed
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
1 parent f516506 commit 00edd81

File tree

11 files changed

+102
-41
lines changed

11 files changed

+102
-41
lines changed

changelog/3661.removal.rst

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Calling a fixture function directly, as opposed to request them in a test function, now issues a ``RemovedInPytest4Warning`` in Python 3. It will be changed into an error in pytest ``4.0``. In Python 2 nothing changed due to technical difficulties in implementing the warning reliably.
2+
3+
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.

src/_pytest/compat.py

+1
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ def iscoroutinefunction(func):
8484

8585

8686
def getlocation(function, curdir):
87+
function = get_real_func(function)
8788
fn = py.path.local(inspect.getfile(function))
8889
lineno = function.__code__.co_firstlineno
8990
if fn.relto(curdir):

src/_pytest/deprecated.py

+6
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ class RemovedInPytest4Warning(DeprecationWarning):
2222
"Please remove the prefix and use the @pytest.fixture decorator instead."
2323
)
2424

25+
FIXTURE_FUNCTION_CALL = (
26+
"Fixture {name} called directly. Fixtures are not meant to be called directly, "
27+
"are created automatically when test functions request them as parameters. "
28+
"See https://docs.pytest.org/en/latest/fixture.html for more information."
29+
)
30+
2531
CFG_PYTEST_SECTION = (
2632
"[pytest] section in {filename} files is deprecated, use [tool:pytest] instead."
2733
)

src/_pytest/fixtures.py

+49
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import sys
77
import warnings
88
from collections import OrderedDict, deque, defaultdict
9+
10+
import six
911
from more_itertools import flatten
1012

1113
import attr
@@ -29,6 +31,7 @@
2931
safe_getattr,
3032
FuncargnamesCompatAttr,
3133
)
34+
from _pytest.deprecated import FIXTURE_FUNCTION_CALL, RemovedInPytest4Warning
3235
from _pytest.outcomes import fail, TEST_OUTCOME
3336

3437
FIXTURE_MSG = 'fixtures cannot have "pytest_funcarg__" prefix and be decorated with @pytest.fixture:\n{}'
@@ -928,6 +931,12 @@ def pytest_fixture_setup(fixturedef, request):
928931
request._check_scope(argname, request.scope, fixdef.scope)
929932
kwargs[argname] = result
930933

934+
# if function has been defined with @pytest.fixture, we want to
935+
# pass the special __being_called_by_pytest paramater so we don't raise a warning
936+
defined_using_fixture_decorator = hasattr(fixturedef.func, "_pytestfixturefunction")
937+
if defined_using_fixture_decorator and six.PY3:
938+
kwargs["__being_called_by_pytest"] = True
939+
931940
fixturefunc = resolve_fixture_function(fixturedef, request)
932941
my_cache_key = request.param_index
933942
try:
@@ -947,6 +956,44 @@ def _ensure_immutable_ids(ids):
947956
return tuple(ids)
948957

949958

959+
def wrap_function_to_warning_if_called_directly(function, fixture_marker):
960+
"""Wrap the given fixture function so we can issue warnings about it being called directly, instead of
961+
used as an argument in a test function.
962+
963+
The warning is emited only in Python 2, because I did'nt find a reliable way to make the wrapper function
964+
keep the original signature, and we probably will drop Python 2 in Pytest 4 anyway.
965+
"""
966+
if six.PY2:
967+
return function
968+
969+
is_yield_function = is_generator(function)
970+
msg = FIXTURE_FUNCTION_CALL.format(name=fixture_marker.name or function.__name__)
971+
warning = RemovedInPytest4Warning(msg)
972+
973+
if is_yield_function:
974+
975+
@functools.wraps(function)
976+
def result(*args, **kwargs):
977+
__tracebackhide__ = True
978+
__being_called_by_pytest = kwargs.pop("__being_called_by_pytest", False)
979+
if not __being_called_by_pytest:
980+
warnings.warn(warning, stacklevel=3)
981+
for x in function(*args, **kwargs):
982+
yield x
983+
984+
else:
985+
986+
@functools.wraps(function)
987+
def result(*args, **kwargs):
988+
__tracebackhide__ = True
989+
__being_called_by_pytest = kwargs.pop("__being_called_by_pytest", False)
990+
if not __being_called_by_pytest:
991+
warnings.warn(warning, stacklevel=3)
992+
return function(*args, **kwargs)
993+
994+
return result
995+
996+
950997
@attr.s(frozen=True)
951998
class FixtureFunctionMarker(object):
952999
scope = attr.ib()
@@ -964,6 +1011,8 @@ def __call__(self, function):
9641011
"fixture is being applied more than once to the same function"
9651012
)
9661013

1014+
function = wrap_function_to_warning_if_called_directly(function, self)
1015+
9671016
function._pytestfixturefunction = self
9681017
return function
9691018

src/_pytest/pytester.py

+5
Original file line numberDiff line numberDiff line change
@@ -669,6 +669,11 @@ def copy_example(self, name=None):
669669
else:
670670
raise LookupError("example is not found as a file or directory")
671671

672+
def run_example(self, name=None, *pytest_args):
673+
"""Runs the given example and returns the results of the run"""
674+
p = self.copy_example(name)
675+
return self.runpytest(p, *pytest_args)
676+
672677
Session = Session
673678

674679
def getnode(self, config, arg):

testing/deprecated_test.py

+15
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
from __future__ import absolute_import, division, print_function
2+
3+
import six
4+
25
import pytest
36

47

@@ -263,3 +266,15 @@ def test_func():
263266
str(PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST).splitlines()[0]
264267
not in res.stderr.str()
265268
)
269+
270+
271+
@pytest.mark.skipif(six.PY2, reason="We issue the warning in Python 3 only")
272+
def test_call_fixture_function_deprecated():
273+
"""Check if a warning is raised if a fixture function is called directly (#3661)"""
274+
275+
@pytest.fixture
276+
def fix():
277+
return 1
278+
279+
with pytest.deprecated_call():
280+
assert fix() == 1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import pytest
2+
3+
4+
@pytest.mark.parametrize("a", [r"qwe/\abc"])
5+
def test_fixture(tmpdir, a):
6+
tmpdir.check(dir=1)
7+
assert tmpdir.listdir() == []

testing/python/fixture.py

+6-3
Original file line numberDiff line numberDiff line change
@@ -1456,6 +1456,7 @@ def test_parsefactories_conftest_and_module_and_class(self, testdir):
14561456
testdir.makepyfile(
14571457
"""
14581458
import pytest
1459+
import six
14591460
14601461
@pytest.fixture
14611462
def hello(request):
@@ -1468,9 +1469,11 @@ def test_hello(self, item, fm):
14681469
faclist = fm.getfixturedefs("hello", item.nodeid)
14691470
print (faclist)
14701471
assert len(faclist) == 3
1471-
assert faclist[0].func(item._request) == "conftest"
1472-
assert faclist[1].func(item._request) == "module"
1473-
assert faclist[2].func(item._request) == "class"
1472+
1473+
kwargs = {'__being_called_by_pytest': True} if six.PY3 else {}
1474+
assert faclist[0].func(item._request, **kwargs) == "conftest"
1475+
assert faclist[1].func(item._request, **kwargs) == "module"
1476+
assert faclist[2].func(item._request, **kwargs) == "class"
14741477
"""
14751478
)
14761479
reprec = testdir.inline_run("-s")

testing/test_assertion.py

+6-7
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
PY3 = sys.version_info >= (3, 0)
1313

1414

15-
@pytest.fixture
1615
def mock_config():
1716
class Config(object):
1817
verbose = False
@@ -768,15 +767,15 @@ def test_rewritten():
768767
assert testdir.runpytest().ret == 0
769768

770769

771-
def test_reprcompare_notin(mock_config):
772-
detail = plugin.pytest_assertrepr_compare(
773-
mock_config, "not in", "foo", "aaafoobbb"
774-
)[1:]
770+
def test_reprcompare_notin():
771+
config = mock_config()
772+
detail = plugin.pytest_assertrepr_compare(config, "not in", "foo", "aaafoobbb")[1:]
775773
assert detail == ["'foo' is contained here:", " aaafoobbb", "? +++"]
776774

777775

778-
def test_reprcompare_whitespaces(mock_config):
779-
detail = plugin.pytest_assertrepr_compare(mock_config, "==", "\r\n", "\n")
776+
def test_reprcompare_whitespaces():
777+
config = mock_config()
778+
detail = plugin.pytest_assertrepr_compare(config, "==", "\r\n", "\n")
780779
assert detail == [
781780
r"'\r\n' == '\n'",
782781
r"Strings contain only whitespace, escaping them using repr()",

testing/test_conftest.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,7 @@
1010

1111
@pytest.fixture(scope="module", params=["global", "inpackage"])
1212
def basedir(request, tmpdir_factory):
13-
from _pytest.tmpdir import tmpdir
14-
15-
tmpdir = tmpdir(request, tmpdir_factory)
13+
tmpdir = tmpdir_factory.mktemp("basedir", numbered=True)
1614
tmpdir.ensure("adir/conftest.py").write("a=1 ; Directory = 3")
1715
tmpdir.ensure("adir/b/conftest.py").write("b=2 ; a = 1.5")
1816
if request.param == "inpackage":

testing/test_tmpdir.py

+3-28
Original file line numberDiff line numberDiff line change
@@ -3,35 +3,10 @@
33
import py
44
import pytest
55

6-
from _pytest.tmpdir import tmpdir
76

8-
9-
def test_funcarg(testdir):
10-
testdir.makepyfile(
11-
"""
12-
def pytest_generate_tests(metafunc):
13-
metafunc.addcall(id='a')
14-
metafunc.addcall(id='b')
15-
def test_func(tmpdir): pass
16-
"""
17-
)
18-
from _pytest.tmpdir import TempdirFactory
19-
20-
reprec = testdir.inline_run()
21-
calls = reprec.getcalls("pytest_runtest_setup")
22-
item = calls[0].item
23-
config = item.config
24-
tmpdirhandler = TempdirFactory(config)
25-
item._initrequest()
26-
p = tmpdir(item._request, tmpdirhandler)
27-
assert p.check()
28-
bn = p.basename.strip("0123456789")
29-
assert bn.endswith("test_func_a_")
30-
item.name = "qwe/\\abc"
31-
p = tmpdir(item._request, tmpdirhandler)
32-
assert p.check()
33-
bn = p.basename.strip("0123456789")
34-
assert bn == "qwe__abc"
7+
def test_tmpdir_fixture(testdir):
8+
results = testdir.run_example("tmpdir/tmpdir_fixture.py")
9+
results.stdout.fnmatch_lines("*1 passed*")
3510

3611

3712
def test_ensuretemp(recwarn):

0 commit comments

Comments
 (0)