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:
parent
f8749eeb5c
commit
011f88f7e7
|
@ -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.
|
|
@ -84,6 +84,7 @@ def iscoroutinefunction(func):
|
||||||
|
|
||||||
|
|
||||||
def getlocation(function, curdir):
|
def getlocation(function, curdir):
|
||||||
|
function = get_real_func(function)
|
||||||
fn = py.path.local(inspect.getfile(function))
|
fn = py.path.local(inspect.getfile(function))
|
||||||
lineno = function.__code__.co_firstlineno
|
lineno = function.__code__.co_firstlineno
|
||||||
if fn.relto(curdir):
|
if fn.relto(curdir):
|
||||||
|
|
|
@ -22,6 +22,12 @@ FUNCARG_PREFIX = (
|
||||||
"Please remove the prefix and use the @pytest.fixture decorator instead."
|
"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 = (
|
CFG_PYTEST_SECTION = (
|
||||||
"[pytest] section in {filename} files is deprecated, use [tool:pytest] instead."
|
"[pytest] section in {filename} files is deprecated, use [tool:pytest] instead."
|
||||||
)
|
)
|
||||||
|
|
|
@ -6,6 +6,8 @@ import os
|
||||||
import sys
|
import sys
|
||||||
import warnings
|
import warnings
|
||||||
from collections import OrderedDict, deque, defaultdict
|
from collections import OrderedDict, deque, defaultdict
|
||||||
|
|
||||||
|
import six
|
||||||
from more_itertools import flatten
|
from more_itertools import flatten
|
||||||
|
|
||||||
import attr
|
import attr
|
||||||
|
@ -29,6 +31,7 @@ from _pytest.compat import (
|
||||||
safe_getattr,
|
safe_getattr,
|
||||||
FuncargnamesCompatAttr,
|
FuncargnamesCompatAttr,
|
||||||
)
|
)
|
||||||
|
from _pytest.deprecated import FIXTURE_FUNCTION_CALL, RemovedInPytest4Warning
|
||||||
from _pytest.outcomes import fail, TEST_OUTCOME
|
from _pytest.outcomes import fail, TEST_OUTCOME
|
||||||
|
|
||||||
FIXTURE_MSG = 'fixtures cannot have "pytest_funcarg__" prefix and be decorated with @pytest.fixture:\n{}'
|
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):
|
def _teardown_yield_fixture(fixturefunc, it):
|
||||||
"""Executes the teardown of a fixture function by advancing the iterator after the
|
"""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:
|
try:
|
||||||
next(it)
|
next(it)
|
||||||
except StopIteration:
|
except StopIteration:
|
||||||
|
@ -928,6 +931,13 @@ 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:
|
||||||
|
@ -947,6 +957,44 @@ def _ensure_immutable_ids(ids):
|
||||||
return tuple(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)
|
@attr.s(frozen=True)
|
||||||
class FixtureFunctionMarker(object):
|
class FixtureFunctionMarker(object):
|
||||||
scope = attr.ib()
|
scope = attr.ib()
|
||||||
|
@ -964,6 +1012,8 @@ class FixtureFunctionMarker(object):
|
||||||
"fixture is being applied more than once to the same function"
|
"fixture is being applied more than once to the same function"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
function = wrap_function_to_warning_if_called_directly(function, self)
|
||||||
|
|
||||||
function._pytestfixturefunction = self
|
function._pytestfixturefunction = self
|
||||||
return function
|
return function
|
||||||
|
|
||||||
|
|
|
@ -669,6 +669,11 @@ class Testdir(object):
|
||||||
else:
|
else:
|
||||||
raise LookupError("example is not found as a file or directory")
|
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
|
Session = Session
|
||||||
|
|
||||||
def getnode(self, config, arg):
|
def getnode(self, config, arg):
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
from __future__ import absolute_import, division, print_function
|
from __future__ import absolute_import, division, print_function
|
||||||
|
|
||||||
|
|
||||||
import pytest
|
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]
|
str(PYTEST_PLUGINS_FROM_NON_TOP_LEVEL_CONFTEST).splitlines()[0]
|
||||||
not in res.stderr.str()
|
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
|
||||||
|
|
|
@ -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() == []
|
|
@ -1456,6 +1456,7 @@ class TestFixtureManagerParseFactories(object):
|
||||||
testdir.makepyfile(
|
testdir.makepyfile(
|
||||||
"""
|
"""
|
||||||
import pytest
|
import pytest
|
||||||
|
import six
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def hello(request):
|
def hello(request):
|
||||||
|
@ -1468,9 +1469,11 @@ class TestFixtureManagerParseFactories(object):
|
||||||
faclist = fm.getfixturedefs("hello", item.nodeid)
|
faclist = fm.getfixturedefs("hello", item.nodeid)
|
||||||
print (faclist)
|
print (faclist)
|
||||||
assert len(faclist) == 3
|
assert len(faclist) == 3
|
||||||
assert faclist[0].func(item._request) == "conftest"
|
|
||||||
assert faclist[1].func(item._request) == "module"
|
kwargs = {'__being_called_by_pytest': True}
|
||||||
assert faclist[2].func(item._request) == "class"
|
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")
|
reprec = testdir.inline_run("-s")
|
||||||
|
|
|
@ -12,7 +12,6 @@ from _pytest.assertion import truncate
|
||||||
PY3 = sys.version_info >= (3, 0)
|
PY3 = sys.version_info >= (3, 0)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def mock_config():
|
def mock_config():
|
||||||
class Config(object):
|
class Config(object):
|
||||||
verbose = False
|
verbose = False
|
||||||
|
@ -768,15 +767,15 @@ def test_rewritten(testdir):
|
||||||
assert testdir.runpytest().ret == 0
|
assert testdir.runpytest().ret == 0
|
||||||
|
|
||||||
|
|
||||||
def test_reprcompare_notin(mock_config):
|
def test_reprcompare_notin():
|
||||||
detail = plugin.pytest_assertrepr_compare(
|
config = mock_config()
|
||||||
mock_config, "not in", "foo", "aaafoobbb"
|
detail = plugin.pytest_assertrepr_compare(config, "not in", "foo", "aaafoobbb")[1:]
|
||||||
)[1:]
|
|
||||||
assert detail == ["'foo' is contained here:", " aaafoobbb", "? +++"]
|
assert detail == ["'foo' is contained here:", " aaafoobbb", "? +++"]
|
||||||
|
|
||||||
|
|
||||||
def test_reprcompare_whitespaces(mock_config):
|
def test_reprcompare_whitespaces():
|
||||||
detail = plugin.pytest_assertrepr_compare(mock_config, "==", "\r\n", "\n")
|
config = mock_config()
|
||||||
|
detail = plugin.pytest_assertrepr_compare(config, "==", "\r\n", "\n")
|
||||||
assert detail == [
|
assert detail == [
|
||||||
r"'\r\n' == '\n'",
|
r"'\r\n' == '\n'",
|
||||||
r"Strings contain only whitespace, escaping them using repr()",
|
r"Strings contain only whitespace, escaping them using repr()",
|
||||||
|
|
|
@ -10,9 +10,7 @@ from _pytest.main import EXIT_NOTESTSCOLLECTED, EXIT_USAGEERROR
|
||||||
|
|
||||||
@pytest.fixture(scope="module", params=["global", "inpackage"])
|
@pytest.fixture(scope="module", params=["global", "inpackage"])
|
||||||
def basedir(request, tmpdir_factory):
|
def basedir(request, tmpdir_factory):
|
||||||
from _pytest.tmpdir import tmpdir
|
tmpdir = tmpdir_factory.mktemp("basedir", numbered=True)
|
||||||
|
|
||||||
tmpdir = tmpdir(request, tmpdir_factory)
|
|
||||||
tmpdir.ensure("adir/conftest.py").write("a=1 ; Directory = 3")
|
tmpdir.ensure("adir/conftest.py").write("a=1 ; Directory = 3")
|
||||||
tmpdir.ensure("adir/b/conftest.py").write("b=2 ; a = 1.5")
|
tmpdir.ensure("adir/b/conftest.py").write("b=2 ; a = 1.5")
|
||||||
if request.param == "inpackage":
|
if request.param == "inpackage":
|
||||||
|
|
|
@ -3,35 +3,10 @@ import sys
|
||||||
import py
|
import py
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from _pytest.tmpdir import tmpdir
|
|
||||||
|
|
||||||
|
def test_tmpdir_fixture(testdir):
|
||||||
def test_funcarg(testdir):
|
results = testdir.run_example("tmpdir/tmpdir_fixture.py")
|
||||||
testdir.makepyfile(
|
results.stdout.fnmatch_lines("*1 passed*")
|
||||||
"""
|
|
||||||
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_ensuretemp(recwarn):
|
def test_ensuretemp(recwarn):
|
||||||
|
|
Loading…
Reference in New Issue