diff --git a/changelog/11284.deprecation.rst b/changelog/11284.deprecation.rst new file mode 100644 index 00000000000..12bcbdc569c --- /dev/null +++ b/changelog/11284.deprecation.rst @@ -0,0 +1,3 @@ +Accessing ``item.funcargs`` with fixture names other than the direct ones, i.e. the direct args, the ones with ``autouse`` and the ones with ``usefixtures`` issues a warning. + +This will become an error in pytest 9. diff --git a/doc/en/deprecations.rst b/doc/en/deprecations.rst index 76cc3482ad3..c1c644d8c7f 100644 --- a/doc/en/deprecations.rst +++ b/doc/en/deprecations.rst @@ -278,6 +278,17 @@ The accompanying ``py.path.local`` based paths have been deprecated: plugins whi resolved in future versions as we slowly get rid of the :pypi:`py` dependency (see :issue:`9283` for a longer discussion). +.. _item-funcargs-deprecation: + +Accessing ``item.funcargs`` with non-directly requested fixture names +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. versionremoved:: 8.1 + +Accessing ``item.funcargs`` with non-directly requested fixture names issues a warning and will be erroneous starting from pytest 9. +Directly requested fixtures are the direct arguments to the test, ``usefixtures`` fixtures and ``autouse`` fixtures. + +To request a fixture other than the directly requested ones, use :func:`request.getfixturevalue ` instead. .. _nose-deprecation: diff --git a/doc/en/example/simple.rst b/doc/en/example/simple.rst index 21e5f4a09a5..c908cbe3ac1 100644 --- a/doc/en/example/simple.rst +++ b/doc/en/example/simple.rst @@ -818,7 +818,7 @@ case we just write some information out to a ``failures`` file: mode = "a" if os.path.exists("failures") else "w" with open("failures", mode, encoding="utf-8") as f: # let's also access a fixture for the fun of it - if "tmp_path" in item.fixturenames: + if "tmp_path" in item.funcargs: extra = " ({})".format(item.funcargs["tmp_path"]) else: extra = "" diff --git a/src/_pytest/assertion/rewrite.py b/src/_pytest/assertion/rewrite.py index 149101e716f..b1a644cbfec 100644 --- a/src/_pytest/assertion/rewrite.py +++ b/src/_pytest/assertion/rewrite.py @@ -17,6 +17,7 @@ from pathlib import Path from pathlib import PurePath from typing import Callable +from typing import DefaultDict from typing import Dict from typing import IO from typing import Iterable @@ -668,9 +669,9 @@ def __init__( else: self.enable_assertion_pass_hook = False self.source = source - self.scope: tuple[ast.AST, ...] = () - self.variables_overwrite: defaultdict[ - tuple[ast.AST, ...], Dict[str, str] + self.scope: Tuple[ast.AST, ...] = () + self.variables_overwrite: DefaultDict[ + Tuple[ast.AST, ...], Dict[str, str] ] = defaultdict(dict) def run(self, mod: ast.Module) -> None: diff --git a/src/_pytest/deprecated.py b/src/_pytest/deprecated.py index 1bc2cf57e80..55cddc4a854 100644 --- a/src/_pytest/deprecated.py +++ b/src/_pytest/deprecated.py @@ -48,6 +48,13 @@ "See docs: https://docs.pytest.org/en/stable/deprecations.html#applying-a-mark-to-a-fixture-function" ) +ITEM_FUNCARGS_MEMBERS = PytestRemovedIn9Warning( + "Accessing `item.funcargs` with a fixture name not directly requested" + " by the item, through a direct argument, `usefixtures` marker or" + " an `autouse` fixture, is deprecated and will raise KeyError starting" + " from pytest 9. Please use request.getfixturevalue instead." +) + # You want to make some `__init__` or function "private". # # def my_private_function(some, args): diff --git a/src/_pytest/doctest.py b/src/_pytest/doctest.py index 4ce32a298e8..a555334365d 100644 --- a/src/_pytest/doctest.py +++ b/src/_pytest/doctest.py @@ -40,6 +40,7 @@ from _pytest.outcomes import skip from _pytest.pathlib import fnmatch_ex from _pytest.pathlib import import_path +from _pytest.python import DeprecatingFuncArgs from _pytest.python import Module from _pytest.python_api import approx from _pytest.warning_types import PytestWarning @@ -284,7 +285,9 @@ def from_parent( # type: ignore return super().from_parent(name=name, parent=parent, runner=runner, dtest=dtest) def _initrequest(self) -> None: - self.funcargs: Dict[str, object] = {} + self.funcargs: Dict[str, object] = DeprecatingFuncArgs( + self._fixtureinfo.initialnames + ) self._request = TopRequest(self, _ispytest=True) # type: ignore[arg-type] def setup(self) -> None: diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index c294ec586b5..cd70d65765f 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -682,7 +682,8 @@ def __repr__(self) -> str: def _fillfixtures(self) -> None: item = self._pyfuncitem - for argname in item.fixturenames: + fixturenames = getattr(item, "fixturenames", self.fixturenames) + for argname in fixturenames: if argname not in item.funcargs: item.funcargs[argname] = self.getfixturevalue(argname) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index e55772d5ece..cde34280c4b 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -15,6 +15,7 @@ from typing import Any from typing import Callable from typing import Dict +from typing import Final from typing import final from typing import Generator from typing import Iterable @@ -55,6 +56,7 @@ from _pytest.config import hookimpl from _pytest.config.argparsing import Parser from _pytest.deprecated import check_ispytest +from _pytest.deprecated import ITEM_FUNCARGS_MEMBERS from _pytest.fixtures import FixtureDef from _pytest.fixtures import FixtureRequest from _pytest.fixtures import FuncFixtureInfo @@ -1657,6 +1659,19 @@ def write_docstring(tw: TerminalWriter, doc: str, indent: str = " ") -> None: tw.line(indent + line) +class DeprecatingFuncArgs(Dict[str, object]): + def __init__(self, initialnames: Sequence[str]) -> None: + super().__init__() + self.warned: bool = False + self.initialnames: Final = initialnames + + def __getitem__(self, key: str) -> object: + if not self.warned and key not in self.initialnames: + self.warned = True + warnings.warn(ITEM_FUNCARGS_MEMBERS, stacklevel=2) + return super().__getitem__(key) + + class Function(PyobjMixin, nodes.Item): """Item responsible for setting up and executing a Python test function. @@ -1745,7 +1760,9 @@ def from_parent(cls, parent, **kw): # todo: determine sound type limitations return super().from_parent(parent=parent, **kw) def _initrequest(self) -> None: - self.funcargs: Dict[str, object] = {} + self.funcargs: Dict[str, object] = DeprecatingFuncArgs( + self._fixtureinfo.initialnames + ) self._request = fixtures.TopRequest(self, _ispytest=True) @property diff --git a/testing/deprecated_test.py b/testing/deprecated_test.py index ebff49ce6d9..6d1b79e1cf4 100644 --- a/testing/deprecated_test.py +++ b/testing/deprecated_test.py @@ -133,3 +133,32 @@ def foo(): raise NotImplementedError() assert len(record) == 2 # one for each mark decorator + + +def test_deprecated_access_to_item_funcargs(pytester: Pytester) -> None: + pytester.makepyfile( + """ + import pytest + + @pytest.fixture + def fixture1(): + return None + + @pytest.fixture + def fixture2(fixture1): + return None + + def test(request, fixture2): + with pytest.warns( + pytest.PytestRemovedIn9Warning, + match=r"Accessing `item.funcargs` with a fixture", + ) as record: + request.node.funcargs["fixture1"] + assert request.node.funcargs.warned + request.node.funcargs.warned = False + request.node.funcargs["fixture2"] + assert len(record) == 1 + """ + ) + output = pytester.runpytest() + output.assert_outcomes(passed=1) diff --git a/testing/test_doctest.py b/testing/test_doctest.py index f4d3155c435..28b3d65c7ef 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -881,6 +881,41 @@ def test_foo(): result = pytester.runpytest(p, "--doctest-modules") result.stdout.fnmatch_lines(["*collected 1 item*"]) + def test_deprecated_access_to_item_funcargs(self, pytester: Pytester): + pytester.makeconftest( + """ + import pytest + + @pytest.fixture + def fixture1(): + return None + + @pytest.fixture(autouse=True) + def fixture2(fixture1): + return None + """ + ) + pytester.makepyfile( + """ + ''' + >>> import pytest + >>> request = getfixture('request') + >>> with pytest.warns( + ... pytest.PytestRemovedIn9Warning, + ... match=r"Accessing `item.funcargs` with a fixture", + ... ) as record: + ... request.node.funcargs["fixture1"] + ... assert request.node.funcargs.warned + ... request.node.funcargs.warned = False + ... request.node.funcargs["fixture2"] + >>> len(record) + 1 + ''' + """ + ) + result = pytester.runpytest("--doctest-modules") + result.assert_outcomes(passed=1) + class TestLiterals: @pytest.mark.parametrize("config_mode", ["ini", "comment"])