This commit is contained in:
Sadra Barikbin 2024-02-02 22:17:08 +02:00 committed by GitHub
commit fe06dc0c4b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 114 additions and 7 deletions

View File

@ -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.

View File

@ -274,6 +274,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` resolved in future versions as we slowly get rid of the :pypi:`py`
dependency (see :issue:`9283` for a longer discussion). 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: .. _nose-deprecation:

View File

@ -843,7 +843,7 @@ case we just write some information out to a ``failures`` file:
mode = "a" if os.path.exists("failures") else "w" mode = "a" if os.path.exists("failures") else "w"
with open("failures", mode, encoding="utf-8") as f: with open("failures", mode, encoding="utf-8") as f:
# let's also access a fixture for the fun of it # 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"]) extra = " ({})".format(item.funcargs["tmp_path"])
else: else:
extra = "" extra = ""

View File

@ -18,6 +18,7 @@ import sys
import tokenize import tokenize
import types import types
from typing import Callable from typing import Callable
from typing import DefaultDict
from typing import Dict from typing import Dict
from typing import IO from typing import IO
from typing import Iterable from typing import Iterable
@ -671,9 +672,9 @@ class AssertionRewriter(ast.NodeVisitor):
else: else:
self.enable_assertion_pass_hook = False self.enable_assertion_pass_hook = False
self.source = source self.source = source
self.scope: tuple[ast.AST, ...] = () self.scope: Tuple[ast.AST, ...] = ()
self.variables_overwrite: defaultdict[ self.variables_overwrite: DefaultDict[
tuple[ast.AST, ...], Dict[str, str] Tuple[ast.AST, ...], Dict[str, str]
] = defaultdict(dict) ] = defaultdict(dict)
def run(self, mod: ast.Module) -> None: def run(self, mod: ast.Module) -> None:

View File

@ -50,6 +50,13 @@ MARKED_FIXTURE = PytestRemovedIn9Warning(
"See docs: https://docs.pytest.org/en/stable/deprecations.html#applying-a-mark-to-a-fixture-function" "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". # You want to make some `__init__` or function "private".
# #
# def my_private_function(some, args): # def my_private_function(some, args):

View File

@ -41,6 +41,7 @@ from _pytest.outcomes import OutcomeException
from _pytest.outcomes import skip from _pytest.outcomes import skip
from _pytest.pathlib import fnmatch_ex from _pytest.pathlib import fnmatch_ex
from _pytest.pathlib import import_path from _pytest.pathlib import import_path
from _pytest.python import DeprecatingFuncArgs
from _pytest.python import Module from _pytest.python import Module
from _pytest.python_api import approx from _pytest.python_api import approx
from _pytest.warning_types import PytestWarning from _pytest.warning_types import PytestWarning
@ -286,7 +287,9 @@ class DoctestItem(Item):
return super().from_parent(name=name, parent=parent, runner=runner, dtest=dtest) return super().from_parent(name=name, parent=parent, runner=runner, dtest=dtest)
def _initrequest(self) -> None: 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] self._request = TopRequest(self, _ispytest=True) # type: ignore[arg-type]
def setup(self) -> None: def setup(self) -> None:

View File

@ -679,7 +679,8 @@ class TopRequest(FixtureRequest):
def _fillfixtures(self) -> None: def _fillfixtures(self) -> None:
item = self._pyfuncitem item = self._pyfuncitem
for argname in item.fixturenames: fixturenames = getattr(item, "fixturenames", self.fixturenames)
for argname in fixturenames:
if argname not in item.funcargs: if argname not in item.funcargs:
item.funcargs[argname] = self.getfixturevalue(argname) item.funcargs[argname] = self.getfixturevalue(argname)

View File

@ -15,6 +15,7 @@ import types
from typing import Any from typing import Any
from typing import Callable from typing import Callable
from typing import Dict from typing import Dict
from typing import Final
from typing import final from typing import final
from typing import Generator from typing import Generator
from typing import Iterable from typing import Iterable
@ -56,6 +57,7 @@ from _pytest.config import ExitCode
from _pytest.config import hookimpl from _pytest.config import hookimpl
from _pytest.config.argparsing import Parser from _pytest.config.argparsing import Parser
from _pytest.deprecated import check_ispytest from _pytest.deprecated import check_ispytest
from _pytest.deprecated import ITEM_FUNCARGS_MEMBERS
from _pytest.fixtures import FixtureDef from _pytest.fixtures import FixtureDef
from _pytest.fixtures import FixtureRequest from _pytest.fixtures import FixtureRequest
from _pytest.fixtures import FuncFixtureInfo from _pytest.fixtures import FuncFixtureInfo
@ -1650,6 +1652,19 @@ def write_docstring(tw: TerminalWriter, doc: str, indent: str = " ") -> None:
tw.line(indent + line) 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): class Function(PyobjMixin, nodes.Item):
"""Item responsible for setting up and executing a Python test function. """Item responsible for setting up and executing a Python test function.
@ -1738,7 +1753,9 @@ class Function(PyobjMixin, nodes.Item):
return super().from_parent(parent=parent, **kw) return super().from_parent(parent=parent, **kw)
def _initrequest(self) -> None: 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) self._request = fixtures.TopRequest(self, _ispytest=True)
@property @property

View File

@ -134,3 +134,32 @@ def test_fixture_disallowed_between_marks():
raise NotImplementedError() raise NotImplementedError()
assert len(record) == 2 # one for each mark decorator 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)

View File

@ -878,6 +878,41 @@ class TestDoctests:
result = pytester.runpytest(p, "--doctest-modules") result = pytester.runpytest(p, "--doctest-modules")
result.stdout.fnmatch_lines(["*collected 1 item*"]) 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: class TestLiterals:
@pytest.mark.parametrize("config_mode", ["ini", "comment"]) @pytest.mark.parametrize("config_mode", ["ini", "comment"])