Skip to content

Commit a8ac094

Browse files
authored
Merge pull request pytest-dev#14567 from pytest-dev/more-visibility-deprecate
fixtures: deprecate `FixtureDef.has_location` and `None` global fixture visibility
2 parents e5620cd + 547eb13 commit a8ac094

8 files changed

Lines changed: 109 additions & 47 deletions

File tree

changelog/14004.deprecation.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@ Passing ``baseid`` to :class:`~pytest.FixtureDef` or ``nodeid`` strings to fixtu
22

33
Use the ``node`` parameter instead for fixture scoping. This enables more robust node-based
44
matching instead of string prefix matching.
5+
If you've used ``nodeid=None``, pass ``node=session`` instead.
56

67
This will be removed in pytest 10.

changelog/14513.deprecation.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
The private ``FixtureDef.has_location`` attribute is now deprecated and will be removed in pytest 10.
2+
See :ref:`fixturedef-has-location-deprecated` for details.

doc/en/deprecations.rst

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ Below is a complete list of all pytest features which are considered deprecated.
2020
Passing ``baseid``/``nodeid`` strings to fixture registration APIs
2121
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
2222

23-
.. deprecated:: 9.2
23+
.. deprecated:: 9.1
2424

2525
Passing ``baseid`` to :class:`~pytest.FixtureDef` or ``nodeid`` strings to
2626
``FixtureManager._register_fixture`` and ``FixtureManager.parsefactories``
@@ -39,9 +39,25 @@ node-based matching instead of fragile string prefix matching.
3939
fixture_manager.parsefactories(holder=plugin_obj, node=directory_node)
4040
fixture_manager._register_fixture(name="fix", func=func, node=directory_node)
4141
42+
The equivalent of passing ``nodeid=None`` (global visibility) is ``node=session``.
43+
4244
In pytest 10, the ``baseid`` and ``nodeid`` string parameters will be removed.
4345

4446

47+
.. _fixturedef-has-location-deprecated:
48+
49+
``FixtureDef.has_location``
50+
~~~~~~~~~~~~~~~~~~~~~~~~~~~
51+
52+
.. deprecated:: 9.1
53+
54+
The private ``FixtureDef.has_location`` attribute is deprecated and will be removed in pytest 10.
55+
56+
It indicated whether a fixture was found from a node or a conftest in the collection tree (as opposed to a non-conftest plugin).
57+
It was used to determine the override order of fixtures, pushing fixtures with "no location" to the front of the override chain (such that they are chosen last).
58+
The override order is now determined by the visibility of the fixtures in the collection tree, making this distinction obsolete.
59+
60+
4561
.. _console-main:
4662

4763
``pytest.console_main()``

src/_pytest/deprecated.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,11 @@
123123
"Pass node instead for fixture scoping."
124124
)
125125

