From a6f3ec732798b2bd06746c55df2b147596caeed3 Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 9 Nov 2025 00:38:01 +0200 Subject: [PATCH 1/2] Remove legacy py.path argument support in hooks Deprecated feature scheduled for removal in pytest 9. Part of #13893. --- doc/en/deprecations.rst | 55 ++++++++++++------------- src/_pytest/config/__init__.py | 4 +- src/_pytest/config/compat.py | 72 --------------------------------- src/_pytest/deprecated.py | 7 ---- src/_pytest/hookspec.py | 74 ++++++---------------------------- src/_pytest/main.py | 3 +- testing/deprecated_test.py | 59 --------------------------- 7 files changed, 43 insertions(+), 231 deletions(-) diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index f2a665a6267..3a7324da7ed 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -297,33 +297,6 @@ Changed ``hookwrapper`` attributes: * ``historic`` -.. _legacy-path-hooks-deprecated: - -``py.path.local`` arguments for hooks replaced with ``pathlib.Path`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 7.0 - -In order to support the transition from ``py.path.local`` to :mod:`pathlib`, the following hooks now receive additional arguments: - -* :hook:`pytest_ignore_collect(collection_path: pathlib.Path) ` as equivalent to ``path`` -* :hook:`pytest_collect_file(file_path: pathlib.Path) ` as equivalent to ``path`` -* :hook:`pytest_pycollect_makemodule(module_path: pathlib.Path) ` as equivalent to ``path`` -* :hook:`pytest_report_header(start_path: pathlib.Path) ` as equivalent to ``startdir`` -* :hook:`pytest_report_collectionfinish(start_path: pathlib.Path) ` as equivalent to ``startdir`` - -The accompanying ``py.path.local`` based paths have been deprecated: plugins which manually invoke those hooks should only pass the new ``pathlib.Path`` arguments, and users should change their hook implementations to use the new ``pathlib.Path`` arguments. - -.. note:: - The name of the :class:`~_pytest.nodes.Node` arguments and attributes, - :ref:`outlined above ` (the new attribute - being ``path``) is **the opposite** of the situation for hooks (the old - argument being ``path``). - - This is an unfortunate artifact due to historical reasons, which should be - resolved in future versions as we slowly get rid of the :pypi:`py` - dependency (see :issue:`9283` for a longer discussion). - Directly constructing internal classes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -430,6 +403,34 @@ an appropriate period of deprecation has passed. Some breaking changes which could not be deprecated are also listed. +.. _legacy-path-hooks-deprecated: + +``py.path.local`` arguments for hooks replaced with ``pathlib.Path`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 7.0 +.. versionremoved:: 9.0 + +In order to support the transition from ``py.path.local`` to :mod:`pathlib`, the following hooks now receive additional arguments: + +* :hook:`pytest_ignore_collect(collection_path: pathlib.Path) ` as equivalent to ``path`` +* :hook:`pytest_collect_file(file_path: pathlib.Path) ` as equivalent to ``path`` +* :hook:`pytest_pycollect_makemodule(module_path: pathlib.Path) ` as equivalent to ``path`` +* :hook:`pytest_report_header(start_path: pathlib.Path) ` as equivalent to ``startdir`` +* :hook:`pytest_report_collectionfinish(start_path: pathlib.Path) ` as equivalent to ``startdir`` + +The accompanying ``py.path.local`` based paths have been deprecated: plugins which manually invoke those hooks should only pass the new ``pathlib.Path`` arguments, and users should change their hook implementations to use the new ``pathlib.Path`` arguments. + +.. note:: + The name of the :class:`~_pytest.nodes.Node` arguments and attributes, + :ref:`outlined above ` (the new attribute + being ``path``) is **the opposite** of the situation for hooks (the old + argument being ``path``). + + This is an unfortunate artifact due to historical reasons, which should be + resolved in future versions as we slowly get rid of the :pypi:`py` + dependency (see :issue:`9283` for a longer discussion). + .. _yield tests deprecated: ``yield`` tests diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 812daed88f2..455ff03e908 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -38,14 +38,12 @@ from typing import TYPE_CHECKING import warnings -import pluggy from pluggy import HookimplMarker from pluggy import HookimplOpts from pluggy import HookspecMarker from pluggy import HookspecOpts from pluggy import PluginManager -from .compat import PathAwareHookProxy from .exceptions import PrintHelp as PrintHelp from .exceptions import UsageError as UsageError from .findpaths import determine_setup @@ -1087,7 +1085,7 @@ def __init__( self._store = self.stash self.trace = self.pluginmanager.trace.root.get("config") - self.hook: pluggy.HookRelay = PathAwareHookProxy(self.pluginmanager.hook) # type: ignore[assignment] + self.hook = self.pluginmanager.hook self._inicache: dict[str, Any] = {} self._cleanup_stack = contextlib.ExitStack() self.pluginmanager.register(self, "pytestconfig") diff --git a/src/_pytest/config/compat.py b/src/_pytest/config/compat.py index 21eab4c7e47..9c61b4dac09 100644 --- a/src/_pytest/config/compat.py +++ b/src/_pytest/config/compat.py @@ -1,26 +1,8 @@ from __future__ import annotations -from collections.abc import Mapping -import functools from pathlib import Path -from typing import Any -import warnings - -import pluggy from ..compat import LEGACY_PATH -from ..compat import legacy_path -from ..deprecated import HOOK_LEGACY_PATH_ARG - - -# hookname: (Path, LEGACY_PATH) -imply_paths_hooks: Mapping[str, tuple[str, str]] = { - "pytest_ignore_collect": ("collection_path", "path"), - "pytest_collect_file": ("file_path", "path"), - "pytest_pycollect_makemodule": ("module_path", "path"), - "pytest_report_header": ("start_path", "startdir"), - "pytest_report_collectionfinish": ("start_path", "startdir"), -} def _check_path(path: Path, fspath: LEGACY_PATH) -> None: @@ -29,57 +11,3 @@ def _check_path(path: Path, fspath: LEGACY_PATH) -> None: f"Path({fspath!r}) != {path!r}\n" "if both path and fspath are given they need to be equal" ) - - -class PathAwareHookProxy: - """ - this helper wraps around hook callers - until pluggy supports fixingcalls, this one will do - - it currently doesn't return full hook caller proxies for fixed hooks, - this may have to be changed later depending on bugs - """ - - def __init__(self, hook_relay: pluggy.HookRelay) -> None: - self._hook_relay = hook_relay - - def __dir__(self) -> list[str]: - return dir(self._hook_relay) - - def __getattr__(self, key: str) -> pluggy.HookCaller: - hook: pluggy.HookCaller = getattr(self._hook_relay, key) - if key not in imply_paths_hooks: - self.__dict__[key] = hook - return hook - else: - path_var, fspath_var = imply_paths_hooks[key] - - @functools.wraps(hook) - def fixed_hook(**kw: Any) -> Any: - path_value: Path | None = kw.pop(path_var, None) - fspath_value: LEGACY_PATH | None = kw.pop(fspath_var, None) - if fspath_value is not None: - warnings.warn( - HOOK_LEGACY_PATH_ARG.format( - pylib_path_arg=fspath_var, pathlib_path_arg=path_var - ), - stacklevel=2, - ) - if path_value is not None: - if fspath_value is not None: - _check_path(path_value, fspath_value) - else: - fspath_value = legacy_path(path_value) - else: - assert fspath_value is not None - path_value = Path(fspath_value) - - kw[path_var] = path_value - kw[fspath_var] = fspath_value - return hook(**kw) - - fixed_hook.name = hook.name # type: ignore[attr-defined] - fixed_hook.spec = hook.spec # type: ignore[attr-defined] - fixed_hook.__name__ = key - self.__dict__[key] = fixed_hook - return fixed_hook # type: ignore[return-value] diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index a8be4881433..0d51d2e1b9f 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -39,13 +39,6 @@ PRIVATE = PytestDeprecationWarning("A private pytest class or function was used.") -HOOK_LEGACY_PATH_ARG = UnformattedWarning( - PytestRemovedIn9Warning, - "The ({pylib_path_arg}: py.path.local) argument is deprecated, please use ({pathlib_path_arg}: pathlib.Path)\n" - "see https://docs.pytest.org/en/latest/deprecations.html" - "#py-path-local-arguments-for-hooks-replaced-with-pathlib-path", -) - NODE_CTOR_FSPATH_ARG = UnformattedWarning( PytestRemovedIn9Warning, "The (fspath: py.path.local) argument to {node_type_name} is deprecated. " diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index c5bcc36ad4b..8c4333810e7 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -13,8 +13,6 @@ from pluggy import HookspecMarker -from .deprecated import HOOK_LEGACY_PATH_ARG - if TYPE_CHECKING: import pdb @@ -23,7 +21,6 @@ from _pytest._code.code import ExceptionInfo from _pytest._code.code import ExceptionRepr - from _pytest.compat import LEGACY_PATH from _pytest.config import _PluggyPlugin from _pytest.config import Config from _pytest.config import ExitCode @@ -302,17 +299,8 @@ def pytest_collection_finish(session: Session) -> None: """ -@hookspec( - firstresult=True, - warn_on_impl_args={ - "path": HOOK_LEGACY_PATH_ARG.format( - pylib_path_arg="path", pathlib_path_arg="collection_path" - ), - }, -) -def pytest_ignore_collect( - collection_path: Path, path: LEGACY_PATH, config: Config -) -> bool | None: +@hookspec(firstresult=True) +def pytest_ignore_collect(collection_path: Path, config: Config) -> bool | None: """Return ``True`` to ignore this path for collection. Return ``None`` to let other plugins ignore the path for collection. @@ -333,7 +321,7 @@ def pytest_ignore_collect( .. versionchanged:: 7.0.0 The ``collection_path`` parameter was added as a :class:`pathlib.Path` equivalent of the ``path`` parameter. The ``path`` parameter - has been deprecated. + has been deprecated and removed in pytest 9.0.0. Use in conftest plugins ======================= @@ -375,16 +363,7 @@ def pytest_collect_directory(path: Path, parent: Collector) -> Collector | None: """ -@hookspec( - warn_on_impl_args={ - "path": HOOK_LEGACY_PATH_ARG.format( - pylib_path_arg="path", pathlib_path_arg="file_path" - ), - }, -) -def pytest_collect_file( - file_path: Path, path: LEGACY_PATH, parent: Collector -) -> Collector | None: +def pytest_collect_file(file_path: Path, parent: Collector) -> Collector | None: """Create a :class:`~pytest.Collector` for the given path, or None if not relevant. For best results, the returned collector should be a subclass of @@ -399,7 +378,7 @@ def pytest_collect_file( .. versionchanged:: 7.0.0 The ``file_path`` parameter was added as a :class:`pathlib.Path` equivalent of the ``path`` parameter. The ``path`` parameter - has been deprecated. + has been deprecated and removed in pytest 9.0.0. Use in conftest plugins ======================= @@ -501,17 +480,8 @@ def pytest_make_collect_report(collector: Collector) -> CollectReport | None: # ------------------------------------------------------------------------- -@hookspec( - firstresult=True, - warn_on_impl_args={ - "path": HOOK_LEGACY_PATH_ARG.format( - pylib_path_arg="path", pathlib_path_arg="module_path" - ), - }, -) -def pytest_pycollect_makemodule( - module_path: Path, path: LEGACY_PATH, parent -) -> Module | None: +@hookspec(firstresult=True) +def pytest_pycollect_makemodule(module_path: Path, parent) -> Module | None: """Return a :class:`pytest.Module` collector or None for the given path. This hook will be called for each matching test module path. @@ -526,9 +496,8 @@ def pytest_pycollect_makemodule( .. versionchanged:: 7.0.0 The ``module_path`` parameter was added as a :class:`pathlib.Path` - equivalent of the ``path`` parameter. - - The ``path`` parameter has been deprecated in favor of ``fspath``. + equivalent of the ``path`` parameter. The ``path`` parameter has been + deprecated in favor of ``module_path`` and removed in pytest 9.0.0. Use in conftest plugins ======================= @@ -1036,16 +1005,7 @@ def pytest_assertion_pass(item: Item, lineno: int, orig: str, expl: str) -> None # ------------------------------------------------------------------------- -@hookspec( - warn_on_impl_args={ - "startdir": HOOK_LEGACY_PATH_ARG.format( - pylib_path_arg="startdir", pathlib_path_arg="start_path" - ), - }, -) -def pytest_report_header( # type:ignore[empty-body] - config: Config, start_path: Path, startdir: LEGACY_PATH -) -> str | list[str]: +def pytest_report_header(config: Config, start_path: Path) -> str | list[str]: # type: ignore[empty-body] """Return a string or list of strings to be displayed as header info for terminal reporting. :param config: The pytest config object. @@ -1063,7 +1023,7 @@ def pytest_report_header( # type:ignore[empty-body] .. versionchanged:: 7.0.0 The ``start_path`` parameter was added as a :class:`pathlib.Path` equivalent of the ``startdir`` parameter. The ``startdir`` parameter - has been deprecated. + has been deprecated and removed in pytest 9.0.0. Use in conftest plugins ======================= @@ -1072,17 +1032,9 @@ def pytest_report_header( # type:ignore[empty-body] """ -@hookspec( - warn_on_impl_args={ - "startdir": HOOK_LEGACY_PATH_ARG.format( - pylib_path_arg="startdir", pathlib_path_arg="start_path" - ), - }, -) -def pytest_report_collectionfinish( # type:ignore[empty-body] +def pytest_report_collectionfinish( # type: ignore[empty-body] config: Config, start_path: Path, - startdir: LEGACY_PATH, items: Sequence[Item], ) -> str | list[str]: """Return a string or list of strings to be displayed after collection @@ -1108,7 +1060,7 @@ def pytest_report_collectionfinish( # type:ignore[empty-body] .. versionchanged:: 7.0.0 The ``start_path`` parameter was added as a :class:`pathlib.Path` equivalent of the ``startdir`` parameter. The ``startdir`` parameter - has been deprecated. + has been deprecated and removed in pytest 9.0.0. Use in conftest plugins ======================= diff --git a/src/_pytest/main.py b/src/_pytest/main.py index 9bc930df8e8..02c7fb373fd 100644 --- a/src/_pytest/main.py +++ b/src/_pytest/main.py @@ -34,7 +34,6 @@ from _pytest.config import UsageError from _pytest.config.argparsing import OverrideIniAction from _pytest.config.argparsing import Parser -from _pytest.config.compat import PathAwareHookProxy from _pytest.outcomes import exit from _pytest.pathlib import absolutepath from _pytest.pathlib import bestrelpath @@ -727,7 +726,7 @@ def gethookproxy(self, fspath: os.PathLike[str]) -> pluggy.HookRelay: proxy: pluggy.HookRelay if remove_mods: # One or more conftests are not in use at this path. - proxy = PathAwareHookProxy(FSHookProxy(pm, remove_mods)) # type: ignore[arg-type,assignment] + proxy = FSHookProxy(pm, remove_mods) # type: ignore[assignment] else: # All plugins are active for this fspath. proxy = self.config.hook diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index 5d0e69c58c1..ca9bef2ba76 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -1,9 +1,7 @@ # mypy: allow-untyped-defs from __future__ import annotations -from pathlib import Path import re -import sys from _pytest import deprecated from _pytest.compat import legacy_path @@ -92,63 +90,6 @@ def __init__(self, foo: int, *, _ispytest: bool = False) -> None: PrivateInit(10, _ispytest=True) -@pytest.mark.parametrize("hooktype", ["hook", "ihook"]) -def test_hookproxy_warnings_for_pathlib(tmp_path, hooktype, request): - path = legacy_path(tmp_path) - - PATH_WARN_MATCH = r".*path: py\.path\.local\) argument is deprecated, please use \(collection_path: pathlib\.Path.*" - if hooktype == "ihook": - hooks = request.node.ihook - else: - hooks = request.config.hook - - with pytest.warns(PytestDeprecationWarning, match=PATH_WARN_MATCH) as r: - l1 = sys._getframe().f_lineno - hooks.pytest_ignore_collect( - config=request.config, path=path, collection_path=tmp_path - ) - l2 = sys._getframe().f_lineno - - (record,) = r - assert record.filename == __file__ - assert l1 < record.lineno < l2 - - hooks.pytest_ignore_collect(config=request.config, collection_path=tmp_path) - - # Passing entirely *different* paths is an outright error. - with pytest.raises(ValueError, match=r"path.*fspath.*need to be equal"): - with pytest.warns(PytestDeprecationWarning, match=PATH_WARN_MATCH) as r: - hooks.pytest_ignore_collect( - config=request.config, path=path, collection_path=Path("/bla/bla") - ) - - -def test_hookimpl_warnings_for_pathlib() -> None: - class Plugin: - def pytest_ignore_collect(self, path: object) -> None: - raise NotImplementedError() - - def pytest_collect_file(self, path: object) -> None: - raise NotImplementedError() - - def pytest_pycollect_makemodule(self, path: object) -> None: - raise NotImplementedError() - - def pytest_report_header(self, startdir: object) -> str: - raise NotImplementedError() - - def pytest_report_collectionfinish(self, startdir: object) -> str: - raise NotImplementedError() - - pm = pytest.PytestPluginManager() - with pytest.warns( - pytest.PytestRemovedIn9Warning, - match=r"py\.path\.local.* argument is deprecated", - ) as wc: - pm.register(Plugin()) - assert len(wc.list) == 5 - - def test_node_ctor_fspath_argument_is_deprecated(pytester: Pytester) -> None: mod = pytester.getmodulecol("") From 86608c3aaca8c6c9c009f463a01eec397f5f17ad Mon Sep 17 00:00:00 2001 From: Ran Benita Date: Sun, 9 Nov 2025 22:12:53 +0200 Subject: [PATCH 2/2] Hard error when setting a mark on a fixture function Deprecated feature scheduled for removal in pytest 9. Part of #13893. --- doc/en/deprecations.rst | 35 +++++++++++----------- src/_pytest/deprecated.py | 5 ---- src/_pytest/fixtures.py | 6 ++-- src/_pytest/mark/structures.py | 10 ++++--- testing/deprecated_test.py | 53 ---------------------------------- testing/test_mark.py | 42 +++++++++++++++++++++++++++ 6 files changed, 70 insertions(+), 81 deletions(-) diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index 3a7324da7ed..3b3968978eb 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -367,23 +367,6 @@ conflicts (such as :class:`pytest.File` now taking ``path`` instead of ``fspath``, as :ref:`outlined above `), a deprecation warning is now raised. -Applying a mark to a fixture function -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. deprecated:: 7.4 - -Applying a mark to a fixture function never had any effect, but it is a common user error. - -.. code-block:: python - - @pytest.mark.usefixtures("clean_database") - @pytest.fixture - def user() -> User: ... - -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. - -Now pytest will issue a warning when it encounters this problem, and will raise an error in the future versions. - The ``yield_fixture`` function/decorator ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -403,6 +386,24 @@ an appropriate period of deprecation has passed. Some breaking changes which could not be deprecated are also listed. +Applying a mark to a fixture function +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. deprecated:: 7.4 +.. versionremoved:: 9.0 + +Applying a mark to a fixture function never had any effect, but it is a common user error. + +.. code-block:: python + + @pytest.mark.usefixtures("clean_database") + @pytest.fixture + def user() -> User: ... + +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. + +Now pytest will issue a warning when it encounters this problem, and will raise an error in the future versions. + .. _legacy-path-hooks-deprecated: ``py.path.local`` arguments for hooks replaced with ``pathlib.Path`` diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 0d51d2e1b9f..5155edc9203 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -56,11 +56,6 @@ "#configuring-hook-specs-impls-using-markers", ) -MARKED_FIXTURE = PytestRemovedIn9Warning( - "Marks applied to fixtures have no effect\n" - "See docs: https://docs.pytest.org/en/stable/deprecations.html#applying-a-mark-to-a-fixture-function" -) - MONKEYPATCH_LEGACY_NAMESPACE_PACKAGES = PytestRemovedIn10Warning( "monkeypatch.syspath_prepend() called with pkg_resources legacy namespace packages detected.\n" "Legacy namespace packages (using pkg_resources.declare_namespace) are deprecated.\n" diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 27846db13a4..e32c461c1e4 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -53,7 +53,6 @@ from _pytest.config import ExitCode from _pytest.config.argparsing import Parser from _pytest.deprecated import check_ispytest -from _pytest.deprecated import MARKED_FIXTURE from _pytest.deprecated import YIELD_FIXTURE from _pytest.main import Session from _pytest.mark import Mark @@ -1236,7 +1235,10 @@ def __call__(self, function: FixtureFunction) -> FixtureFunctionDefinition: ) if hasattr(function, "pytestmark"): - warnings.warn(MARKED_FIXTURE, stacklevel=2) + fail( + "Marks cannot be applied to fixtures.\n" + "See docs: https://docs.pytest.org/en/stable/deprecations.html#applying-a-mark-to-a-fixture-function" + ) fixture_definition = FixtureFunctionDefinition( function=function, fixture_function_marker=self, _ispytest=True diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 9f2f6279158..3edf6ab1163 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -26,7 +26,6 @@ from ..compat import NotSetType from _pytest.config import Config from _pytest.deprecated import check_ispytest -from _pytest.deprecated import MARKED_FIXTURE from _pytest.deprecated import PARAMETRIZE_NON_COLLECTION_ITERABLE from _pytest.outcomes import fail from _pytest.raises import AbstractRaises @@ -409,7 +408,7 @@ def __call__(self, *args: object, **kwargs: object): if isinstance(func, staticmethod | classmethod): unwrapped_func = func.__func__ if len(args) == 1 and (istestfunc(unwrapped_func) or is_class): - store_mark(unwrapped_func, self.mark, stacklevel=3) + store_mark(unwrapped_func, self.mark) return func return self.with_args(*args, **kwargs) @@ -464,7 +463,7 @@ def normalize_mark_list( yield mark_obj -def store_mark(obj, mark: Mark, *, stacklevel: int = 2) -> None: +def store_mark(obj, mark: Mark) -> None: """Store a Mark on an object. This is used to implement the Mark declarations/decorators correctly. @@ -474,7 +473,10 @@ def store_mark(obj, mark: Mark, *, stacklevel: int = 2) -> None: from ..fixtures import getfixturemarker if getfixturemarker(obj) is not None: - warnings.warn(MARKED_FIXTURE, stacklevel=stacklevel) + fail( + "Marks cannot be applied to fixtures.\n" + "See docs: https://docs.pytest.org/en/stable/deprecations.html#applying-a-mark-to-a-fixture-function" + ) # Always reassign name to avoid updating pytestmark in a reference that # was only borrowed. diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index ca9bef2ba76..e7f1d396f3c 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -107,56 +107,3 @@ def collect(self): parent=mod.parent, fspath=legacy_path("bla"), ) - - -def test_fixture_disallow_on_marked_functions(): - """Test that applying @pytest.fixture to a marked function warns (#3364).""" - with pytest.warns( - pytest.PytestRemovedIn9Warning, - match=r"Marks applied to fixtures have no effect", - ) as record: - - @pytest.fixture - @pytest.mark.parametrize("example", ["hello"]) - @pytest.mark.usefixtures("tmp_path") - def foo(): - raise NotImplementedError() - - # it's only possible to get one warning here because you're already prevented - # from applying @fixture twice - # ValueError("fixture is being applied more than once to the same function") - assert len(record) == 1 - - -def test_fixture_disallow_marks_on_fixtures(): - """Test that applying a mark to a fixture warns (#3364).""" - with pytest.warns( - pytest.PytestRemovedIn9Warning, - match=r"Marks applied to fixtures have no effect", - ) as record: - - @pytest.mark.parametrize("example", ["hello"]) - @pytest.mark.usefixtures("tmp_path") - @pytest.fixture - def foo(): - raise NotImplementedError() - - assert len(record) == 2 # one for each mark decorator - # should point to this file - assert all(rec.filename == __file__ for rec in record) - - -def test_fixture_disallowed_between_marks(): - """Test that applying a mark to a fixture warns (#3364).""" - with pytest.warns( - pytest.PytestRemovedIn9Warning, - match=r"Marks applied to fixtures have no effect", - ) as record: - - @pytest.mark.parametrize("example", ["hello"]) - @pytest.fixture - @pytest.mark.usefixtures("tmp_path") - def foo(): - raise NotImplementedError() - - assert len(record) == 2 # one for each mark decorator diff --git a/testing/test_mark.py b/testing/test_mark.py index 8d76ea310eb..67219313183 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -1302,3 +1302,45 @@ def test_staticmethod_wrapper_on_top(value: int): ) result = pytester.runpytest() result.assert_outcomes(passed=8) + + +def test_fixture_disallow_on_marked_functions() -> None: + """Test that applying @pytest.fixture to a marked function errors (#3364).""" + with pytest.raises( + pytest.fail.Exception, + match=r"Marks cannot be applied to fixtures", + ): + + @pytest.fixture + @pytest.mark.parametrize("example", ["hello"]) + @pytest.mark.usefixtures("tmp_path") + def foo(): + raise NotImplementedError() + + +def test_fixture_disallow_marks_on_fixtures() -> None: + """Test that applying a mark to a fixture errors (#3364).""" + with pytest.raises( + pytest.fail.Exception, + match=r"Marks cannot be applied to fixtures", + ): + + @pytest.mark.parametrize("example", ["hello"]) + @pytest.mark.usefixtures("tmp_path") + @pytest.fixture + def foo(): + raise NotImplementedError() + + +def test_fixture_disallowed_between_marks() -> None: + """Test that applying a mark to a fixture errors (#3364).""" + with pytest.raises( + pytest.fail.Exception, + match=r"Marks cannot be applied to fixtures", + ): + + @pytest.mark.parametrize("example", ["hello"]) + @pytest.fixture + @pytest.mark.usefixtures("tmp_path") + def foo(): + raise NotImplementedError()