Skip to content

Commit 011f88f

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 f8749ee commit 011f88f

File tree

11 files changed

+103
-42
lines changed

11 files changed

+103
-42
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``. It will be changed into an error in pytest ``4.0``.
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

+51-1
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{}'
@@ -798,7 +801,7 @@ def call_fixture_func(fixturefunc, request, kwargs):
798801

799802
def _teardown_yield_fixture(fixturefunc, it):
800803
"""Executes the teardown of a fixture function by advancing the iterator after the
801-
yield and ensure the iteration ends (if not it means there is more than one yield in the function"""
804+
yield and ensure the iteration ends (if not it means there is more than one yield in the function)"""
802805
try:
803806
next(it)
804807
except StopIteration:
@@ -928,6 +931,13 @@ 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 parameter so we don't raise a warning
936+
# this is an ugly hack, see #3720 for an opportunity to improve this
937+
defined_using_fixture_decorator = hasattr(fixturedef.func, "_pytestfixturefunction")
938+
if defined_using_fixture_decorator:
939+
kwargs["__being_called_by_pytest"] = True
940+
931941
fixturefunc = resolve_fixture_function(fixturedef, request)
932942
my_cache_key = request.param_index
933943
try:
@@ -947,6 +957,44 @@ def _ensure_immutable_ids(ids):
947957
return tuple(ids)
948958

949959

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

1015+
function = wrap_function_to_warning_if_called_directly(function, self)
1016+
9671017
function._pytestfixturefunction = self
9681018
return function
9691019

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

+14
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
from __future__ import absolute_import, division, print_function
2+
3+
24
import pytest
35

46

@@ -263,3 +265,15 @@ def test_func():
263265
str(PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST).splitlines()[0]
264266
not in res.stderr.str()
265267
)
268+
269+
270+
# @pytest.mark.skipif(six.PY2, reason="We issue the warning in Python 3 only")
271+
def test_call_fixture_function_deprecated():
272+
"""Check if a warning is raised if a fixture function is called directly (#3661)"""
273+
274+
@pytest.fixture
275+
def fix():
276+
return 1
277+
278+
with pytest.deprecated_call():
279+
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}
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)