Skip to content

Commit 5eaf820

Browse files
authored
Merge pull request #8428 from graingert/warn-when-a-mark-is-applied-to-a-fixture
2 parents fe51121 + 518ca37 commit 5eaf820

File tree

7 files changed

+90
-2
lines changed

7 files changed

+90
-2
lines changed

changelog/3664.deprecation.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Applying a mark to a fixture function now issues a warning: marks in fixtures never had any effect, but it is a common user error to apply a mark to a fixture (for example ``usefixtures``) and expect it to work.
2+
3+
This will become an error in the future.

doc/en/deprecations.rst

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,25 @@ conflicts (such as :class:`pytest.File` now taking ``path`` instead of
380380
``fspath``, as :ref:`outlined above <node-ctor-fspath-deprecation>`), a
381381
deprecation warning is now raised.
382382

383+
Applying a mark to a fixture function
384+
-------------------------------------
385+
386+
.. deprecated:: 7.4
387+
388+
Applying a mark to a fixture function never had any effect, but it is a common user error.
389+
390+
.. code-block:: python
391+
392+
@pytest.mark.usefixtures("clean_database")
393+
@pytest.fixture
394+
def user() -> User:
395+
...
396+
397+
Users expected in this case that the ``usefixtures`` mark would have its intended effect of using the ``clean_database`` fixture when ``user`` was invoked, when in fact it has no effect at all.
398+
399+
Now pytest will issue a warning when it encounters this problem, and will raise an error in the future versions.
400+
401+
383402
Backward compatibilities in ``Parser.addoption``
384403
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
385404

doc/en/how-to/fixtures.rst

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1752,8 +1752,7 @@ into an ini-file:
17521752
def my_fixture_that_sadly_wont_use_my_other_fixture():
17531753
...
17541754
1755-
Currently this will not generate any error or warning, but this is intended
1756-
to be handled by :issue:`3664`.
1755+
This generates a deprecation warning, and will become an error in Pytest 8.
17571756

17581757
.. _`override fixtures`:
17591758

src/_pytest/deprecated.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,11 @@
122122
"#configuring-hook-specs-impls-using-markers",
123123
)
124124

125+
MARKED_FIXTURE = PytestRemovedIn8Warning(
126+
"Marks applied to fixtures have no effect\n"
127+
"See docs: https://docs.pytest.org/en/stable/deprecations.html#applying-a-mark-to-a-fixture-function"
128+
)
129+
125130
# You want to make some `__init__` or function "private".
126131
#
127132
# def my_private_function(some, args):

src/_pytest/fixtures.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@
5353
from _pytest.config import Config
5454
from _pytest.config.argparsing import Parser
5555
from _pytest.deprecated import check_ispytest
56+
from _pytest.deprecated import MARKED_FIXTURE
5657
from _pytest.deprecated import YIELD_FIXTURE
5758
from _pytest.mark import Mark
5859
from _pytest.mark import ParameterSet
@@ -1199,6 +1200,9 @@ def __call__(self, function: FixtureFunction) -> FixtureFunction:
11991200
"fixture is being applied more than once to the same function"
12001201
)
12011202

1203+
if hasattr(function, "pytestmark"):
1204+
warnings.warn(MARKED_FIXTURE, stacklevel=2)
1205+
12021206
function = wrap_function_to_error_out_if_called_directly(function, self)
12031207

12041208
name = self.name or function.__name__

src/_pytest/mark/structures.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from ..compat import NotSetType
2929
from _pytest.config import Config
3030
from _pytest.deprecated import check_ispytest
31+
from _pytest.deprecated import MARKED_FIXTURE
3132
from _pytest.outcomes import fail
3233
from _pytest.warning_types import PytestUnknownMarkWarning
3334

@@ -412,6 +413,12 @@ def store_mark(obj, mark: Mark) -> None:
412413
This is used to implement the Mark declarations/decorators correctly.
413414
"""
414415
assert isinstance(mark, Mark), mark
416+
417+
from ..fixtures import getfixturemarker
418+
419+
if getfixturemarker(obj) is not None:
420+
warnings.warn(MARKED_FIXTURE, stacklevel=2)
421+
415422
# Always reassign name to avoid updating pytestmark in a reference that
416423
# was only borrowed.
417424
obj.pytestmark = [*get_unpacked_marks(obj, consider_mro=False), mark]

testing/deprecated_test.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,57 @@ def test_importing_instance_is_deprecated(pytester: Pytester) -> None:
281281
from _pytest.python import Instance # noqa: F401
282282

283283

284+
def test_fixture_disallow_on_marked_functions():
285+
"""Test that applying @pytest.fixture to a marked function warns (#3364)."""
286+
with pytest.warns(
287+
pytest.PytestRemovedIn8Warning,
288+
match=r"Marks applied to fixtures have no effect",
289+
) as record:
290+
291+
@pytest.fixture
292+
@pytest.mark.parametrize("example", ["hello"])
293+
@pytest.mark.usefixtures("tmp_path")
294+
def foo():
295+
raise NotImplementedError()
296+
297+
# it's only possible to get one warning here because you're already prevented
298+
# from applying @fixture twice
299+
# ValueError("fixture is being applied more than once to the same function")
300+
assert len(record) == 1
301+
302+
303+
def test_fixture_disallow_marks_on_fixtures():
304+
"""Test that applying a mark to a fixture warns (#3364)."""
305+
with pytest.warns(
306+
pytest.PytestRemovedIn8Warning,
307+
match=r"Marks applied to fixtures have no effect",
308+
) as record:
309+
310+
@pytest.mark.parametrize("example", ["hello"])
311+
@pytest.mark.usefixtures("tmp_path")
312+
@pytest.fixture
313+
def foo():
314+
raise NotImplementedError()
315+
316+
assert len(record) == 2 # one for each mark decorator
317+
318+
319+
def test_fixture_disallowed_between_marks():
320+
"""Test that applying a mark to a fixture warns (#3364)."""
321+
with pytest.warns(
322+
pytest.PytestRemovedIn8Warning,
323+
match=r"Marks applied to fixtures have no effect",
324+
) as record:
325+
326+
@pytest.mark.parametrize("example", ["hello"])
327+
@pytest.fixture
328+
@pytest.mark.usefixtures("tmp_path")
329+
def foo():
330+
raise NotImplementedError()
331+
332+
assert len(record) == 2 # one for each mark decorator
333+
334+
284335
@pytest.mark.filterwarnings("default")
285336
def test_nose_deprecated_with_setup(pytester: Pytester) -> None:
286337
pytest.importorskip("nose")

0 commit comments

Comments
 (0)