Skip to content

Commit 09b7873

Browse files
Move fixtures.py::add_funcarg_pseudo_fixture_def to Metafunc.parametrize (#11220)
To remove fixtures.py::add_funcargs_pseudo_fixture_def and add its logic i.e. registering funcargs as params and making corresponding fixturedefs, right to Metafunc.parametrize in which parametrization takes place. To remove funcargs from metafunc attributes as we populate metafunc params and make pseudo fixturedefs simultaneously and there's no need to keep funcargs separately.
1 parent b2186e2 commit 09b7873

File tree

4 files changed

+167
-130
lines changed

4 files changed

+167
-130
lines changed

src/_pytest/fixtures.py

Lines changed: 0 additions & 88 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,6 @@
6363
from _pytest.scope import _ScopeName
6464
from _pytest.scope import HIGH_SCOPES
6565
from _pytest.scope import Scope
66-
from _pytest.stash import StashKey
6766

6867

6968
if TYPE_CHECKING:
@@ -147,89 +146,6 @@ def get_scope_node(
147146
assert_never(scope)
148147

149148

150-
# Used for storing artificial fixturedefs for direct parametrization.
151-
name2pseudofixturedef_key = StashKey[Dict[str, "FixtureDef[Any]"]]()
152-
153-
154-
def add_funcarg_pseudo_fixture_def(
155-
collector: nodes.Collector, metafunc: "Metafunc", fixturemanager: "FixtureManager"
156-
) -> None:
157-
import _pytest.python
158-
159-
# This function will transform all collected calls to functions
160-
# if they use direct funcargs (i.e. direct parametrization)
161-
# because we want later test execution to be able to rely on
162-
# an existing FixtureDef structure for all arguments.
163-
# XXX we can probably avoid this algorithm if we modify CallSpec2
164-
# to directly care for creating the fixturedefs within its methods.
165-
if not metafunc._calls[0].funcargs:
166-
# This function call does not have direct parametrization.
167-
return
168-
# Collect funcargs of all callspecs into a list of values.
169-
arg2params: Dict[str, List[object]] = {}
170-
arg2scope: Dict[str, Scope] = {}
171-
for callspec in metafunc._calls:
172-
for argname, argvalue in callspec.funcargs.items():
173-
assert argname not in callspec.params
174-
callspec.params[argname] = argvalue
175-
arg2params_list = arg2params.setdefault(argname, [])
176-
callspec.indices[argname] = len(arg2params_list)
177-
arg2params_list.append(argvalue)
178-
if argname not in arg2scope:
179-
scope = callspec._arg2scope.get(argname, Scope.Function)
180-
arg2scope[argname] = scope
181-
callspec.funcargs.clear()
182-
183-
# Register artificial FixtureDef's so that later at test execution
184-
# time we can rely on a proper FixtureDef to exist for fixture setup.
185-
arg2fixturedefs = metafunc._arg2fixturedefs
186-
for argname, valuelist in arg2params.items():
187-
# If we have a scope that is higher than function, we need
188-
# to make sure we only ever create an according fixturedef on
189-
# a per-scope basis. We thus store and cache the fixturedef on the
190-
# node related to the scope.
191-
scope = arg2scope[argname]
192-
node = None
193-
if scope is not Scope.Function:
194-
node = get_scope_node(collector, scope)
195-
if node is None:
196-
# If used class scope and there is no class, use module-level
197-
# collector (for now).
198-
if scope is Scope.Class:
199-
assert isinstance(collector, _pytest.python.Module)
200-
node = collector
201-
# If used package scope and there is no package, use session
202-
# (for now).
203-
elif scope is Scope.Package:
204-
node = collector.session
205-
else:
206-
assert False, f"Unhandled missing scope: {scope}"
207-
if node is None:
208-
name2pseudofixturedef = None
209-
else:
210-
default: Dict[str, FixtureDef[Any]] = {}
211-
name2pseudofixturedef = node.stash.setdefault(
212-
name2pseudofixturedef_key, default
213-
)
214-
if name2pseudofixturedef is not None and argname in name2pseudofixturedef:
215-
arg2fixturedefs[argname] = [name2pseudofixturedef[argname]]
216-
else:
217-
fixturedef = FixtureDef(
218-
fixturemanager=fixturemanager,
219-
baseid="",
220-
argname=argname,
221-
func=get_direct_param_fixture_func,
222-
scope=arg2scope[argname],
223-
params=valuelist,
224-
unittest=False,
225-
ids=None,
226-
_ispytest=True,
227-
)
228-
arg2fixturedefs[argname] = [fixturedef]
229-
if name2pseudofixturedef is not None:
230-
name2pseudofixturedef[argname] = fixturedef
231-
232-
233149
def getfixturemarker(obj: object) -> Optional["FixtureFunctionMarker"]:
234150
"""Return fixturemarker or None if it doesn't exist or raised
235151
exceptions."""
@@ -365,10 +281,6 @@ def reorder_items_atscope(
365281
return items_done
366282

367283

368-
def get_direct_param_fixture_func(request: "FixtureRequest") -> Any:
369-
return request.param
370-
371-
372284
@dataclasses.dataclass(frozen=True)
373285
class FuncFixtureInfo:
374286
"""Fixture-related information for a fixture-requesting item (e.g. test

src/_pytest/python.py

Lines changed: 75 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@
4040
from _pytest._io import TerminalWriter
4141
from _pytest._io.saferepr import saferepr
4242
from _pytest.compat import ascii_escaped
43-
from _pytest.compat import assert_never
4443
from _pytest.compat import get_default_arg_names
4544
from _pytest.compat import get_real_func
4645
from _pytest.compat import getimfunc
@@ -59,7 +58,10 @@
5958
from _pytest.deprecated import check_ispytest
6059
from _pytest.deprecated import INSTANCE_COLLECTOR
6160
from _pytest.deprecated import NOSE_SUPPORT_METHOD
61+
from _pytest.fixtures import FixtureDef
62+
from _pytest.fixtures import FixtureRequest
6263
from _pytest.fixtures import FuncFixtureInfo
64+
from _pytest.fixtures import get_scope_node
6365
from _pytest.main import Session
6466
from _pytest.mark import MARK_GEN
6567
from _pytest.mark import ParameterSet
@@ -77,6 +79,7 @@
7779
from _pytest.pathlib import visit
7880
from _pytest.scope import _ScopeName
7981
from _pytest.scope import Scope
82+
from _pytest.stash import StashKey
8083
from _pytest.warning_types import PytestCollectionWarning
8184
from _pytest.warning_types import PytestReturnNotNoneWarning
8285
from _pytest.warning_types import PytestUnhandledCoroutineWarning
@@ -493,13 +496,11 @@ def _genfunctions(self, name: str, funcobj) -> Iterator["Function"]:
493496
if not metafunc._calls:
494497
yield Function.from_parent(self, name=name, fixtureinfo=fixtureinfo)
495498
else:
496-
# Add funcargs() as fixturedefs to fixtureinfo.arg2fixturedefs.
497-
fm = self.session._fixturemanager
498-
fixtures.add_funcarg_pseudo_fixture_def(self, metafunc, fm)
499-
500-
# Add_funcarg_pseudo_fixture_def may have shadowed some fixtures
501-
# with direct parametrization, so make sure we update what the
502-
# function really needs.
499+
# Direct parametrizations taking place in module/class-specific
500+
# `metafunc.parametrize` calls may have shadowed some fixtures, so make sure
501+
# we update what the function really needs a.k.a its fixture closure. Note that
502+
# direct parametrizations using `@pytest.mark.parametrize` have already been considered
503+
# into making the closure using `ignore_args` arg to `getfixtureclosure`.
503504
fixtureinfo.prune_dependency_tree()
504505

505506
for callspec in metafunc._calls:
@@ -1116,11 +1117,8 @@ class CallSpec2:
11161117
and stored in item.callspec.
11171118
"""
11181119

1119-
# arg name -> arg value which will be passed to the parametrized test
1120-
# function (direct parameterization).
1121-
funcargs: Dict[str, object] = dataclasses.field(default_factory=dict)
1122-
# arg name -> arg value which will be passed to a fixture of the same name
1123-
# (indirect parametrization).
1120+
# arg name -> arg value which will be passed to a fixture or pseudo-fixture
1121+
# of the same name. (indirect or direct parametrization respectively)
11241122
params: Dict[str, object] = dataclasses.field(default_factory=dict)
11251123
# arg name -> arg index.
11261124
indices: Dict[str, int] = dataclasses.field(default_factory=dict)
@@ -1134,32 +1132,23 @@ class CallSpec2:
11341132
def setmulti(
11351133
self,
11361134
*,
1137-
valtypes: Mapping[str, "Literal['params', 'funcargs']"],
11381135
argnames: Iterable[str],
11391136
valset: Iterable[object],
11401137
id: str,
11411138
marks: Iterable[Union[Mark, MarkDecorator]],
11421139
scope: Scope,
11431140
param_index: int,
11441141
) -> "CallSpec2":
1145-
funcargs = self.funcargs.copy()
11461142
params = self.params.copy()
11471143
indices = self.indices.copy()
11481144
arg2scope = self._arg2scope.copy()
11491145
for arg, val in zip(argnames, valset):
1150-
if arg in params or arg in funcargs:
1146+
if arg in params:
11511147
raise ValueError(f"duplicate parametrization of {arg!r}")
1152-
valtype_for_arg = valtypes[arg]
1153-
if valtype_for_arg == "params":
1154-
params[arg] = val
1155-
elif valtype_for_arg == "funcargs":
1156-
funcargs[arg] = val
1157-
else:
1158-
assert_never(valtype_for_arg)
1148+
params[arg] = val
11591149
indices[arg] = param_index
11601150
arg2scope[arg] = scope
11611151
return CallSpec2(
1162-
funcargs=funcargs,
11631152
params=params,
11641153
indices=indices,
11651154
_arg2scope=arg2scope,
@@ -1178,6 +1167,14 @@ def id(self) -> str:
11781167
return "-".join(self._idlist)
11791168

11801169

1170+
def get_direct_param_fixture_func(request: FixtureRequest) -> Any:
1171+
return request.param
1172+
1173+
1174+
# Used for storing pseudo fixturedefs for direct parametrization.
1175+
name2pseudofixturedef_key = StashKey[Dict[str, FixtureDef[Any]]]()
1176+
1177+
11811178
@final
11821179
class Metafunc:
11831180
"""Objects passed to the :hook:`pytest_generate_tests` hook.
@@ -1320,8 +1317,6 @@ def parametrize(
13201317

13211318
self._validate_if_using_arg_names(argnames, indirect)
13221319

1323-
arg_values_types = self._resolve_arg_value_types(argnames, indirect)
1324-
13251320
# Use any already (possibly) generated ids with parametrize Marks.
13261321
if _param_mark and _param_mark._param_ids_from:
13271322
generated_ids = _param_mark._param_ids_from._param_ids_generated
@@ -1336,6 +1331,60 @@ def parametrize(
13361331
if _param_mark and _param_mark._param_ids_from and generated_ids is None:
13371332
object.__setattr__(_param_mark._param_ids_from, "_param_ids_generated", ids)
13381333

1334+
# Add funcargs as fixturedefs to fixtureinfo.arg2fixturedefs by registering
1335+
# artificial "pseudo" FixtureDef's so that later at test execution time we can
1336+
# rely on a proper FixtureDef to exist for fixture setup.
1337+
arg2fixturedefs = self._arg2fixturedefs
1338+
node = None
1339+
# If we have a scope that is higher than function, we need
1340+
# to make sure we only ever create an according fixturedef on
1341+
# a per-scope basis. We thus store and cache the fixturedef on the
1342+
# node related to the scope.
1343+
if scope_ is not Scope.Function:
1344+
collector = self.definition.parent
1345+
assert collector is not None
1346+
node = get_scope_node(collector, scope_)
1347+
if node is None:
1348+
# If used class scope and there is no class, use module-level
1349+
# collector (for now).
1350+
if scope_ is Scope.Class:
1351+
assert isinstance(collector, _pytest.python.Module)
1352+
node = collector
1353+
# If used package scope and there is no package, use session
1354+
# (for now).
1355+
elif scope_ is Scope.Package:
1356+
node = collector.session
1357+
else:
1358+
assert False, f"Unhandled missing scope: {scope}"
1359+
if node is None:
1360+
name2pseudofixturedef = None
1361+
else:
1362+
default: Dict[str, FixtureDef[Any]] = {}
1363+
name2pseudofixturedef = node.stash.setdefault(
1364+
name2pseudofixturedef_key, default
1365+
)
1366+
arg_values_types = self._resolve_arg_value_types(argnames, indirect)
1367+
for argname in argnames:
1368+
if arg_values_types[argname] == "params":
1369+
continue
1370+
if name2pseudofixturedef is not None and argname in name2pseudofixturedef:
1371+
fixturedef = name2pseudofixturedef[argname]
1372+
else:
1373+
fixturedef = FixtureDef(
1374+
fixturemanager=self.definition.session._fixturemanager,
1375+
baseid="",
1376+
argname=argname,
1377+
func=get_direct_param_fixture_func,
1378+
scope=scope_,
1379+
params=None,
1380+
unittest=False,
1381+
ids=None,
1382+
_ispytest=True,
1383+
)
1384+
if name2pseudofixturedef is not None:
1385+
name2pseudofixturedef[argname] = fixturedef
1386+
arg2fixturedefs[argname] = [fixturedef]
1387+
13391388
# Create the new calls: if we are parametrize() multiple times (by applying the decorator
13401389
# more than once) then we accumulate those calls generating the cartesian product
13411390
# of all calls.
@@ -1345,7 +1394,6 @@ def parametrize(
13451394
zip(ids, parametersets)
13461395
):
13471396
newcallspec = callspec.setmulti(
1348-
valtypes=arg_values_types,
13491397
argnames=argnames,
13501398
valset=param_set.values,
13511399
id=param_id,

testing/example_scripts/issue_519.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,13 @@ def checked_order():
2222
assert order == [
2323
("issue_519.py", "fix1", "arg1v1"),
2424
("test_one[arg1v1-arg2v1]", "fix2", "arg2v1"),
25-
("test_two[arg1v1-arg2v1]", "fix2", "arg2v1"),
2625
("test_one[arg1v1-arg2v2]", "fix2", "arg2v2"),
26+
("test_two[arg1v1-arg2v1]", "fix2", "arg2v1"),
2727
("test_two[arg1v1-arg2v2]", "fix2", "arg2v2"),
2828
("issue_519.py", "fix1", "arg1v2"),
2929
("test_one[arg1v2-arg2v1]", "fix2", "arg2v1"),
30-
("test_two[arg1v2-arg2v1]", "fix2", "arg2v1"),
3130
("test_one[arg1v2-arg2v2]", "fix2", "arg2v2"),
31+
("test_two[arg1v2-arg2v1]", "fix2", "arg2v1"),
3232
("test_two[arg1v2-arg2v2]", "fix2", "arg2v2"),
3333
]
3434

0 commit comments

Comments
 (0)