22
22
from pathlib import Path
23
23
import re
24
24
import types
25
+ import typing
25
26
from typing import Any
26
27
from typing import final
27
28
from typing import Literal
56
57
from _pytest .fixtures import get_scope_node
57
58
from _pytest .main import Session
58
59
from _pytest .mark import ParameterSet
60
+ from _pytest .mark import RawParameterSet
59
61
from _pytest .mark .structures import get_unpacked_marks
60
62
from _pytest .mark .structures import Mark
61
63
from _pytest .mark .structures import MarkDecorator
@@ -105,6 +107,7 @@ def pytest_addoption(parser: Parser) -> None:
105
107
)
106
108
107
109
110
+ @hookimpl (tryfirst = True )
108
111
def pytest_generate_tests (metafunc : Metafunc ) -> None :
109
112
for marker in metafunc .definition .iter_markers (name = "parametrize" ):
110
113
metafunc .parametrize (* marker .args , ** marker .kwargs , _param_mark = marker )
@@ -1022,27 +1025,34 @@ def _idval_from_argname(argname: str, idx: int) -> str:
1022
1025
1023
1026
@final
1024
1027
@dataclasses .dataclass (frozen = True )
1025
- class CallSpec2 :
1028
+ class CallSpec :
1026
1029
"""A planned parameterized invocation of a test function.
1027
1030
1028
- Calculated during collection for a given test function's Metafunc.
1029
- Once collection is over, each callspec is turned into a single Item
1030
- and stored in item.callspec.
1031
+ Calculated during collection for a given test function's `` Metafunc`` .
1032
+ Once collection is over, each callspec is turned into a single `` Item``
1033
+ and stored in `` item.callspec`` .
1031
1034
"""
1032
1035
1033
- # arg name -> arg value which will be passed to a fixture or pseudo-fixture
1034
- # of the same name. (indirect or direct parametrization respectively)
1035
- params : dict [str , object ] = dataclasses .field (default_factory = dict )
1036
- # arg name -> arg index.
1037
- indices : dict [str , int ] = dataclasses .field (default_factory = dict )
1036
+ #: arg name -> arg value which will be passed to a fixture or pseudo-fixture
1037
+ #: of the same name. (indirect or direct parametrization respectively)
1038
+ params : Mapping [str , object ] = dataclasses .field (default_factory = dict )
1039
+ #: arg name -> arg index.
1040
+ indices : Mapping [str , int ] = dataclasses .field (default_factory = dict )
1041
+ #: Marks which will be applied to the item.
1042
+ marks : Sequence [Mark ] = dataclasses .field (default_factory = list )
1043
+
1038
1044
# Used for sorting parametrized resources.
1039
1045
_arg2scope : Mapping [str , Scope ] = dataclasses .field (default_factory = dict )
1040
1046
# Parts which will be added to the item's name in `[..]` separated by "-".
1041
1047
_idlist : Sequence [str ] = dataclasses .field (default_factory = tuple )
1042
- # Marks which will be applied to the item.
1043
- marks : list [Mark ] = dataclasses .field (default_factory = list )
1048
+ # Make __init__ internal.
1049
+ _ispytest : dataclasses .InitVar [bool ] = False
1050
+
1051
+ def __post_init__ (self , _ispytest : bool ):
1052
+ """:meta private:"""
1053
+ check_ispytest (_ispytest )
1044
1054
1045
- def setmulti (
1055
+ def _setmulti (
1046
1056
self ,
1047
1057
* ,
1048
1058
argnames : Iterable [str ],
@@ -1051,32 +1061,35 @@ def setmulti(
1051
1061
marks : Iterable [Mark | MarkDecorator ],
1052
1062
scope : Scope ,
1053
1063
param_index : int ,
1054
- ) -> CallSpec2 :
1055
- params = self .params . copy ( )
1056
- indices = self .indices . copy ( )
1064
+ ) -> CallSpec :
1065
+ params = dict ( self .params )
1066
+ indices = dict ( self .indices )
1057
1067
arg2scope = dict (self ._arg2scope )
1058
1068
for arg , val in zip (argnames , valset ):
1059
1069
if arg in params :
1060
1070
raise ValueError (f"duplicate parametrization of { arg !r} " )
1061
1071
params [arg ] = val
1062
1072
indices [arg ] = param_index
1063
1073
arg2scope [arg ] = scope
1064
- return CallSpec2 (
1074
+ return CallSpec (
1065
1075
params = params ,
1066
1076
indices = indices ,
1077
+ marks = [* self .marks , * normalize_mark_list (marks )],
1067
1078
_arg2scope = arg2scope ,
1068
1079
_idlist = [* self ._idlist , id ],
1069
- marks = [ * self . marks , * normalize_mark_list ( marks )] ,
1080
+ _ispytest = True ,
1070
1081
)
1071
1082
1072
1083
def getparam (self , name : str ) -> object :
1084
+ """:meta private:"""
1073
1085
try :
1074
1086
return self .params [name ]
1075
1087
except KeyError as e :
1076
1088
raise ValueError (name ) from e
1077
1089
1078
1090
@property
1079
1091
def id (self ) -> str :
1092
+ """The combined display name of ``params``."""
1080
1093
return "-" .join (self ._idlist )
1081
1094
1082
1095
@@ -1130,14 +1143,15 @@ def __init__(
1130
1143
self ._arg2fixturedefs = fixtureinfo .name2fixturedefs
1131
1144
1132
1145
# Result of parametrize().
1133
- self ._calls : list [CallSpec2 ] = []
1146
+ self ._calls : list [CallSpec ] = []
1134
1147
1135
1148
self ._params_directness : dict [str , Literal ["indirect" , "direct" ]] = {}
1136
1149
1137
1150
def parametrize (
1138
1151
self ,
1139
1152
argnames : str | Sequence [str ],
1140
- argvalues : Iterable [ParameterSet | Sequence [object ] | object ],
1153
+ argvalues : Iterable [RawParameterSet ]
1154
+ | Callable [[CallSpec ], Iterable [RawParameterSet ]],
1141
1155
indirect : bool | Sequence [str ] = False ,
1142
1156
ids : Iterable [object | None ] | Callable [[Any ], object | None ] | None = None ,
1143
1157
scope : _ScopeName | None = None ,
@@ -1171,7 +1185,7 @@ def parametrize(
1171
1185
If N argnames were specified, argvalues must be a list of
1172
1186
N-tuples, where each tuple-element specifies a value for its
1173
1187
respective argname.
1174
- :type argvalues: Iterable[_pytest.mark.structures.ParameterSet | Sequence[object] | object]
1188
+ :type argvalues: Iterable[_pytest.mark.structures.ParameterSet | Sequence[object] | object] | Callable
1175
1189
:param indirect:
1176
1190
A list of arguments' names (subset of argnames) or a boolean.
1177
1191
If True the list contains all names from the argnames. Each
@@ -1206,13 +1220,19 @@ def parametrize(
1206
1220
It will also override any fixture-function defined scope, allowing
1207
1221
to set a dynamic scope using test context or configuration.
1208
1222
"""
1209
- argnames , parametersets = ParameterSet ._for_parametrize (
1210
- argnames ,
1211
- argvalues ,
1212
- self .function ,
1213
- self .config ,
1214
- nodeid = self .definition .nodeid ,
1215
- )
1223
+ if callable (argvalues ):
1224
+ raw_argnames = argnames
1225
+ param_factory = argvalues
1226
+ argnames , _ = ParameterSet ._parse_parametrize_args (raw_argnames )
1227
+ else :
1228
+ param_factory = None
1229
+ argnames , parametersets = ParameterSet ._for_parametrize (
1230
+ argnames ,
1231
+ argvalues ,
1232
+ self .function ,
1233
+ self .config ,
1234
+ nodeid = self .definition .nodeid ,
1235
+ )
1216
1236
del argvalues
1217
1237
1218
1238
if "request" in argnames :
@@ -1230,19 +1250,22 @@ def parametrize(
1230
1250
1231
1251
self ._validate_if_using_arg_names (argnames , indirect )
1232
1252
1233
- # Use any already (possibly) generated ids with parametrize Marks.
1234
- if _param_mark and _param_mark ._param_ids_from :
1235
- generated_ids = _param_mark ._param_ids_from ._param_ids_generated
1236
- if generated_ids is not None :
1237
- ids = generated_ids
1253
+ if param_factory is None :
1254
+ # Use any already (possibly) generated ids with parametrize Marks.
1255
+ if _param_mark and _param_mark ._param_ids_from :
1256
+ generated_ids = _param_mark ._param_ids_from ._param_ids_generated
1257
+ if generated_ids is not None :
1258
+ ids = generated_ids
1238
1259
1239
- ids = self ._resolve_parameter_set_ids (
1240
- argnames , ids , parametersets , nodeid = self .definition .nodeid
1241
- )
1260
+ ids_ = self ._resolve_parameter_set_ids (
1261
+ argnames , ids , parametersets , nodeid = self .definition .nodeid
1262
+ )
1242
1263
1243
- # Store used (possibly generated) ids with parametrize Marks.
1244
- if _param_mark and _param_mark ._param_ids_from and generated_ids is None :
1245
- object .__setattr__ (_param_mark ._param_ids_from , "_param_ids_generated" , ids )
1264
+ # Store used (possibly generated) ids with parametrize Marks.
1265
+ if _param_mark and _param_mark ._param_ids_from and generated_ids is None :
1266
+ object .__setattr__ (
1267
+ _param_mark ._param_ids_from , "_param_ids_generated" , ids_
1268
+ )
1246
1269
1247
1270
# Add funcargs as fixturedefs to fixtureinfo.arg2fixturedefs by registering
1248
1271
# artificial "pseudo" FixtureDef's so that later at test execution time we can
@@ -1301,11 +1324,22 @@ def parametrize(
1301
1324
# more than once) then we accumulate those calls generating the cartesian product
1302
1325
# of all calls.
1303
1326
newcalls = []
1304
- for callspec in self ._calls or [CallSpec2 ()]:
1327
+ for callspec in self ._calls or [CallSpec (_ispytest = True )]:
1328
+ if param_factory :
1329
+ _ , parametersets = ParameterSet ._for_parametrize (
1330
+ raw_argnames ,
1331
+ param_factory (callspec ),
1332
+ self .function ,
1333
+ self .config ,
1334
+ nodeid = self .definition .nodeid ,
1335
+ )
1336
+ ids_ = self ._resolve_parameter_set_ids (
1337
+ argnames , ids , parametersets , nodeid = self .definition .nodeid
1338
+ )
1305
1339
for param_index , (param_id , param_set ) in enumerate (
1306
- zip (ids , parametersets )
1340
+ zip (ids_ , parametersets )
1307
1341
):
1308
- newcallspec = callspec .setmulti (
1342
+ newcallspec = callspec ._setmulti (
1309
1343
argnames = argnames ,
1310
1344
valset = param_set .values ,
1311
1345
id = param_id ,
@@ -1453,7 +1487,7 @@ def _recompute_direct_params_indices(self) -> None:
1453
1487
for argname , param_type in self ._params_directness .items ():
1454
1488
if param_type == "direct" :
1455
1489
for i , callspec in enumerate (self ._calls ):
1456
- callspec .indices [argname ] = i
1490
+ typing . cast ( dict [ str , int ], callspec .indices ) [argname ] = i
1457
1491
1458
1492
1459
1493
def _find_parametrized_scope (
@@ -1538,7 +1572,7 @@ def __init__(
1538
1572
name : str ,
1539
1573
parent ,
1540
1574
config : Config | None = None ,
1541
- callspec : CallSpec2 | None = None ,
1575
+ callspec : CallSpec | None = None ,
1542
1576
callobj = NOTSET ,
1543
1577
keywords : Mapping [str , Any ] | None = None ,
1544
1578
session : Session | None = None ,
0 commit comments