10
10
from functools import partial
11
11
from textwrap import dedent
12
12
from typing import List
13
+ from typing import Optional
13
14
from typing import Tuple
14
15
15
16
import py
36
37
from _pytest .main import FSHookProxy
37
38
from _pytest .mark import MARK_GEN
38
39
from _pytest .mark .structures import get_unpacked_marks
40
+ from _pytest .mark .structures import Mark
39
41
from _pytest .mark .structures import normalize_mark_list
40
42
from _pytest .outcomes import fail
41
43
from _pytest .outcomes import skip
@@ -122,7 +124,7 @@ def pytest_cmdline_main(config):
122
124
123
125
def pytest_generate_tests (metafunc ):
124
126
for marker in metafunc .definition .iter_markers (name = "parametrize" ):
125
- metafunc .parametrize (* marker .args , ** marker .kwargs )
127
+ metafunc .parametrize (* marker .args , ** marker .kwargs , _param_mark = marker )
126
128
127
129
128
130
def pytest_configure (config ):
@@ -914,7 +916,16 @@ def funcargnames(self):
914
916
warnings .warn (FUNCARGNAMES , stacklevel = 2 )
915
917
return self .fixturenames
916
918
917
- def parametrize (self , argnames , argvalues , indirect = False , ids = None , scope = None ):
919
+ def parametrize (
920
+ self ,
921
+ argnames ,
922
+ argvalues ,
923
+ indirect = False ,
924
+ ids = None ,
925
+ scope = None ,
926
+ * ,
927
+ _param_mark : Optional [Mark ] = None
928
+ ):
918
929
""" Add new invocations to the underlying test function using the list
919
930
of argvalues for the given argnames. Parametrization is performed
920
931
during the collection phase. If you need to setup expensive resources
@@ -937,13 +948,22 @@ def parametrize(self, argnames, argvalues, indirect=False, ids=None, scope=None)
937
948
function so that it can perform more expensive setups during the
938
949
setup phase of a test rather than at collection time.
939
950
940
- :arg ids: list of string ids, or a callable.
941
- If strings, each is corresponding to the argvalues so that they are
942
- part of the test id. If None is given as id of specific test, the
943
- automatically generated id for that argument will be used.
944
- If callable, it should take one argument (a single argvalue) and return
945
- a string or return None. If None, the automatically generated id for that
946
- argument will be used.
951
+ :arg ids: sequence of (or generator for) ids for ``argvalues``,
952
+ or a callable to return part of the id for each argvalue.
953
+
954
+ With sequences (and generators like ``itertools.count()``) the
955
+ returned ids should be of type ``string``, ``int``, ``float``,
956
+ ``bool``, or ``None``.
957
+ They are mapped to the corresponding index in ``argvalues``.
958
+ ``None`` means to use the auto-generated id.
959
+
960
+ If it is a callable it will be called for each entry in
961
+ ``argvalues``, and the return value is used as part of the
962
+ auto-generated id for the whole set (where parts are joined with
963
+ dashes ("-")).
964
+ This is useful to provide more specific ids for certain items, e.g.
965
+ dates. Returning ``None`` will use an auto-generated id.
966
+
947
967
If no ids are provided they will be generated automatically from
948
968
the argvalues.
949
969
@@ -977,8 +997,18 @@ def parametrize(self, argnames, argvalues, indirect=False, ids=None, scope=None)
977
997
978
998
arg_values_types = self ._resolve_arg_value_types (argnames , indirect )
979
999
1000
+ # Use any already (possibly) generated ids with parametrize Marks.
1001
+ if _param_mark and _param_mark ._param_ids_from :
1002
+ generated_ids = _param_mark ._param_ids_from ._param_ids_generated
1003
+ if generated_ids is not None :
1004
+ ids = generated_ids
1005
+
980
1006
ids = self ._resolve_arg_ids (argnames , ids , parameters , item = self .definition )
981
1007
1008
+ # Store used (possibly generated) ids with parametrize Marks.
1009
+ if _param_mark and _param_mark ._param_ids_from and generated_ids is None :
1010
+ object .__setattr__ (_param_mark ._param_ids_from , "_param_ids_generated" , ids )
1011
+
982
1012
scopenum = scope2index (
983
1013
scope , descr = "parametrize() call in {}" .format (self .function .__name__ )
984
1014
)
@@ -1013,26 +1043,47 @@ def _resolve_arg_ids(self, argnames, ids, parameters, item):
1013
1043
:rtype: List[str]
1014
1044
:return: the list of ids for each argname given
1015
1045
"""
1016
- from _pytest ._io .saferepr import saferepr
1017
-
1018
1046
idfn = None
1019
1047
if callable (ids ):
1020
1048
idfn = ids
1021
1049
ids = None
1022
1050
if ids :
1023
1051
func_name = self .function .__name__
1024
- if len (ids ) != len (parameters ):
1025
- msg = "In {}: {} parameter sets specified, with different number of ids: {}"
1026
- fail (msg .format (func_name , len (parameters ), len (ids )), pytrace = False )
1027
- for id_value in ids :
1028
- if id_value is not None and not isinstance (id_value , str ):
1029
- msg = "In {}: ids must be list of strings, found: {} (type: {!r})"
1052
+ ids = self ._validate_ids (ids , parameters , func_name )
1053
+ ids = idmaker (argnames , parameters , idfn , ids , self .config , item = item )
1054
+ return ids
1055
+
1056
+ def _validate_ids (self , ids , parameters , func_name ):
1057
+ try :
1058
+ len (ids )
1059
+ except TypeError :
1060
+ try :
1061
+ it = iter (ids )
1062
+ except TypeError :
1063
+ raise TypeError ("ids must be a callable, sequence or generator" )
1064
+ else :
1065
+ import itertools
1066
+
1067
+ new_ids = list (itertools .islice (it , len (parameters )))
1068
+ else :
1069
+ new_ids = list (ids )
1070
+
1071
+ if len (new_ids ) != len (parameters ):
1072
+ msg = "In {}: {} parameter sets specified, with different number of ids: {}"
1073
+ fail (msg .format (func_name , len (parameters ), len (ids )), pytrace = False )
1074
+ for idx , id_value in enumerate (new_ids ):
1075
+ if id_value is not None :
1076
+ if isinstance (id_value , (float , int , bool )):
1077
+ new_ids [idx ] = str (id_value )
1078
+ elif not isinstance (id_value , str ):
1079
+ from _pytest ._io .saferepr import saferepr
1080
+
1081
+ msg = "In {}: ids must be list of string/float/int/bool, found: {} (type: {!r}) at index {}"
1030
1082
fail (
1031
- msg .format (func_name , saferepr (id_value ), type (id_value )),
1083
+ msg .format (func_name , saferepr (id_value ), type (id_value ), idx ),
1032
1084
pytrace = False ,
1033
1085
)
1034
- ids = idmaker (argnames , parameters , idfn , ids , self .config , item = item )
1035
- return ids
1086
+ return new_ids
1036
1087
1037
1088
def _resolve_arg_value_types (self , argnames , indirect ):
1038
1089
"""Resolves if each parametrized argument must be considered a parameter to a fixture or a "funcarg"
0 commit comments