126+
FIXTUREDEF_HAS_LOCATION_DEPRECATED = PytestRemovedIn10Warning(
127+
"FixtureDef.has_location is deprecated and will be removed in pytest 10. "
128+
"See https://docs.pytest.org/en/stable/deprecations.html#fixturedef-has-location-deprecated"
129+
)
130+
126131
PARSEFACTORIES_NODEID_DEPRECATED = PytestRemovedIn10Warning(
127132
"Passing nodeid string to parsefactories is deprecated. "
128133
"Use parsefactories(holder=obj, node=node) instead."

src/_pytest/fixtures.py

Lines changed: 51 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@
3232
from typing import TypeVar
3333
import warnings
3434

35-
from .compat import deprecated
3635
import _pytest
3736
from _pytest import nodes
3837
from _pytest._code import getfslineno
@@ -41,6 +40,7 @@
4140
from _pytest._code.code import TerminalRepr
4241
from _pytest._io import TerminalWriter
4342
from _pytest.compat import assert_never
43+
from _pytest.compat import deprecated
4444
from _pytest.compat import get_real_func
4545
from _pytest.compat import getfuncargnames
4646
from _pytest.compat import getimfunc
@@ -60,6 +60,7 @@
6060
from _pytest.deprecated import FIXTURE_BASEID_DEPRECATED
6161
from _pytest.deprecated import FIXTURE_GETFIXTUREVALUE_DURING_TEARDOWN
6262
from _pytest.deprecated import FIXTURE_NODEID_DEPRECATED
63+
from _pytest.deprecated import FIXTUREDEF_HAS_LOCATION_DEPRECATED
6364
from _pytest.deprecated import PARSEFACTORIES_NODEID_DEPRECATED
6465
from _pytest.deprecated import YIELD_FIXTURE
6566
from _pytest.main import Session
@@ -140,9 +141,8 @@ def is_visibility_more_specific(
140141
than that of ``other``, i.e. ``candidate`` is defined on a strict descendant
141142
in the collection tree of where ``other`` is defined."""
142143
if candidate.node is None or other.node is None:
143-
# Fallback for fixtures registered with a string nodeid (deprecated) or
144-
# with global visibility (no node). In this case compare baseids, which
145-
# are nodeid prefixes.
144+
# Fallback for fixtures registered with a string nodeid (deprecated).
145+
# In this case compare baseids, which are nodeid prefixes.
146146
# This branch can be removed once baseid deprecation is done (pytest 10).
147147
if candidate.baseid == other.baseid:
148148
return False
@@ -1056,26 +1056,27 @@ class FixtureDef(Generic[FixtureValue]):
10561056
def __init__(
10571057
self,
10581058
config: Config,
1059-
baseid: str | None,
1059+
baseid: str | None | NotSetType,
10601060
argname: str,
10611061
func: _FixtureFunc[FixtureValue],
10621062
scope: Scope | ScopeName | Callable[[str, Config], ScopeName] | None,
10631063
params: Sequence[object] | None,
10641064
ids: tuple[object | None, ...] | Callable[[Any], object | None] | None = None,
10651065
*,
1066-
_ispytest: bool = False,
1066+
node: nodes.Node | NotSetType = NOTSET,
10671067
# only used in a deprecationwarning msg, can be removed in pytest9
10681068
_autouse: bool = False,
1069-
node: nodes.Node | None = None,
1069+
_ispytest: bool = False,
10701070
) -> None:
10711071
check_ispytest(_ispytest)
1072-
# Emit deprecation warning if baseid string is used when node could be provided.
1073-
# baseid=None (global plugins) and baseid="" (synthetic fixtures) are fine.
1074-
if baseid and node is None:
1072+
# Emit deprecation warning if deprecated baseid string is used.
1073+
if node is NOTSET:
10751074
warnings.warn(FIXTURE_BASEID_DEPRECATED, stacklevel=2)
1075+
if baseid is NOTSET:
1076+
baseid = None
10761077
# The node where this fixture was defined, if available.
10771078
# Used for node-based matching which is more robust than string matching.
1078-
self.node: Final = node
1079+
self.node: Final = node if node is not NOTSET else None
10791080
# The "base" node ID for the fixture.
10801081
#
10811082
# This is a node ID prefix. A fixture is only available to a node (e.g.
@@ -1090,11 +1091,15 @@ def __init__(
10901091
#
10911092
# For other plugins, the baseid is the empty string (always matches).
10921093
# When node is available, baseid is derived from node.nodeid.
1093-
self.baseid: Final = node.nodeid if node is not None else (baseid or "")
1094+
#
1095+
# Deprecated: replaced by ``node``.
1096+
self.baseid: Final = node.nodeid if node is not NOTSET else (baseid or "")
10941097
# Whether the fixture was found from a node or a conftest in the
10951098
# collection tree. Will be false for fixtures defined in non-conftest
10961099
# plugins.
1097-
self.has_location: Final = node is not None or baseid is not None
1100+
#
1101+
# Deprecated: kept only to back the deprecated ``has_location`` property.
1102+
self._has_location: Final = node is not NOTSET or baseid is not None
10981103
# The fixture factory function.
10991104
self.func: Final = func
11001105
# The name by which the fixture may be requested.
@@ -1129,6 +1134,11 @@ def scope(self) -> ScopeName:
11291134
"""Scope string, one of "function", "class", "module", "package", "session"."""
11301135
return self._scope.value
11311136

1137+
@property
1138+
def has_location(self) -> bool:
1139+
warnings.warn(FIXTUREDEF_HAS_LOCATION_DEPRECATED, stacklevel=2)
1140+
return self._has_location
1141+
11321142
def addfinalizer(self, finalizer: Callable[[], object]) -> None:
11331143
self._finalizers.append(finalizer)
11341144

@@ -1243,11 +1253,12 @@ class RequestFixtureDef(FixtureDef[FixtureRequest]):
12431253
def __init__(self, request: FixtureRequest) -> None:
12441254
super().__init__(
12451255
config=request.config,
1246-
baseid=None,
1256+
baseid=NOTSET,
12471257
argname="request",
12481258
func=lambda: request,
12491259
scope=Scope.Function,
12501260
params=None,
1261+
node=request.node,
12511262
_ispytest=True,
12521263
)
12531264
self.cached_result = (request, [0], None)
@@ -1776,8 +1787,8 @@ def pytest_plugin_registered(self, plugin: _PluggyPlugin, plugin_name: str) -> N
17761787
# Store conftest for deferred parsing when its Directory is collected.
17771788
self._pending_conftests[conftest_dir] = plugin
17781789
else:
1779-
# Non-conftest plugins have global visibility (nodeid=None).
1780-
self.parsefactories(plugin, None)
1790+
# Non-conftest plugins have global visibility.
1791+
self.parsefactories(holder=plugin, node=self.session)
17811792

17821793
@hookimpl(wrapper=True)
17831794
def pytest_make_collect_report(
@@ -1926,12 +1937,12 @@ def _register_fixture(
19261937
*,
19271938
name: str,
19281939
func: _FixtureFunc[object],
1929-
nodeid: str | None = None,
1940+
nodeid: str | None | NotSetType = NOTSET,
19301941
scope: Scope | ScopeName | Callable[[str, Config], ScopeName] = "function",
19311942
params: Sequence[object] | None = None,
19321943
ids: tuple[object | None, ...] | Callable[[Any], object | None] | None = None,
19331944
autouse: bool = False,
1934-
node: nodes.Node | None = None,
1945+
node: nodes.Node | NotSetType = NOTSET,
19351946
) -> None:
19361947
"""Register a fixture
19371948
@@ -1955,13 +1966,12 @@ def _register_fixture(
19551966
:param autouse:
19561967
Whether this is an autouse fixture.
19571968
"""
1958-
# Emit deprecation warning if nodeid string is used when node could be provided.
1959-
# nodeid=None (global plugins) is fine.
1960-
if nodeid and node is None:
1969+
# Emit deprecation warning if nodeid string.
1970+
if nodeid is not NOTSET or node is NOTSET:
19611971
warnings.warn(FIXTURE_NODEID_DEPRECATED, stacklevel=2)
19621972
fixture_def = FixtureDef(
19631973
config=self.config,
1964-
baseid=nodeid if node is None else None,
1974+
baseid=nodeid,
19651975
argname=name,
19661976
func=func,
19671977
scope=scope,
@@ -1991,9 +2001,9 @@ def _register_fixture(
19912001
else:
19922002
faclist.append(fixture_def)
19932003
if autouse:
1994-
if node is not None:
2004+
if node is not NOTSET:
19952005
self._node_autousenames.setdefault(node, []).append(name)
1996-
elif nodeid:
2006+
elif nodeid is not NOTSET and nodeid is not None:
19972007
# Legacy: plugin passed nodeid string without node reference.
19982008
self._nodeid_autousenames.setdefault(nodeid, []).append(name)
19992009
else:
@@ -2008,6 +2018,9 @@ def parsefactories(
20082018
raise NotImplementedError()
20092019

20102020
@overload
2021+
@deprecated(
2022+
"parsefactories(obj, nodeid) is deprecated, use parsefactories(holder=obj, node=node) instead"
2023+
)
20112024
def parsefactories(
20122025
self,
20132026
node_or_obj: object,
@@ -2018,8 +2031,8 @@ def parsefactories(
20182031
@overload
20192032
def parsefactories(
20202033
self,
2021-
node_or_obj: None = ...,
2022-
nodeid: None = ...,
2034+
node_or_obj: NotSetType = ...,
2035+
nodeid: NotSetType = ...,
20232036
*,
20242037
holder: object,
20252038
node: nodes.Node,
@@ -2028,44 +2041,42 @@ def parsefactories(
20282041

20292042
def parsefactories(
20302043
self,
2031-
node_or_obj: nodes.Node | object | None = None,
2032-
nodeid: str | NotSetType | None = NOTSET,
2044+
node_or_obj: nodes.Node | object | NotSetType = NOTSET,
2045+
nodeid: str | None | NotSetType = NOTSET,
20332046
*,
2034-
holder: object | None = None,
2035-
node: nodes.Node | None = None,
2047+
holder: object | NotSetType = NOTSET,
2048+
node: nodes.Node | NotSetType = NOTSET,
20362049
) -> None:
20372050
"""Collect fixtures from a collection node or object.
20382051
20392052
Found fixtures are parsed into `FixtureDef`s and saved.
20402053
20412054
The preferred API uses keyword-only arguments:
20422055
- ``holder``: The object to scan for fixtures.
2043-
- ``node``: The node determining fixture visibility scope.
2056+
- ``node``: The node determining fixture visibility.
20442057
20452058
Legacy positional API (translated internally):
20462059
- ``parsefactories(node)``: Uses node.obj as holder, node for scope.
20472060
- ``parsefactories(obj, nodeid)``: Uses obj as holder, nodeid string for scope.
20482061
"""
20492062
# Translate legacy API to holder/node sources of truth
20502063
# Either effective_node or effective_nodeid will be set, not both
2051-
effective_node: nodes.Node | None = None
2052-
effective_nodeid: str | None = None
2064+
effective_node: nodes.Node | NotSetType = NOTSET
2065+
effective_nodeid: str | None | NotSetType = NOTSET
20532066

2054-
if holder is not None:
2067+
if holder is not NOTSET:
20552068
# New API: holder and node explicitly provided
20562069
holderobj = holder
20572070
effective_node = node
2058-
elif node_or_obj is None:
2071+
elif node_or_obj is NOTSET:
20592072
raise TypeError("parsefactories() requires holder or node_or_obj")
20602073
elif nodeid is not NOTSET:
2061-
# Legacy: parsefactories(obj, nodeid) - string-based scoping only
2062-
# Only warn if a non-None nodeid string is passed (None means global plugin)
2063-
if nodeid is not None:
2064-
warnings.warn(PARSEFACTORIES_NODEID_DEPRECATED, stacklevel=2)
2074+
# Legacy: parsefactories(obj, nodeid) - string-based scoping only.
2075+
warnings.warn(PARSEFACTORIES_NODEID_DEPRECATED, stacklevel=2)
20652076
holderobj = node_or_obj
20662077
effective_nodeid = nodeid
20672078
else:
2068-
# Legacy: parsefactories(node) - node has .obj attribute
2079+
# parsefactories(node) - node has .obj attribute
20692080
assert isinstance(node_or_obj, nodes.Node)
20702081
holderobj = cast(object, node_or_obj.obj) # type: ignore[attr-defined]
20712082
effective_node = node_or_obj

src/_pytest/python.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1164,15 +1164,16 @@ class DirectParamFixtureDef(FixtureDef[FixtureValue]):
11641164
usually behaves like any other FixtureDef.
11651165
"""
11661166

1167-
def __init__(self, *, config: Config, argname: str, scope: Scope) -> None:
1167+
def __init__(self, *, node: nodes.Node, argname: str, scope: Scope) -> None:
11681168
super().__init__(
1169-
config=config,
1170-
baseid="",
1169+
config=node.config,
1170+
baseid=NOTSET,
11711171
argname=argname,
11721172
func=get_direct_param_fixture_func,
11731173
scope=scope,
11741174
params=None,
11751175
ids=None,
1176+
node=node,
11761177
_ispytest=True,
11771178
)
11781179

@@ -1395,7 +1396,7 @@ def parametrize(
13951396
fixturedef = name2directparamfixturedef[argname]
13961397
else:
13971398
fixturedef = DirectParamFixtureDef(
1398-
config=self.config,
1399+
node=self.definition.session,
13991400
argname=argname,
14001401
scope=scope_,
14011402
)

testing/deprecated_test.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ def fix_b():
174174
fm.parsefactories(mod_none, None)
175175
176176
nodeid_warns = [x for x in w if "parsefactories" in str(x.message)]
177-
assert len(nodeid_warns) == 1, f"Expected 1 warning, got: {w}"
177+
assert len(nodeid_warns) == 2, f"Expected 2 warning, got: {w}"
178178
"""
179179
)
180180
pytester.makepyfile(
@@ -287,3 +287,26 @@ def test_scoped_invisible(request):
287287
)
288288
result = pytester.runpytest("-W", "ignore::pytest.PytestRemovedIn10Warning")
289289
result.assert_outcomes(passed=2)
290+
291+
def test_fixturedef_has_location_deprecated(self, pytester: Pytester) -> None:
292+
"""Accessing FixtureDef.has_location warns."""
293+
pytester.makepyfile(
294+
"""
295+
import pytest
296+
297+
@pytest.fixture
298+
def fix():
299+
return 1
300+
301+
def test_it(request):
302+
fixturedef = request._fixturemanager.getfixturedefs(
303+
"fix", request._pyfuncitem
304+
)[0]
305+
with pytest.warns(
306+
pytest.PytestRemovedIn10Warning, match="has_location"
307+
):
308+
assert fixturedef.has_location is True
309+
"""
310+
)
311+
result = pytester.runpytest()
312+
result.assert_outcomes(passed=1)

testing/python/metafunc.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
import textwrap
1111
from typing import Any
1212
from typing import cast
13+
from typing import ClassVar
1314

1415
import hypothesis
1516
from hypothesis import strategies
@@ -44,7 +45,9 @@ class FixtureManagerMock:
4445

4546
@dataclasses.dataclass
4647
class SessionMock:
48+
config: Any
4749
_fixturemanager: FixtureManagerMock
50+
nodeid: ClassVar = ""
4851

4952
@dataclasses.dataclass
5053
class DefinitionMock(python.FunctionDefinition):
@@ -55,7 +58,7 @@ class DefinitionMock(python.FunctionDefinition):
5558
fixtureinfo: Any = FuncFixtureInfoMock(names)
5659
definition: Any = DefinitionMock._create(obj=func, _nodeid="mock::nodeid")
5760
definition._fixtureinfo = fixtureinfo
58-
definition.session = SessionMock(FixtureManagerMock({}))
61+
definition.session = SessionMock(config, FixtureManagerMock({}))
5962
return python.Metafunc(definition, fixtureinfo, config, _ispytest=True)
6063

6164
def test_no_funcargs(self) -> None:

0 commit comments

Comments
 (0)