@@ -929,6 +929,139 @@ def hasnew(obj: object) -> bool:
929
929
return False
930
930
931
931
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
+
932
1065
@final
933
1066
@attr .s (frozen = True , slots = True , auto_attribs = True )
934
1067
class CallSpec2 :
@@ -1217,12 +1350,15 @@ def _resolve_parameter_set_ids(
1217
1350
else :
1218
1351
idfn = None
1219
1352
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 ()
1221
1357
1222
1358
def _validate_ids (
1223
1359
self ,
1224
1360
ids : Iterable [Union [None , str , float , int , bool ]],
1225
- parameters : Sequence [ParameterSet ],
1361
+ parametersets : Sequence [ParameterSet ],
1226
1362
func_name : str ,
1227
1363
) -> List [Union [None , str ]]:
1228
1364
try :
@@ -1232,12 +1368,12 @@ def _validate_ids(
1232
1368
iter (ids )
1233
1369
except TypeError as e :
1234
1370
raise TypeError ("ids must be a callable or an iterable" ) from e
1235
- num_ids = len (parameters )
1371
+ num_ids = len (parametersets )
1236
1372
1237
1373
# 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 :
1239
1375
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 )
1241
1377
1242
1378
new_ids = []
1243
1379
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]) -
1374
1510
return val if escape_option else ascii_escaped (val ) # type: ignore
1375
1511
1376
1512
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
-
1476
1513
def _pretty_fixture_path (func ) -> str :
1477
1514
cwd = Path .cwd ()
1478
1515
loc = Path (getlocation (func , str (cwd )))
0 commit comments