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:
parent
c01a5c177b
commit
c3aa4647c7
|
@ -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.
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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__."
|
||||
]
|
||||
)
|
||||
|
||||
|
|
Loading…
Reference in New Issue