14
14
from pathlib import Path
15
15
from typing import Any
16
16
from typing import Callable
17
+ from typing import cast
17
18
from typing import Dict
18
19
from typing import final
19
20
from typing import Generator
59
60
from _pytest .deprecated import check_ispytest
60
61
from _pytest .deprecated import INSTANCE_COLLECTOR
61
62
from _pytest .deprecated import NOSE_SUPPORT_METHOD
63
+ from _pytest .fixtures import FixtureDef
64
+ from _pytest .fixtures import FixtureRequest
62
65
from _pytest .fixtures import FuncFixtureInfo
66
+ from _pytest .fixtures import get_scope_node
63
67
from _pytest .main import Session
64
68
from _pytest .mark import MARK_GEN
65
69
from _pytest .mark import ParameterSet
77
81
from _pytest .pathlib import visit
78
82
from _pytest .scope import _ScopeName
79
83
from _pytest .scope import Scope
84
+ from _pytest .stash import StashKey
80
85
from _pytest .warning_types import PytestCollectionWarning
81
86
from _pytest .warning_types import PytestReturnNotNoneWarning
82
87
from _pytest .warning_types import PytestUnhandledCoroutineWarning
@@ -493,13 +498,8 @@ def _genfunctions(self, name: str, funcobj) -> Iterator["Function"]:
493
498
if not metafunc ._calls :
494
499
yield Function .from_parent (self , name = name , fixtureinfo = fixtureinfo )
495
500
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.
501
+ # Dynamic direct parametrization may have shadowed some fixtures,
502
+ # so make sure we update what the function really needs.
503
503
fixtureinfo .prune_dependency_tree ()
504
504
505
505
for callspec in metafunc ._calls :
@@ -1116,11 +1116,8 @@ class CallSpec2:
1116
1116
and stored in item.callspec.
1117
1117
"""
1118
1118
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).
1119
+ # arg name -> arg value which will be passed to a fixture or pseudo-fixture
1120
+ # of the same name. (indirect or direct parametrization respectively)
1124
1121
params : Dict [str , object ] = dataclasses .field (default_factory = dict )
1125
1122
# arg name -> arg index.
1126
1123
indices : Dict [str , int ] = dataclasses .field (default_factory = dict )
@@ -1134,32 +1131,23 @@ class CallSpec2:
1134
1131
def setmulti (
1135
1132
self ,
1136
1133
* ,
1137
- valtypes : Mapping [str , "Literal['params', 'funcargs']" ],
1138
1134
argnames : Iterable [str ],
1139
1135
valset : Iterable [object ],
1140
1136
id : str ,
1141
1137
marks : Iterable [Union [Mark , MarkDecorator ]],
1142
1138
scope : Scope ,
1143
1139
param_index : int ,
1144
1140
) -> "CallSpec2" :
1145
- funcargs = self .funcargs .copy ()
1146
1141
params = self .params .copy ()
1147
1142
indices = self .indices .copy ()
1148
1143
arg2scope = self ._arg2scope .copy ()
1149
1144
for arg , val in zip (argnames , valset ):
1150
- if arg in params or arg in funcargs :
1145
+ if arg in params :
1151
1146
raise ValueError (f"duplicate { 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 )
1147
+ params [arg ] = val
1159
1148
indices [arg ] = param_index
1160
1149
arg2scope [arg ] = scope
1161
1150
return CallSpec2 (
1162
- funcargs = funcargs ,
1163
1151
params = params ,
1164
1152
indices = indices ,
1165
1153
_arg2scope = arg2scope ,
@@ -1178,6 +1166,14 @@ def id(self) -> str:
1178
1166
return "-" .join (self ._idlist )
1179
1167
1180
1168
1169
+ def get_direct_param_fixture_func (request : FixtureRequest ) -> Any :
1170
+ return request .param
1171
+
1172
+
1173
+ # Used for storing artificial fixturedefs for direct parametrization.
1174
+ name2pseudofixturedef_key = StashKey [Dict [str , FixtureDef [Any ]]]()
1175
+
1176
+
1181
1177
@final
1182
1178
class Metafunc :
1183
1179
"""Objects passed to the :hook:`pytest_generate_tests` hook.
@@ -1319,8 +1315,6 @@ def parametrize(
1319
1315
1320
1316
self ._validate_if_using_arg_names (argnames , indirect )
1321
1317
1322
- arg_values_types = self ._resolve_arg_value_types (argnames , indirect )
1323
-
1324
1318
# Use any already (possibly) generated ids with parametrize Marks.
1325
1319
if _param_mark and _param_mark ._param_ids_from :
1326
1320
generated_ids = _param_mark ._param_ids_from ._param_ids_generated
@@ -1335,6 +1329,59 @@ def parametrize(
1335
1329
if _param_mark and _param_mark ._param_ids_from and generated_ids is None :
1336
1330
object .__setattr__ (_param_mark ._param_ids_from , "_param_ids_generated" , ids )
1337
1331
1332
+ # Add funcargs as fixturedefs to fixtureinfo.arg2fixturedefs by registering
1333
+ # artificial FixtureDef's so that later at test execution time we can rely
1334
+ # on a proper FixtureDef to exist for fixture setup.
1335
+ arg2fixturedefs = self ._arg2fixturedefs
1336
+ node = None
1337
+ # If we have a scope that is higher than function, we need
1338
+ # to make sure we only ever create an according fixturedef on
1339
+ # a per-scope basis. We thus store and cache the fixturedef on the
1340
+ # node related to the scope.
1341
+ if scope_ is not Scope .Function :
1342
+ collector = cast (nodes .Node , self .definition .parent )
1343
+ node = get_scope_node (collector , scope_ )
1344
+ if node is None :
1345
+ # If used class scope and there is no class, use module-level
1346
+ # collector (for now).
1347
+ if scope_ is Scope .Class :
1348
+ assert isinstance (collector , _pytest .python .Module )
1349
+ node = collector
1350
+ # If used package scope and there is no package, use session
1351
+ # (for now).
1352
+ elif scope_ is Scope .Package :
1353
+ node = collector .session
1354
+ else :
1355
+ assert_never (scope_ ) # type: ignore[arg-type]
1356
+ if node is None :
1357
+ name2pseudofixturedef = None
1358
+ else :
1359
+ default : Dict [str , FixtureDef [Any ]] = {}
1360
+ name2pseudofixturedef = node .stash .setdefault (
1361
+ name2pseudofixturedef_key , default
1362
+ )
1363
+ arg_values_types = self ._resolve_arg_value_types (argnames , indirect )
1364
+ for argname in argnames :
1365
+ if arg_values_types [argname ] == "params" :
1366
+ continue
1367
+ if name2pseudofixturedef is not None and argname in name2pseudofixturedef :
1368
+ fixturedef = name2pseudofixturedef [argname ]
1369
+ else :
1370
+ fixturedef = FixtureDef (
1371
+ fixturemanager = self .definition .session ._fixturemanager ,
1372
+ baseid = "" ,
1373
+ argname = argname ,
1374
+ func = get_direct_param_fixture_func ,
1375
+ scope = scope_ ,
1376
+ params = None ,
1377
+ unittest = False ,
1378
+ ids = None ,
1379
+ _ispytest = True ,
1380
+ )
1381
+ if name2pseudofixturedef is not None :
1382
+ name2pseudofixturedef [argname ] = fixturedef
1383
+ arg2fixturedefs [argname ] = [fixturedef ]
1384
+
1338
1385
# Create the new calls: if we are parametrize() multiple times (by applying the decorator
1339
1386
# more than once) then we accumulate those calls generating the cartesian product
1340
1387
# of all calls.
@@ -1344,7 +1391,6 @@ def parametrize(
1344
1391
zip (ids , parametersets )
1345
1392
):
1346
1393
newcallspec = callspec .setmulti (
1347
- valtypes = arg_values_types ,
1348
1394
argnames = argnames ,
1349
1395
valset = param_set .values ,
1350
1396
id = param_id ,
0 commit comments