python: unify code to generate ID from value

In the following

    @pytest.mark.parametrize(..., ids=[val])

the ID values are only allowed to be `str`, `float`, `int` or `bool`.

In the following

    @pytest.mark.parametrize(..., [val])

    @pytest.mark.parametrize(..., [pytest.param(..., id=val])

a different code path is used, which also allows `bytes`, `complex`,
`re.Pattern`, `Enum` and anything with a `__name__`.

In the interest of consistency, use the latter code path for all cases.
This commit is contained in:
Ran Benita 2022-02-05 12:25:48 +02:00
parent c01a5c177b
commit c3aa4647c7
4 changed files with 85 additions and 79 deletions

View File

@ -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.

View File

@ -939,10 +939,7 @@ class FixtureDef(Generic[FixtureValue]):
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
@ -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):
@ -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,
@ -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:
@ -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:
@ -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]:
@ -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.

View File

@ -940,7 +940,7 @@ 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.
@ -948,6 +948,9 @@ class IdMaker:
# 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
@ -982,9 +985,7 @@ class IdMaker:
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(
@ -1053,6 +1054,25 @@ class IdMaker:
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
@ -1182,10 +1202,7 @@ class Metafunc:
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,
*,
@ -1316,10 +1333,7 @@ class Metafunc:
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,
@ -1349,16 +1363,22 @@ class Metafunc:
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:
@ -1373,22 +1393,7 @@ class Metafunc:
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,

View File

@ -106,8 +106,8 @@ class TestMetafunc:
with pytest.raises(
fail.Exception,
match=(
r"In func: ids must be list of string/float/int/bool, found:"
r" Exc\(from_gen\) \(type: <class .*Exc'>\) at index 2"
r"In func: ids contains unsupported value Exc\(from_gen\) \(type: <class .*Exc'>\) at index 2. "
r"Supported types are: .*"
),
):
metafunc.parametrize("x", [1, 2, 3], ids=gen()) # type: ignore[arg-type]
@ -285,7 +285,7 @@ class TestMetafunc:
deadline=400.0
) # very close to std deadline and CI boxes are not reliable in CPU power
def test_idval_hypothesis(self, value) -> None:
escaped = IdMaker([], [], None, None, None, None)._idval(value, "a", 6)
escaped = IdMaker([], [], None, None, None, None, None)._idval(value, "a", 6)
assert isinstance(escaped, str)
escaped.encode("ascii")
@ -308,7 +308,8 @@ class TestMetafunc:
]
for val, expected in values:
assert (
IdMaker([], [], None, None, None, None)._idval(val, "a", 6) == expected
IdMaker([], [], None, None, None, None, None)._idval(val, "a", 6)
== expected
)
def test_unicode_idval_with_config(self) -> None:
@ -337,7 +338,7 @@ class TestMetafunc:
("ação", MockConfig({option: False}), "a\\xe7\\xe3o"),
]
for val, config, expected in values:
actual = IdMaker([], [], None, None, config, None)._idval(val, "a", 6)
actual = IdMaker([], [], None, None, config, None, None)._idval(val, "a", 6)
assert actual == expected
def test_bytes_idval(self) -> None:
@ -351,7 +352,8 @@ class TestMetafunc:
]
for val, expected in values:
assert (
IdMaker([], [], None, None, None, None)._idval(val, "a", 6) == expected
IdMaker([], [], None, None, None, None, None)._idval(val, "a", 6)
== expected
)
def test_class_or_function_idval(self) -> None:
@ -367,7 +369,8 @@ class TestMetafunc:
values = [(TestClass, "TestClass"), (test_function, "test_function")]
for val, expected in values:
assert (
IdMaker([], [], None, None, None, None)._idval(val, "a", 6) == expected
IdMaker([], [], None, None, None, None, None)._idval(val, "a", 6)
== expected
)
def test_notset_idval(self) -> None:
@ -376,7 +379,9 @@ class TestMetafunc:
Regression test for #7686.
"""
assert IdMaker([], [], None, None, None, None)._idval(NOTSET, "a", 0) == "a0"
assert (
IdMaker([], [], None, None, None, None, None)._idval(NOTSET, "a", 0) == "a0"
)
def test_idmaker_autoname(self) -> None:
"""#250"""
@ -387,6 +392,7 @@ class TestMetafunc:
None,
None,
None,
None,
).make_unique_parameterset_ids()
assert result == ["string-1.0", "st-ring-2.0"]
@ -397,17 +403,18 @@ class TestMetafunc:
None,
None,
None,
None,
).make_unique_parameterset_ids()
assert result == ["a0-1.0", "a1-b1"]
# unicode mixing, issue250
result = IdMaker(
("a", "b"), [pytest.param({}, b"\xc3\xb4")], None, None, None, None
("a", "b"), [pytest.param({}, b"\xc3\xb4")], None, None, None, None, None
).make_unique_parameterset_ids()
assert result == ["a0-\\xc3\\xb4"]
def test_idmaker_with_bytes_regex(self) -> None:
result = IdMaker(
("a"), [pytest.param(re.compile(b"foo"), 1.0)], None, None, None, None
("a"), [pytest.param(re.compile(b"foo"), 1.0)], None, None, None, None, None
).make_unique_parameterset_ids()
assert result == ["foo"]
@ -433,6 +440,7 @@ class TestMetafunc:
None,
None,
None,
None,
).make_unique_parameterset_ids()
assert result == [
"1.0--1.1",
@ -465,6 +473,7 @@ class TestMetafunc:
None,
None,
None,
None,
).make_unique_parameterset_ids()
assert result == ["\\x00-1", "\\x05-2", "\\x00-3", "\\x05-4", "\\t-5", "\\t-6"]
@ -479,6 +488,7 @@ class TestMetafunc:
None,
None,
None,
None,
).make_unique_parameterset_ids()
assert result == ["hello \\x00", "hello \\x05"]
@ -486,7 +496,7 @@ class TestMetafunc:
enum = pytest.importorskip("enum")
e = enum.Enum("Foo", "one, two")
result = IdMaker(
("a", "b"), [pytest.param(e.one, e.two)], None, None, None, None
("a", "b"), [pytest.param(e.one, e.two)], None, None, None, None, None
).make_unique_parameterset_ids()
assert result == ["Foo.one-Foo.two"]
@ -509,6 +519,7 @@ class TestMetafunc:
None,
None,
None,
None,
).make_unique_parameterset_ids()
assert result == ["10.0-IndexError()", "20-KeyError()", "three-b2"]
@ -529,6 +540,7 @@ class TestMetafunc:
None,
None,
None,
None,
).make_unique_parameterset_ids()
assert result == ["a-a0", "a-a1", "a-a2"]
@ -560,7 +572,13 @@ class TestMetafunc:
]
for config, expected in values:
result = IdMaker(
("a",), [pytest.param("string")], lambda _: "ação", None, config, None
("a",),
[pytest.param("string")],
lambda _: "ação",
None,
config,
None,
None,
).make_unique_parameterset_ids()
assert result == [expected]
@ -592,7 +610,7 @@ class TestMetafunc:
]
for config, expected in values:
result = IdMaker(
("a",), [pytest.param("string")], None, ["ação"], config, None
("a",), [pytest.param("string")], None, ["ação"], config, None, None
).make_unique_parameterset_ids()
assert result == [expected]
@ -657,6 +675,7 @@ class TestMetafunc:
["a", None],
None,
None,
None,
).make_unique_parameterset_ids()
assert result == ["a", "3-4"]
@ -668,6 +687,7 @@ class TestMetafunc:
["a", None],
None,
None,
None,
).make_unique_parameterset_ids()
assert result == ["me", "you"]
@ -679,6 +699,7 @@ class TestMetafunc:
["a", "a", "b", "c", "b"],
None,
None,
None,
).make_unique_parameterset_ids()
assert result == ["a0", "a1", "b0", "c", "b1"]
@ -1318,7 +1339,7 @@ class TestMetafuncFunctional:
"""
import pytest
@pytest.mark.parametrize("x, expected", [(1, 2), (3, 4), (5, 6)], ids=(None, 2, type))
@pytest.mark.parametrize("x, expected", [(1, 2), (3, 4), (5, 6)], ids=(None, 2, OSError()))
def test_ids_numbers(x,expected):
assert x * 2 == expected
"""
@ -1326,8 +1347,8 @@ class TestMetafuncFunctional:
result = pytester.runpytest()
result.stdout.fnmatch_lines(
[
"In test_ids_numbers: ids must be list of string/float/int/bool,"
" found: <class 'type'> (type: <class 'type'>) at index 2"
"In test_ids_numbers: ids contains unsupported value OSError() (type: <class 'OSError'>) at index 2. "
"Supported types are: str, bytes, int, float, complex, bool, enum, regex or anything with a __name__."
]
)