2222from pathlib import Path
2323import re
2424import types
25+ import typing
2526from typing import Any
2627from typing import final
2728from typing import Literal
5657from _pytest .fixtures import get_scope_node
5758from _pytest .main import Session
5859from _pytest .mark import ParameterSet
60+ from _pytest .mark import RawParameterSet
5961from _pytest .mark .structures import get_unpacked_marks
6062from _pytest .mark .structures import Mark
6163from _pytest .mark .structures import MarkDecorator
@@ -105,6 +107,7 @@ def pytest_addoption(parser: Parser) -> None:
105107 )
106108
107109
110+ @hookimpl (tryfirst = True )
108111def pytest_generate_tests (metafunc : Metafunc ) -> None :
109112 for marker in metafunc .definition .iter_markers (name = "parametrize" ):
110113 metafunc .parametrize (* marker .args , ** marker .kwargs , _param_mark = marker )
@@ -1022,27 +1025,34 @@ def _idval_from_argname(argname: str, idx: int) -> str:
10221025
10231026@final
10241027@dataclasses .dataclass (frozen = True )
1025- class CallSpec2 :
1028+ class CallSpec :
10261029 """A planned parameterized invocation of a test function.
10271030
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`` .
10311034 """
10321035
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+
10381044 # Used for sorting parametrized resources.
10391045 _arg2scope : Mapping [str , Scope ] = dataclasses .field (default_factory = dict )
10401046 # Parts which will be added to the item's name in `[..]` separated by "-".
10411047 _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 )
10441054
1045- def setmulti (
1055+ def _setmulti (
10461056 self ,
10471057 * ,
10481058 argnames : Iterable [str ],
@@ -1051,32 +1061,35 @@ def setmulti(
10511061 marks : Iterable [Mark | MarkDecorator ],
10521062 scope : Scope ,
10531063 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 )
10571067 arg2scope = dict (self ._arg2scope )
10581068 for arg , val in zip (argnames , valset ):
10591069 if arg in params :
10601070 raise ValueError (f"duplicate parametrization of { arg !r} " )
10611071 params [arg ] = val
10621072 indices [arg ] = param_index
10631073 arg2scope [arg ] = scope
1064- return CallSpec2 (
1074+ return CallSpec (
10651075 params = params ,
10661076 indices = indices ,
1077+ marks = [* self .marks , * normalize_mark_list (marks )],
10671078 _arg2scope = arg2scope ,
10681079 _idlist = [* self ._idlist , id ],
1069- marks = [ * self . marks , * normalize_mark_list ( marks )] ,
1080+ _ispytest = True ,
10701081 )
10711082
10721083 def getparam (self , name : str ) -> object :
1084+ """:meta private:"""
10731085 try :
10741086 return self .params [name ]
10751087 except KeyError as e :
10761088 raise ValueError (name ) from e
10771089
10781090 @property
10791091 def id (self ) -> str :
1092+ """The combined display name of ``params``."""
10801093 return "-" .join (self ._idlist )
10811094
10821095
@@ -1130,14 +1143,15 @@ def __init__(
11301143 self ._arg2fixturedefs = fixtureinfo .name2fixturedefs
11311144
11321145 # Result of parametrize().
1133- self ._calls : list [CallSpec2 ] = []
1146+ self ._calls : list [CallSpec ] = []
11341147
11351148 self ._params_directness : dict [str , Literal ["indirect" , "direct" ]] = {}
11361149
11371150 def parametrize (
11381151 self ,
11391152 argnames : str | Sequence [str ],
1140- argvalues : Iterable [ParameterSet | Sequence [object ] | object ],
1153+ argvalues : Iterable [RawParameterSet ]
1154+ | Callable [[CallSpec ], Iterable [RawParameterSet ]],
11411155 indirect : bool | Sequence [str ] = False ,
11421156 ids : Iterable [object | None ] | Callable [[Any ], object | None ] | None = None ,
11431157 scope : _ScopeName | None = None ,
@@ -1171,7 +1185,7 @@ def parametrize(
11711185 If N argnames were specified, argvalues must be a list of
11721186 N-tuples, where each tuple-element specifies a value for its
11731187 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
11751189 :param indirect:
11761190 A list of arguments' names (subset of argnames) or a boolean.
11771191 If True the list contains all names from the argnames. Each
@@ -1206,13 +1220,19 @@ def parametrize(
12061220 It will also override any fixture-function defined scope, allowing
12071221 to set a dynamic scope using test context or configuration.
12081222 """
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+ )
12161236 del argvalues
12171237
12181238 if "request" in argnames :
@@ -1230,19 +1250,22 @@ def parametrize(
12301250
12311251 self ._validate_if_using_arg_names (argnames , indirect )
12321252
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
12381259
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+ )
12421263
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+ )
12461269
12471270 # Add funcargs as fixturedefs to fixtureinfo.arg2fixturedefs by registering
12481271 # artificial "pseudo" FixtureDef's so that later at test execution time we can
@@ -1301,11 +1324,22 @@ def parametrize(
13011324 # more than once) then we accumulate those calls generating the cartesian product
13021325 # of all calls.
13031326 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+ )
13051339 for param_index , (param_id , param_set ) in enumerate (
1306- zip (ids , parametersets )
1340+ zip (ids_ , parametersets )
13071341 ):
1308- newcallspec = callspec .setmulti (
1342+ newcallspec = callspec ._setmulti (
13091343 argnames = argnames ,
13101344 valset = param_set .values ,
13111345 id = param_id ,
@@ -1453,7 +1487,7 @@ def _recompute_direct_params_indices(self) -> None:
14531487 for argname , param_type in self ._params_directness .items ():
14541488 if param_type == "direct" :
14551489 for i , callspec in enumerate (self ._calls ):
1456- callspec .indices [argname ] = i
1490+ typing . cast ( dict [ str , int ], callspec .indices ) [argname ] = i
14571491
14581492
14591493def _find_parametrized_scope (
@@ -1538,7 +1572,7 @@ def __init__(
15381572 name : str ,
15391573 parent ,
15401574 config : Config | None = None ,
1541- callspec : CallSpec2 | None = None ,
1575+ callspec : CallSpec | None = None ,
15421576 callobj = NOTSET ,
15431577 keywords : Mapping [str , Any ] | None = None ,
15441578 session : Session | None = None ,
0 commit comments