Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

python: unify code to generate ID from value #9678

Merged
merged 1 commit into from Feb 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog/9678.improvement.rst
@@ -0,0 +1,3 @@
More types are now accepted in the ``ids`` argument to ``@pytest.mark.parametrize``.
Previously only `str`, `float`, `int` and `bool` were accepted;
now `bytes`, `complex`, `re.Pattern`, `Enum` and anything with a `__name__` are also accepted.
41 changes: 9 additions & 32 deletions src/_pytest/fixtures.py
Expand Up @@ -939,10 +939,7 @@ def __init__(
params: Optional[Sequence[object]],
unittest: bool = False,
ids: Optional[
Union[
Tuple[Union[None, str, float, int, bool], ...],
Callable[[Any], Optional[object]],
]
Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]]
] = None,
) -> None:
self._fixturemanager = fixturemanager
Expand Down Expand Up @@ -1093,18 +1090,8 @@ def pytest_fixture_setup(


def _ensure_immutable_ids(
ids: Optional[
Union[
Iterable[Union[None, str, float, int, bool]],
Callable[[Any], Optional[object]],
]
],
) -> Optional[
Union[
Tuple[Union[None, str, float, int, bool], ...],
Callable[[Any], Optional[object]],
]
]:
ids: Optional[Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]]]
) -> Optional[Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]]]:
if ids is None:
return None
if callable(ids):
Expand Down Expand Up @@ -1148,9 +1135,8 @@ class FixtureFunctionMarker:
scope: "Union[_ScopeName, Callable[[str, Config], _ScopeName]]"
params: Optional[Tuple[object, ...]] = attr.ib(converter=_params_converter)
autouse: bool = False
ids: Union[
Tuple[Union[None, str, float, int, bool], ...],
Callable[[Any], Optional[object]],
ids: Optional[
Union[Tuple[Optional[object], ...], Callable[[Any], Optional[object]]]
] = attr.ib(
default=None,
converter=_ensure_immutable_ids,
Expand Down Expand Up @@ -1191,10 +1177,7 @@ def fixture(
params: Optional[Iterable[object]] = ...,
autouse: bool = ...,
ids: Optional[
Union[
Iterable[Union[None, str, float, int, bool]],
Callable[[Any], Optional[object]],
]
Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]]
] = ...,
name: Optional[str] = ...,
) -> FixtureFunction:
Expand All @@ -1209,10 +1192,7 @@ def fixture(
params: Optional[Iterable[object]] = ...,
autouse: bool = ...,
ids: Optional[
Union[
Iterable[Union[None, str, float, int, bool]],
Callable[[Any], Optional[object]],
]
Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]]
] = ...,
name: Optional[str] = None,
) -> FixtureFunctionMarker:
Expand All @@ -1226,10 +1206,7 @@ def fixture(
params: Optional[Iterable[object]] = None,
autouse: bool = False,
ids: Optional[
Union[
Iterable[Union[None, str, float, int, bool]],
Callable[[Any], Optional[object]],
]
Union[Sequence[Optional[object]], Callable[[Any], Optional[object]]]
] = None,
name: Optional[str] = None,
) -> Union[FixtureFunctionMarker, FixtureFunction]:
Expand Down Expand Up @@ -1271,7 +1248,7 @@ def fixture(
the fixture.

:param ids:
List of string ids each corresponding to the params so that they are
Sequence of ids each corresponding to the params so that they are
part of the test id. If no ids are provided they will be generated
automatically from the params.

Expand Down
67 changes: 36 additions & 31 deletions src/_pytest/python.py
Expand Up @@ -940,14 +940,17 @@ class IdMaker:
# ParameterSet.
idfn: Optional[Callable[[Any], Optional[object]]]
# Optionally, explicit IDs for ParameterSets by index.
ids: Optional[Sequence[Union[None, str]]]
ids: Optional[Sequence[Optional[object]]]
# Optionally, the pytest config.
# Used for controlling ASCII escaping, and for calling the
# :hook:`pytest_make_parametrize_id` hook.
config: Optional[Config]
# Optionally, the ID of the node being parametrized.
# Used only for clearer error messages.
nodeid: Optional[str]
# Optionally, the ID of the function being parametrized.
# Used only for clearer error messages.
func_name: Optional[str]

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

def _idval_from_value_required(self, val: object, idx: int) -> str:
"""Like _idval_from_value(), but fails if the type is not supported."""
id = self._idval_from_value(val)
if id is not None:
return id

# Fail.
if self.func_name is not None:
prefix = f"In {self.func_name}: "
elif self.nodeid is not None:
prefix = f"In {self.nodeid}: "
else:
prefix = ""
msg = (
f"{prefix}ids contains unsupported value {saferepr(val)} (type: {type(val)!r}) at index {idx}. "
"Supported types are: str, bytes, int, float, complex, bool, enum, regex or anything with a __name__."
)
fail(msg, pytrace=False)

@staticmethod
def _idval_from_argname(argname: str, idx: int) -> str:
"""Make an ID for a parameter in a ParameterSet from the argument name
Expand Down Expand Up @@ -1182,10 +1202,7 @@ def parametrize(
argvalues: Iterable[Union[ParameterSet, Sequence[object], object]],
indirect: Union[bool, Sequence[str]] = False,
ids: Optional[
Union[
Iterable[Union[None, str, float, int, bool]],
Callable[[Any], Optional[object]],
]
Union[Iterable[Optional[object]], Callable[[Any], Optional[object]]]
] = None,
scope: "Optional[_ScopeName]" = None,
*,
Expand Down Expand Up @@ -1316,10 +1333,7 @@ def _resolve_parameter_set_ids(
self,
argnames: Sequence[str],
ids: Optional[
Union[
Iterable[Union[None, str, float, int, bool]],
Callable[[Any], Optional[object]],
]
Union[Iterable[Optional[object]], Callable[[Any], Optional[object]]]
],
parametersets: Sequence[ParameterSet],
nodeid: str,
Expand Down Expand Up @@ -1349,16 +1363,22 @@ def _resolve_parameter_set_ids(
idfn = None
ids_ = self._validate_ids(ids, parametersets, self.function.__name__)
id_maker = IdMaker(
argnames, parametersets, idfn, ids_, self.config, nodeid=nodeid
argnames,
parametersets,
idfn,
ids_,
self.config,
nodeid=nodeid,
func_name=self.function.__name__,
)
return id_maker.make_unique_parameterset_ids()

def _validate_ids(
self,
ids: Iterable[Union[None, str, float, int, bool]],
ids: Iterable[Optional[object]],
parametersets: Sequence[ParameterSet],
func_name: str,
) -> List[Union[None, str]]:
) -> List[Optional[object]]:
try:
num_ids = len(ids) # type: ignore[arg-type]
except TypeError:
Expand All @@ -1373,22 +1393,7 @@ def _validate_ids(
msg = "In {}: {} parameter sets specified, with different number of ids: {}"
fail(msg.format(func_name, len(parametersets), num_ids), pytrace=False)

new_ids = []
for idx, id_value in enumerate(itertools.islice(ids, num_ids)):
if id_value is None or isinstance(id_value, str):
new_ids.append(id_value)
elif isinstance(id_value, (float, int, bool)):
new_ids.append(str(id_value))
else:
msg = ( # type: ignore[unreachable]
"In {}: ids must be list of string/float/int/bool, "
"found: {} (type: {!r}) at index {}"
)
fail(
msg.format(func_name, saferepr(id_value), type(id_value), idx),
pytrace=False,
)
return new_ids
return list(itertools.islice(ids, num_ids))

def _resolve_arg_value_types(
self,
Expand Down