Skip to content

Commit afe41e5

Browse files
authored
Merge pull request #9678 from bluetech/consistent-idval
python: unify code to generate ID from value
2 parents c9cf2d4 + c3aa464 commit afe41e5

File tree

4 files changed

+85
-79
lines changed

4 files changed

+85
-79
lines changed

changelog/9678.improvement.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
More types are now accepted in the ``ids`` argument to ``@pytest.mark.parametrize``.
2+
Previously only `str`, `float`, `int` and `bool` were accepted;
3+
now `bytes`, `complex`, `re.Pattern`, `Enum` and anything with a `__name__` are also accepted.

src/_pytest/fixtures.py

Lines changed: 9 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -939,10 +939,7 @@ def __init__(
939939
params: Optional[Sequence[object]],
940940
unittest: bool = False,
941941
ids: Optional[
942-
Union[
943-
Tuple[Union[None, str, float, int, bool], ...],
944-
Callable[[Any], Optional[object]],
945-
]
942+
Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]]
946943
] = None,
947944
) -> None:
948945
self._fixturemanager = fixturemanager
@@ -1093,18 +1090,8 @@ def pytest_fixture_setup(
10931090

10941091

10951092
def _ensure_immutable_ids(
1096-
ids: Optional[
1097-
Union[
1098-
Iterable[Union[None, str, float, int, bool]],
1099-
Callable[[Any], Optional[object]],
1100-
]
1101-
],
1102-
) -> Optional[
1103-
Union[
1104-
Tuple[Union[None, str, float, int, bool], ...],
1105-
Callable[[Any], Optional[object]],
1106-
]
1107-
]:
1093+
ids: Optional[Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]]]
1094+
) -> Optional[Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]]]:
11081095
if ids is None:
11091096
return None
11101097
if callable(ids):
@@ -1148,9 +1135,8 @@ class FixtureFunctionMarker:
11481135
scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]"
11491136
params: Optional[Tuple[object, ...]] = attr.ib(converter=_params_converter)
11501137
autouse: bool = False
1151-
ids: Union[
1152-
Tuple[Union[None, str, float, int, bool], ...],
1153-
Callable[[Any], Optional[object]],
1138+
ids: Optional[
1139+
Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]]
11541140
] = attr.ib(
11551141
default=None,
11561142
converter=_ensure_immutable_ids,
@@ -1191,10 +1177,7 @@ def fixture(
11911177
params: Optional[Iterable[object]] = ...,
11921178
autouse: bool = ...,
11931179
ids: Optional[
1194-
Union[
1195-
Iterable[Union[None, str, float, int, bool]],
1196-
Callable[[Any], Optional[object]],
1197-
]
1180+
Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]]
11981181
] = ...,
11991182
name: Optional[str] = ...,
12001183
) -> FixtureFunction:
@@ -1209,10 +1192,7 @@ def fixture(
12091192
params: Optional[Iterable[object]] = ...,
12101193
autouse: bool = ...,
12111194
ids: Optional[
1212-
Union[
1213-
Iterable[Union[None, str, float, int, bool]],
1214-
Callable[[Any], Optional[object]],
1215-
]
1195+
Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]]
12161196
] = ...,
12171197
name: Optional[str] = None,
12181198
) -> FixtureFunctionMarker:
@@ -1226,10 +1206,7 @@ def fixture(
12261206
params: Optional[Iterable[object]] = None,
12271207
autouse: bool = False,
12281208
ids: Optional[
1229-
Union[
1230-
Iterable[Union[None, str, float, int, bool]],
1231-
Callable[[Any], Optional[object]],
1232-
]
1209+
Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]]
12331210
] = None,
12341211
name: Optional[str] = None,
12351212
) -> Union[FixtureFunctionMarker, FixtureFunction]:
@@ -1271,7 +1248,7 @@ def fixture(
12711248
the fixture.
12721249
12731250
:param ids:
1274-
List of string ids each corresponding to the params so that they are
1251+
Sequence of ids each corresponding to the params so that they are
12751252
part of the test id. If no ids are provided they will be generated
12761253
automatically from the params.
12771254

src/_pytest/python.py

Lines changed: 36 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -940,14 +940,17 @@ class IdMaker:
940940
# ParameterSet.
941941
idfn: Optional[Callable[[Any], Optional[object]]]
942942
# Optionally, explicit IDs for ParameterSets by index.
943-
ids: Optional[Sequence[Union[None, str]]]
943+
ids: Optional[Sequence[Optional[object]]]
944944
# Optionally, the pytest config.
945945
# Used for controlling ASCII escaping, and for calling the
946946
# :hook:`pytest_make_parametrize_id` hook.
947947
config: Optional[Config]
948948
# Optionally, the ID of the node being parametrized.
949949
# Used only for clearer error messages.
950950
nodeid: Optional[str]
951+
# Optionally, the ID of the function being parametrized.
952+
# Used only for clearer error messages.
953+
func_name: Optional[str]
951954

952955
def make_unique_parameterset_ids(self) -> List[str]:
953956
"""Make a unique identifier for each ParameterSet, that may be used to
@@ -982,9 +985,7 @@ def _resolve_ids(self) -> Iterable[str]:
982985
yield parameterset.id
983986
elif self.ids and idx < len(self.ids) and self.ids[idx] is not None:
984987
# ID provided in the IDs list - parametrize(..., ids=[...]).
985-
id = self.ids[idx]
986-
assert id is not None
987-
yield _ascii_escaped_by_config(id, self.config)
988+
yield self._idval_from_value_required(self.ids[idx], idx)
988989
else:
989990
# ID not provided - generate it.
990991
yield "-".join(
@@ -1053,6 +1054,25 @@ def _idval_from_value(self, val: object) -> Optional[str]:
10531054
return name
10541055
return None
10551056

1057+
def _idval_from_value_required(self, val: object, idx: int) -> str:
1058+
"""Like _idval_from_value(), but fails if the type is not supported."""
1059+
id = self._idval_from_value(val)
1060+
if id is not None:
1061+
return id
1062+
1063+
# Fail.
1064+
if self.func_name is not None:
1065+
prefix = f"In {self.func_name}: "
1066+
elif self.nodeid is not None:
1067+
prefix = f"In {self.nodeid}: "
1068+
else:
1069+
prefix = ""
1070+
msg = (
1071+
f"{prefix}ids contains unsupported value {saferepr(val)} (type: {type(val)!r}) at index {idx}. "
1072+
"Supported types are: str, bytes, int, float, complex, bool, enum, regex or anything with a __name__."
1073+
)
1074+
fail(msg, pytrace=False)
1075+
10561076
@staticmethod
10571077
def _idval_from_argname(argname: str, idx: int) -> str:
10581078
"""Make an ID for a parameter in a ParameterSet from the argument name
@@ -1182,10 +1202,7 @@ def parametrize(
11821202
argvalues: Iterable[Union[ParameterSet, Sequence[object], object]],
11831203
indirect: Union[bool, Sequence[str]] = False,
11841204
ids: Optional[
1185-
Union[
1186-
Iterable[Union[None, str, float, int, bool]],
1187-
Callable[[Any], Optional[object]],
1188-
]
1205+
Union[Iterable[Optional[object]], Callable[[Any], Optional[object]]]
11891206
] = None,
11901207
scope: "Optional[_ScopeName]" = None,
11911208
*,
@@ -1316,10 +1333,7 @@ def _resolve_parameter_set_ids(
13161333
self,
13171334
argnames: Sequence[str],
13181335
ids: Optional[
1319-
Union[
1320-
Iterable[Union[None, str, float, int, bool]],
1321-
Callable[[Any], Optional[object]],
1322-
]
1336+
Union[Iterable[Optional[object]], Callable[[Any], Optional[object]]]
13231337
],
13241338
parametersets: Sequence[ParameterSet],
13251339
nodeid: str,
@@ -1349,16 +1363,22 @@ def _resolve_parameter_set_ids(
13491363
idfn = None
13501364
ids_ = self._validate_ids(ids, parametersets, self.function.__name__)
13511365
id_maker = IdMaker(
1352-
argnames, parametersets, idfn, ids_, self.config, nodeid=nodeid
1366+
argnames,
1367+
parametersets,
1368+
idfn,
1369+
ids_,
1370+
self.config,
1371+
nodeid=nodeid,
1372+
func_name=self.function.__name__,
13531373
)
13541374
return id_maker.make_unique_parameterset_ids()
13551375

13561376
def _validate_ids(
13571377
self,
1358-
ids: Iterable[Union[None, str, float, int, bool]],
1378+
ids: Iterable[Optional[object]],
13591379
parametersets: Sequence[ParameterSet],
13601380
func_name: str,
1361-
) -> List[Union[None, str]]:
1381+
) -> List[Optional[object]]:
13621382
try:
13631383
num_ids = len(ids) # type: ignore[arg-type]
13641384
except TypeError:
@@ -1373,22 +1393,7 @@ def _validate_ids(
13731393
msg = "In {}: {} parameter sets specified, with different number of ids: {}"
13741394
fail(msg.format(func_name, len(parametersets), num_ids), pytrace=False)
13751395

1376-
new_ids = []
1377-
for idx, id_value in enumerate(itertools.islice(ids, num_ids)):
1378-
if id_value is None or isinstance(id_value, str):
1379-
new_ids.append(id_value)
1380-
elif isinstance(id_value, (float, int, bool)):
1381-
new_ids.append(str(id_value))
1382-
else:
1383-
msg = ( # type: ignore[unreachable]
1384-
"In {}: ids must be list of string/float/int/bool, "
1385-
"found: {} (type: {!r}) at index {}"
1386-
)
1387-
fail(
1388-
msg.format(func_name, saferepr(id_value), type(id_value), idx),
1389-
pytrace=False,
1390-
)
1391-
return new_ids
1396+
return list(itertools.islice(ids, num_ids))
13921397

13931398
def _resolve_arg_value_types(
13941399
self,

0 commit comments

Comments
 (0)