@@ -929,6 +929,139 @@ def hasnew(obj: object) -> bool:
929929 return False
930930
931931
932+ @final
933+ @attr .s (frozen = True , auto_attribs = True , slots = True )
934+ class IdMaker :
935+ """Make IDs for a parametrization."""
936+
937+ # The argnames of the parametrization.
938+ argnames : Sequence [str ]
939+ # The ParameterSets of the parametrization.
940+ parametersets : Sequence [ParameterSet ]
941+ # Optionally, a user-provided callable to make IDs for parameters in a
942+ # ParameterSet.
943+ idfn : Optional [Callable [[Any ], Optional [object ]]]
944+ # Optionally, explicit IDs for ParameterSets by index.
945+ ids : Optional [Sequence [Union [None , str ]]]
946+ # Optionally, the pytest config.
947+ # Used for controlling ASCII escaping, and for calling the
948+ # :hook:`pytest_make_parametrize_id` hook.
949+ config : Optional [Config ]
950+ # Optionally, the ID of the node being parametrized.
951+ # Used only for clearer error messages.
952+ nodeid : Optional [str ]
953+
954+ def make_unique_parameterset_ids (self ) -> List [str ]:
955+ """Make a unique identifier for each ParameterSet, that may be used to
956+ identify the parametrization in a node ID.
957+
958+ Format is <prm_1_token>-...-<prm_n_token>[counter], where prm_x_token is
959+ - user-provided id, if given
960+ - else an id derived from the value, applicable for certain types
961+ - else <argname><parameterset index>
962+ The counter suffix is appended only in case a string wouldn't be unique
963+ otherwise.
964+ """
965+ resolved_ids = list (self ._resolve_ids ())
966+ # All IDs must be unique!
967+ if len (resolved_ids ) != len (set (resolved_ids )):
968+ # Record the number of occurrences of each ID.
969+ id_counts = Counter (resolved_ids )
970+ # Map the ID to its next suffix.
971+ id_suffixes : Dict [str , int ] = defaultdict (int )
972+ # Suffix non-unique IDs to make them unique.
973+ for index , id in enumerate (resolved_ids ):
974+ if id_counts [id ] > 1 :
975+ resolved_ids [index ] = f"{ id } { id_suffixes [id ]} "
976+ id_suffixes [id ] += 1
977+ return resolved_ids
978+
979+ def _resolve_ids (self ) -> Iterable [str ]:
980+ """Resolve IDs for all ParameterSets (may contain duplicates)."""
981+ for idx , parameterset in enumerate (self .parametersets ):
982+ if parameterset .id is not None :
983+ # ID provided directly - pytest.param(..., id="...")
984+ yield parameterset .id
985+ elif self .ids and idx < len (self .ids ) and self .ids [idx ] is not None :
986+ # ID provided in the IDs list - parametrize(..., ids=[...]).
987+ id = self .ids [idx ]
988+ assert id is not None
989+ yield _ascii_escaped_by_config (id , self .config )
990+ else :
991+ # ID not provided - generate it.
992+ yield "-" .join (
993+ self ._idval (val , argname , idx )
994+ for val , argname in zip (parameterset .values , self .argnames )
995+ )
996+
997+ def _idval (self , val : object , argname : str , idx : int ) -> str :
998+ """Make an ID for a parameter in a ParameterSet."""
999+ idval = self ._idval_from_function (val , argname , idx )
1000+ if idval is not None :
1001+ return idval
1002+ idval = self ._idval_from_hook (val , argname )
1003+ if idval is not None :
1004+ return idval
1005+ idval = self ._idval_from_value (val )
1006+ if idval is not None :
1007+ return idval
1008+ return self ._idval_from_argname (argname , idx )
1009+
1010+ def _idval_from_function (
1011+ self , val : object , argname : str , idx : int
1012+ ) -> Optional [str ]:
1013+ """Try to make an ID for a parameter in a ParameterSet using the
1014+ user-provided id callable, if given."""
1015+ if self .idfn is None :
1016+ return None
1017+ try :
1018+ id = self .idfn (val )
1019+ except Exception as e :
1020+ prefix = f"{ self .nodeid } : " if self .nodeid is not None else ""
1021+ msg = "error raised while trying to determine id of parameter '{}' at position {}"
1022+ msg = prefix + msg .format (argname , idx )
1023+ raise ValueError (msg ) from e
1024+ if id is None :
1025+ return None
1026+ return self ._idval_from_value (id )
1027+
1028+ def _idval_from_hook (self , val : object , argname : str ) -> Optional [str ]:
1029+ """Try to make an ID for a parameter in a ParameterSet by calling the
1030+ :hook:`pytest_make_parametrize_id` hook."""
1031+ if self .config :
1032+ id : Optional [str ] = self .config .hook .pytest_make_parametrize_id (
1033+ config = self .config , val = val , argname = argname
1034+ )
1035+ return id
1036+ return None
1037+
1038+ def _idval_from_value (self , val : object ) -> Optional [str ]:
1039+ """Try to make an ID for a parameter in a ParameterSet from its value,
1040+ if the value type is supported."""
1041+ if isinstance (val , STRING_TYPES ):
1042+ return _ascii_escaped_by_config (val , self .config )
1043+ elif val is None or isinstance (val , (float , int , bool , complex )):
1044+ return str (val )
1045+ elif isinstance (val , Pattern ):
1046+ return ascii_escaped (val .pattern )
1047+ elif val is NOTSET :
1048+ # Fallback to default. Note that NOTSET is an enum.Enum.
1049+ pass
1050+ elif isinstance (val , enum .Enum ):
1051+ return str (val )
1052+ elif isinstance (getattr (val , "__name__" , None ), str ):
1053+ # Name of a class, function, module, etc.
1054+ name : str = getattr (val , "__name__" )
1055+ return name
1056+ return None
1057+
1058+ @staticmethod
1059+ def _idval_from_argname (argname : str , idx : int ) -> str :
1060+ """Make an ID for a parameter in a ParameterSet from the argument name
1061+ and the index of the ParameterSet."""
1062+ return str (argname ) + str (idx )
1063+
1064+
9321065@final
9331066@attr .s (frozen = True , slots = True , auto_attribs = True )
9341067class CallSpec2 :
@@ -1217,12 +1350,15 @@ def _resolve_parameter_set_ids(
12171350 else :
12181351 idfn = None
12191352 ids_ = self ._validate_ids (ids , parametersets , self .function .__name__ )
1220- return idmaker (argnames , parametersets , idfn , ids_ , self .config , nodeid = nodeid )
1353+ id_maker = IdMaker (
1354+ argnames , parametersets , idfn , ids_ , self .config , nodeid = nodeid
1355+ )
1356+ return id_maker .make_unique_parameterset_ids ()
12211357
12221358 def _validate_ids (
12231359 self ,
12241360 ids : Iterable [Union [None , str , float , int , bool ]],
1225- parameters : Sequence [ParameterSet ],
1361+ parametersets : Sequence [ParameterSet ],
12261362 func_name : str ,
12271363 ) -> List [Union [None , str ]]:
12281364 try :
@@ -1232,12 +1368,12 @@ def _validate_ids(
12321368 iter (ids )
12331369 except TypeError as e :
12341370 raise TypeError ("ids must be a callable or an iterable" ) from e
1235- num_ids = len (parameters )
1371+ num_ids = len (parametersets )
12361372
12371373 # num_ids == 0 is a special case: https://github.com/pytest-dev/pytest/issues/1849
1238- if num_ids != len (parameters ) and num_ids != 0 :
1374+ if num_ids != len (parametersets ) and num_ids != 0 :
12391375 msg = "In {}: {} parameter sets specified, with different number of ids: {}"
1240- fail (msg .format (func_name , len (parameters ), num_ids ), pytrace = False )
1376+ fail (msg .format (func_name , len (parametersets ), num_ids ), pytrace = False )
12411377
12421378 new_ids = []
12431379 for idx , id_value in enumerate (itertools .islice (ids , num_ids )):
@@ -1374,105 +1510,6 @@ def _ascii_escaped_by_config(val: Union[str, bytes], config: Optional[Config]) -
13741510 return val if escape_option else ascii_escaped (val ) # type: ignore
13751511
13761512
1377- def _idval (
1378- val : object ,
1379- argname : str ,
1380- idx : int ,
1381- idfn : Optional [Callable [[Any ], Optional [object ]]],
1382- nodeid : Optional [str ],
1383- config : Optional [Config ],
1384- ) -> str :
1385- if idfn :
1386- try :
1387- generated_id = idfn (val )
1388- if generated_id is not None :
1389- val = generated_id
1390- except Exception as e :
1391- prefix = f"{ nodeid } : " if nodeid is not None else ""
1392- msg = "error raised while trying to determine id of parameter '{}' at position {}"
1393- msg = prefix + msg .format (argname , idx )
1394- raise ValueError (msg ) from e
1395- elif config :
1396- hook_id : Optional [str ] = config .hook .pytest_make_parametrize_id (
1397- config = config , val = val , argname = argname
1398- )
1399- if hook_id :
1400- return hook_id
1401-
1402- if isinstance (val , STRING_TYPES ):
1403- return _ascii_escaped_by_config (val , config )
1404- elif val is None or isinstance (val , (float , int , bool , complex )):
1405- return str (val )
1406- elif isinstance (val , Pattern ):
1407- return ascii_escaped (val .pattern )
1408- elif val is NOTSET :
1409- # Fallback to default. Note that NOTSET is an enum.Enum.
1410- pass
1411- elif isinstance (val , enum .Enum ):
1412- return str (val )
1413- elif isinstance (getattr (val , "__name__" , None ), str ):
1414- # Name of a class, function, module, etc.
1415- name : str = getattr (val , "__name__" )
1416- return name
1417- return str (argname ) + str (idx )
1418-
1419-
1420- def _idvalset (
1421- idx : int ,
1422- parameterset : ParameterSet ,
1423- argnames : Iterable [str ],
1424- idfn : Optional [Callable [[Any ], Optional [object ]]],
1425- ids : Optional [List [Union [None , str ]]],
1426- nodeid : Optional [str ],
1427- config : Optional [Config ],
1428- ) -> str :
1429- if parameterset .id is not None :
1430- return parameterset .id
1431- id = None if ids is None or idx >= len (ids ) else ids [idx ]
1432- if id is None :
1433- this_id = [
1434- _idval (val , argname , idx , idfn , nodeid = nodeid , config = config )
1435- for val , argname in zip (parameterset .values , argnames )
1436- ]
1437- return "-" .join (this_id )
1438- else :
1439- return _ascii_escaped_by_config (id , config )
1440-
1441-
1442- def idmaker (
1443- argnames : Iterable [str ],
1444- parametersets : Iterable [ParameterSet ],
1445- idfn : Optional [Callable [[Any ], Optional [object ]]] = None ,
1446- ids : Optional [List [Union [None , str ]]] = None ,
1447- config : Optional [Config ] = None ,
1448- nodeid : Optional [str ] = None ,
1449- ) -> List [str ]:
1450- resolved_ids = [
1451- _idvalset (
1452- valindex , parameterset , argnames , idfn , ids , config = config , nodeid = nodeid
1453- )
1454- for valindex , parameterset in enumerate (parametersets )
1455- ]
1456-
1457- # All IDs must be unique!
1458- unique_ids = set (resolved_ids )
1459- if len (unique_ids ) != len (resolved_ids ):
1460-
1461- # Record the number of occurrences of each test ID.
1462- test_id_counts = Counter (resolved_ids )
1463-
1464- # Map the test ID to its next suffix.
1465- test_id_suffixes : Dict [str , int ] = defaultdict (int )
1466-
1467- # Suffix non-unique IDs to make them unique.
1468- for index , test_id in enumerate (resolved_ids ):
1469- if test_id_counts [test_id ] > 1 :
1470- resolved_ids [index ] = f"{ test_id } { test_id_suffixes [test_id ]} "
1471- test_id_suffixes [test_id ] += 1
1472-
1473- return resolved_ids
1474-
1475-
14761513def _pretty_fixture_path (func ) -> str :
14771514 cwd = Path .cwd ()
14781515 loc = Path (getlocation (func , str (cwd )))
0 commit comments