-
-
Notifications
You must be signed in to change notification settings - Fork 2.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Deprecate calling fixture functions directly #3705
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,6 +6,8 @@ | |
import sys | ||
import warnings | ||
from collections import OrderedDict, deque, defaultdict | ||
|
||
import six | ||
from more_itertools import flatten | ||
|
||
import attr | ||
|
@@ -29,6 +31,7 @@ | |
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. instead of introducing this kwarg, why not just unpack the real function from the definition object There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That is a great idea, thanks! I wished I have thought about it myself. 😁 I've tried and it works well, except when we have test functions which are Here's an illustrative example: import pytest
class Test(object):
@staticmethod
def test_something():
pass
@pytest.fixture
def fix(self):
return 1
@staticmethod
def test_fix(fix):
assert fix == 1
The place where I did the unwrapping was in def resolve_fixture_function(fixturedef, request):
"""Gets the actual callable that can be called to obtain the fixture value, dealing with unittest-specific
instances and bound methods.
"""
fixturefunc = get_real_func(fixturedef.func) # <- unwrapping!
if fixturedef.unittest:
if request.instance is not None:
# bind the unbound method to the TestCase instance
fixturefunc = fixturefunc.__get__(request.instance)
else:
# the fixture function needs to be bound to the actual
# request.instance so that code working with "fixturedef" behaves
# as expected.
if request.instance is not None:
#is_method = getimfunc(fixturedef.func) != fixturedef.func
is_method = inspect.ismethod(fixturedef.func)
if is_method:
fixturefunc = fixturefunc.__get__(request.instance)
return fixturefunc This issue affects 6 of our tests, on py27 and py36. Any suggestions on how to proceed? The unwrapping before calling is much cleaner, but I'm not sure how to deal with staticmethods in that case. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this seems to me like we should open a follow-up issue and go for the mechanism you currently have as enabling the cleaner way requires a re-factoring/bugfixing in fixture lookup/requesting There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just to make sure I follow, keep using the keyword argument approach, and warning only in Python 3? I can try to make it work with Python 2 if you think setting There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. please try setting wrapped - wrt the keyword argument approach - i dont like it but as far as i understood we need to sort out a structural thing to get real extraction working There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. so we keep kwargs, and we try to set up the warnings for py2 as well There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Follow up in #3720 |
||
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 @@ def __call__(self, function): | |
"fixture is being applied more than once to the same function" | ||
) | ||
|
||
function = wrap_function_to_warning_if_called_directly(function, self) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (just double checked, good that this is going to changing a decorator from returning the original function to returning a wrapper always seems to break things -- I'm hoping nobody was depending on decorator ordering for There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sure! 😁 |
||
|
||
function._pytestfixturefunction = self | ||
return function | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() == [] |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,9 +10,7 @@ | |
|
||
@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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. could this also just take the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No because this There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. oh right of course! |
||
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": | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
functools.wraps is broken on python2 and needs to set
__wrapped__
or so to get the correct metadataThere was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ahh thanks, will try that!