Skip to content
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

Feed a test only with initialnames, not the whole fixture closure #11284

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
3 changes: 3 additions & 0 deletions changelog/11284.deprecation.rst
Original file line number Diff line number Diff line change
@@ -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.
11 changes: 11 additions & 0 deletions doc/en/deprecations.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 <pytest.FixtureRequest.getfixturevalue>` instead.

.. _nose-deprecation:

Expand Down
2 changes: 1 addition & 1 deletion doc/en/example/simple.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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 = ""
Expand Down
7 changes: 4 additions & 3 deletions src/_pytest/assertion/rewrite.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
7 changes: 7 additions & 0 deletions src/_pytest/deprecated.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
5 changes: 4 additions & 1 deletion src/_pytest/doctest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down
3 changes: 2 additions & 1 deletion src/_pytest/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
19 changes: 18 additions & 1 deletion src/_pytest/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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
Expand Down
29 changes: 29 additions & 0 deletions testing/deprecated_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
35 changes: 35 additions & 0 deletions testing/test_doctest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"])
Expand Down