27
27
from typing import TYPE_CHECKING
28
28
from typing import Union
29
29
30
+ import attr
31
+
30
32
import _pytest
31
33
from _pytest import fixtures
32
34
from _pytest import nodes
37
39
from _pytest ._io import TerminalWriter
38
40
from _pytest ._io .saferepr import saferepr
39
41
from _pytest .compat import ascii_escaped
42
+ from _pytest .compat import assert_never
40
43
from _pytest .compat import final
41
44
from _pytest .compat import get_default_arg_names
42
45
from _pytest .compat import get_real_func
@@ -451,11 +454,12 @@ def _genfunctions(self, name: str, funcobj) -> Iterator["Function"]:
451
454
module = modulecol .obj
452
455
clscol = self .getparent (Class )
453
456
cls = clscol and clscol .obj or None
454
- fm = self .session ._fixturemanager
455
457
456
458
definition = FunctionDefinition .from_parent (self , name = name , callobj = funcobj )
457
459
fixtureinfo = definition ._fixtureinfo
458
460
461
+ # pytest_generate_tests impls call metafunc.parametrize() which fills
462
+ # metafunc._calls, the outcome of the hook.
459
463
metafunc = Metafunc (
460
464
definition = definition ,
461
465
fixtureinfo = fixtureinfo ,
@@ -469,13 +473,13 @@ def _genfunctions(self, name: str, funcobj) -> Iterator["Function"]:
469
473
methods .append (module .pytest_generate_tests )
470
474
if cls is not None and hasattr (cls , "pytest_generate_tests" ):
471
475
methods .append (cls ().pytest_generate_tests )
472
-
473
476
self .ihook .pytest_generate_tests .call_extra (methods , dict (metafunc = metafunc ))
474
477
475
478
if not metafunc ._calls :
476
479
yield Function .from_parent (self , name = name , fixtureinfo = fixtureinfo )
477
480
else :
478
481
# Add funcargs() as fixturedefs to fixtureinfo.arg2fixturedefs.
482
+ fm = self .session ._fixturemanager
479
483
fixtures .add_funcarg_pseudo_fixture_def (self , metafunc , fm )
480
484
481
485
# Add_funcarg_pseudo_fixture_def may have shadowed some fixtures
@@ -894,61 +898,75 @@ def hasnew(obj: object) -> bool:
894
898
895
899
896
900
@final
901
+ @attr .s (frozen = True , slots = True , auto_attribs = True )
897
902
class CallSpec2 :
898
- def __init__ (self , metafunc : "Metafunc" ) -> None :
899
- self .metafunc = metafunc
900
- self .funcargs : Dict [str , object ] = {}
901
- self ._idlist : List [str ] = []
902
- self .params : Dict [str , object ] = {}
903
- # Used for sorting parametrized resources.
904
- self ._arg2scope : Dict [str , Scope ] = {}
905
- self .marks : List [Mark ] = []
906
- self .indices : Dict [str , int ] = {}
907
-
908
- def copy (self ) -> "CallSpec2" :
909
- cs = CallSpec2 (self .metafunc )
910
- cs .funcargs .update (self .funcargs )
911
- cs .params .update (self .params )
912
- cs .marks .extend (self .marks )
913
- cs .indices .update (self .indices )
914
- cs ._arg2scope .update (self ._arg2scope )
915
- cs ._idlist = list (self ._idlist )
916
- return cs
917
-
918
- def getparam (self , name : str ) -> object :
919
- try :
920
- return self .params [name ]
921
- except KeyError as e :
922
- raise ValueError (name ) from e
903
+ """A planned parameterized invocation of a test function.
923
904
924
- @property
925
- def id (self ) -> str :
926
- return "-" .join (map (str , self ._idlist ))
905
+ Calculated during collection for a given test function's Metafunc.
906
+ Once collection is over, each callspec is turned into a single Item
907
+ and stored in item.callspec.
908
+ """
927
909
928
- def setmulti2 (
910
+ # arg name -> arg value which will be passed to the parametrized test
911
+ # function (direct parameterization).
912
+ funcargs : Dict [str , object ] = attr .Factory (dict )
913
+ # arg name -> arg value which will be passed to a fixture of the same name
914
+ # (indirect parametrization).
915
+ params : Dict [str , object ] = attr .Factory (dict )
916
+ # arg name -> arg index.
917
+ indices : Dict [str , int ] = attr .Factory (dict )
918
+ # Used for sorting parametrized resources.
919
+ _arg2scope : Dict [str , Scope ] = attr .Factory (dict )
920
+ # Parts which will be added to the item's name in `[..]` separated by "-".
921
+ _idlist : List [str ] = attr .Factory (list )
922
+ # Marks which will be applied to the item.
923
+ marks : List [Mark ] = attr .Factory (list )
924
+
925
+ def setmulti (
929
926
self ,
927
+ * ,
930
928
valtypes : Mapping [str , "Literal['params', 'funcargs']" ],
931
- argnames : Sequence [str ],
929
+ argnames : Iterable [str ],
932
930
valset : Iterable [object ],
933
931
id : str ,
934
932
marks : Iterable [Union [Mark , MarkDecorator ]],
935
933
scope : Scope ,
936
934
param_index : int ,
937
- ) -> None :
935
+ ) -> "CallSpec2" :
936
+ funcargs = self .funcargs .copy ()
937
+ params = self .params .copy ()
938
+ indices = self .indices .copy ()
939
+ arg2scope = self ._arg2scope .copy ()
938
940
for arg , val in zip (argnames , valset ):
939
- if arg in self . params or arg in self . funcargs :
941
+ if arg in params or arg in funcargs :
940
942
raise ValueError (f"duplicate { arg !r} " )
941
943
valtype_for_arg = valtypes [arg ]
942
944
if valtype_for_arg == "params" :
943
- self . params [arg ] = val
945
+ params [arg ] = val
944
946
elif valtype_for_arg == "funcargs" :
945
- self .funcargs [arg ] = val
946
- else : # pragma: no cover
947
- assert False , f"Unhandled valtype for arg: { valtype_for_arg } "
948
- self .indices [arg ] = param_index
949
- self ._arg2scope [arg ] = scope
950
- self ._idlist .append (id )
951
- self .marks .extend (normalize_mark_list (marks ))
947
+ funcargs [arg ] = val
948
+ else :
949
+ assert_never (valtype_for_arg )
950
+ indices [arg ] = param_index
951
+ arg2scope [arg ] = scope
952
+ return CallSpec2 (
953
+ funcargs = funcargs ,
954
+ params = params ,
955
+ arg2scope = arg2scope ,
956
+ indices = indices ,
957
+ idlist = [* self ._idlist , id ],
958
+ marks = [* self .marks , * normalize_mark_list (marks )],
959
+ )
960
+
961
+ def getparam (self , name : str ) -> object :
962
+ try :
963
+ return self .params [name ]
964
+ except KeyError as e :
965
+ raise ValueError (name ) from e
966
+
967
+ @property
968
+ def id (self ) -> str :
969
+ return "-" .join (self ._idlist )
952
970
953
971
954
972
@final
@@ -990,9 +1008,11 @@ def __init__(
990
1008
#: Class object where the test function is defined in or ``None``.
991
1009
self .cls = cls
992
1010
993
- self ._calls : List [CallSpec2 ] = []
994
1011
self ._arg2fixturedefs = fixtureinfo .name2fixturedefs
995
1012
1013
+ # Result of parametrize().
1014
+ self ._calls : List [CallSpec2 ] = []
1015
+
996
1016
def parametrize (
997
1017
self ,
998
1018
argnames : Union [str , List [str ], Tuple [str , ...]],
@@ -1009,9 +1029,18 @@ def parametrize(
1009
1029
_param_mark : Optional [Mark ] = None ,
1010
1030
) -> None :
1011
1031
"""Add new invocations to the underlying test function using the list
1012
- of argvalues for the given argnames. Parametrization is performed
1013
- during the collection phase. If you need to setup expensive resources
1014
- see about setting indirect to do it rather at test setup time.
1032
+ of argvalues for the given argnames. Parametrization is performed
1033
+ during the collection phase. If you need to setup expensive resources
1034
+ see about setting indirect to do it rather than at test setup time.
1035
+
1036
+ Can be called multiple times, in which case each call parametrizes all
1037
+ previous parametrizations, e.g.
1038
+
1039
+ ::
1040
+
1041
+ unparametrized: t
1042
+ parametrize ["x", "y"]: t[x], t[y]
1043
+ parametrize [1, 2]: t[x-1], t[x-2], t[y-1], t[y-2]
1015
1044
1016
1045
:param argnames:
1017
1046
A comma-separated string denoting one or more argument names, or
@@ -1104,17 +1133,16 @@ def parametrize(
1104
1133
# more than once) then we accumulate those calls generating the cartesian product
1105
1134
# of all calls.
1106
1135
newcalls = []
1107
- for callspec in self ._calls or [CallSpec2 (self )]:
1136
+ for callspec in self ._calls or [CallSpec2 ()]:
1108
1137
for param_index , (param_id , param_set ) in enumerate (zip (ids , parameters )):
1109
- newcallspec = callspec .copy ()
1110
- newcallspec .setmulti2 (
1111
- arg_values_types ,
1112
- argnames ,
1113
- param_set .values ,
1114
- param_id ,
1115
- param_set .marks ,
1116
- scope_ ,
1117
- param_index ,
1138
+ newcallspec = callspec .setmulti (
1139
+ valtypes = arg_values_types ,
1140
+ argnames = argnames ,
1141
+ valset = param_set .values ,
1142
+ id = param_id ,
1143
+ marks = param_set .marks ,
1144
+ scope = scope_ ,
1145
+ param_index = param_index ,
1118
1146
)
1119
1147
newcalls .append (newcallspec )
1120
1148
self ._calls = newcalls
0 commit comments