40
40
from _pytest ._io import TerminalWriter
41
41
from _pytest ._io .saferepr import saferepr
42
42
from _pytest .compat import ascii_escaped
43
- from _pytest .compat import assert_never
44
43
from _pytest .compat import get_default_arg_names
45
44
from _pytest .compat import get_real_func
46
45
from _pytest .compat import getimfunc
59
58
from _pytest .deprecated import check_ispytest
60
59
from _pytest .deprecated import INSTANCE_COLLECTOR
61
60
from _pytest .deprecated import NOSE_SUPPORT_METHOD
61
+ from _pytest .fixtures import FixtureDef
62
+ from _pytest .fixtures import FixtureRequest
62
63
from _pytest .fixtures import FuncFixtureInfo
64
+ from _pytest .fixtures import get_scope_node
63
65
from _pytest .main import Session
64
66
from _pytest .mark import MARK_GEN
65
67
from _pytest .mark import ParameterSet
77
79
from _pytest .pathlib import visit
78
80
from _pytest .scope import _ScopeName
79
81
from _pytest .scope import Scope
82
+ from _pytest .stash import StashKey
80
83
from _pytest .warning_types import PytestCollectionWarning
81
84
from _pytest .warning_types import PytestReturnNotNoneWarning
82
85
from _pytest .warning_types import PytestUnhandledCoroutineWarning
@@ -493,13 +496,11 @@ def _genfunctions(self, name: str, funcobj) -> Iterator["Function"]:
493
496
if not metafunc ._calls :
494
497
yield Function .from_parent (self , name = name , fixtureinfo = fixtureinfo )
495
498
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`.
503
504
fixtureinfo .prune_dependency_tree ()
504
505
505
506
for callspec in metafunc ._calls :
@@ -1116,11 +1117,8 @@ class CallSpec2:
1116
1117
and stored in item.callspec.
1117
1118
"""
1118
1119
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)
1124
1122
params : Dict [str , object ] = dataclasses .field (default_factory = dict )
1125
1123
# arg name -> arg index.
1126
1124
indices : Dict [str , int ] = dataclasses .field (default_factory = dict )
@@ -1134,32 +1132,23 @@ class CallSpec2:
1134
1132
def setmulti (
1135
1133
self ,
1136
1134
* ,
1137
- valtypes : Mapping [str , "Literal['params', 'funcargs']" ],
1138
1135
argnames : Iterable [str ],
1139
1136
valset : Iterable [object ],
1140
1137
id : str ,
1141
1138
marks : Iterable [Union [Mark , MarkDecorator ]],
1142
1139
scope : Scope ,
1143
1140
param_index : int ,
1144
1141
) -> "CallSpec2" :
1145
- funcargs = self .funcargs .copy ()
1146
1142
params = self .params .copy ()
1147
1143
indices = self .indices .copy ()
1148
1144
arg2scope = self ._arg2scope .copy ()
1149
1145
for arg , val in zip (argnames , valset ):
1150
- if arg in params or arg in funcargs :
1146
+ if arg in params :
1151
1147
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
1159
1149
indices [arg ] = param_index
1160
1150
arg2scope [arg ] = scope
1161
1151
return CallSpec2 (
1162
- funcargs = funcargs ,
1163
1152
params = params ,
1164
1153
indices = indices ,
1165
1154
_arg2scope = arg2scope ,
@@ -1178,6 +1167,14 @@ def id(self) -> str:
1178
1167
return "-" .join (self ._idlist )
1179
1168
1180
1169
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
+
1181
1178
@final
1182
1179
class Metafunc :
1183
1180
"""Objects passed to the :hook:`pytest_generate_tests` hook.
@@ -1320,8 +1317,6 @@ def parametrize(
1320
1317
1321
1318
self ._validate_if_using_arg_names (argnames , indirect )
1322
1319
1323
- arg_values_types = self ._resolve_arg_value_types (argnames , indirect )
1324
-
1325
1320
# Use any already (possibly) generated ids with parametrize Marks.
1326
1321
if _param_mark and _param_mark ._param_ids_from :
1327
1322
generated_ids = _param_mark ._param_ids_from ._param_ids_generated
@@ -1336,6 +1331,60 @@ def parametrize(
1336
1331
if _param_mark and _param_mark ._param_ids_from and generated_ids is None :
1337
1332
object .__setattr__ (_param_mark ._param_ids_from , "_param_ids_generated" , ids )
1338
1333
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
+
1339
1388
# Create the new calls: if we are parametrize() multiple times (by applying the decorator
1340
1389
# more than once) then we accumulate those calls generating the cartesian product
1341
1390
# of all calls.
@@ -1345,7 +1394,6 @@ def parametrize(
1345
1394
zip (ids , parametersets )
1346
1395
):
1347
1396
newcallspec = callspec .setmulti (
1348
- valtypes = arg_values_types ,
1349
1397
argnames = argnames ,
1350
1398
valset = param_set .values ,
1351
1399
id = param_id ,
0 commit comments