Skip to content

Commit 611bb13

Browse files
Anton3webknjaz
andauthored
Allow hiding a parameter set from test name (#13229)
Fixes #13228 --------- Co-authored-by: 🇺🇦 Sviatoslav Sydorenko (Святослав Сидоренко) <[email protected]>
1 parent 9cf2cae commit 611bb13

File tree

9 files changed

+216
-21
lines changed

9 files changed

+216
-21
lines changed

AUTHORS

+1
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ Anthony Shaw
4343
Anthony Sottile
4444
Anton Grinevich
4545
Anton Lodder
46+
Anton Zhilin
4647
Antony Lee
4748
Arel Cordero
4849
Arias Emmanuel

changelog/13228.feature.rst

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
:ref:`hidden-param` can now be used in ``id`` of :func:`pytest.param` or in
2+
``ids`` of :py:func:`Metafunc.parametrize <pytest.Metafunc.parametrize>`.
3+
It hides the parameter set from the test name.

doc/en/reference/reference.rst

+10
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,16 @@ The current pytest version, as a string::
2020
>>> pytest.__version__
2121
'7.0.0'
2222

23+
.. _`hidden-param`:
24+
25+
pytest.HIDDEN_PARAM
26+
~~~~~~~~~~~~~~~~~~~
27+
28+
.. versionadded:: 8.4
29+
30+
Can be passed to ``ids`` of :py:func:`Metafunc.parametrize <pytest.Metafunc.parametrize>`
31+
or to ``id`` of :func:`pytest.param` to hide a parameter set from the test name.
32+
Can only be used at most 1 time, as test names need to be unique.
2333

2434
.. _`version-tuple`:
2535

src/_pytest/mark/__init__.py

+12-2
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,10 @@
1212

1313
from .expression import Expression
1414
from .expression import ParseError
15+
from .structures import _HiddenParam
1516
from .structures import EMPTY_PARAMETERSET_OPTION
1617
from .structures import get_empty_parameterset_mark
18+
from .structures import HIDDEN_PARAM
1719
from .structures import Mark
1820
from .structures import MARK_GEN
1921
from .structures import MarkDecorator
@@ -33,6 +35,7 @@
3335

3436

3537
__all__ = [
38+
"HIDDEN_PARAM",
3639
"MARK_GEN",
3740
"Mark",
3841
"MarkDecorator",
@@ -48,7 +51,7 @@
4851
def param(
4952
*values: object,
5053
marks: MarkDecorator | Collection[MarkDecorator | Mark] = (),
51-
id: str | None = None,
54+
id: str | _HiddenParam | None = None,
5255
) -> ParameterSet:
5356
"""Specify a parameter in `pytest.mark.parametrize`_ calls or
5457
:ref:`parametrized fixtures <fixture-parametrize-marks>`.
@@ -72,7 +75,14 @@ def test_eval(test_input, expected):
7275
7376
:ref:`pytest.mark.usefixtures <pytest.mark.usefixtures ref>` cannot be added via this parameter.
7477
75-
:param id: The id to attribute to this parameter set.
78+
:type id: str | Literal[pytest.HIDDEN_PARAM] | None
79+
:param id:
80+
The id to attribute to this parameter set.
81+
82+
.. versionadded:: 8.4
83+
:ref:`hidden-param` means to hide the parameter set
84+
from the test name. Can only be used at most 1 time, as
85+
test names need to be unique.
7686
"""
7787
return ParameterSet.param(*values, marks=marks, id=id)
7888

src/_pytest/mark/structures.py

+18-4
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from collections.abc import MutableMapping
1111
from collections.abc import Sequence
1212
import dataclasses
13+
import enum
1314
import inspect
1415
from typing import Any
1516
from typing import final
@@ -38,6 +39,16 @@
3839
EMPTY_PARAMETERSET_OPTION = "empty_parameter_set_mark"
3940

4041

42+
# Singleton type for HIDDEN_PARAM, as described in:
43+
# https://www.python.org/dev/peps/pep-0484/#support-for-singleton-types-in-unions
44+
class _HiddenParam(enum.Enum):
45+
token = 0
46+
47+
48+
#: Can be used as a parameter set id to hide it from the test name.
49+
HIDDEN_PARAM = _HiddenParam.token
50+
51+
4152
def istestfunc(func) -> bool:
4253
return callable(func) and getattr(func, "__name__", "<lambda>") != "<lambda>"
4354

@@ -68,14 +79,14 @@ def get_empty_parameterset_mark(
6879
class ParameterSet(NamedTuple):
6980
values: Sequence[object | NotSetType]
7081
marks: Collection[MarkDecorator | Mark]
71-
id: str | None
82+
id: str | _HiddenParam | None
7283

7384
@classmethod
7485
def param(
7586
cls,
7687
*values: object,
7788
marks: MarkDecorator | Collection[MarkDecorator | Mark] = (),
78-
id: str | None = None,
89+
id: str | _HiddenParam | None = None,
7990
) -> ParameterSet:
8091
if isinstance(marks, MarkDecorator):
8192
marks = (marks,)
@@ -88,8 +99,11 @@ def param(
8899
)
89100

90101
if id is not None:
91-
if not isinstance(id, str):
92-
raise TypeError(f"Expected id to be a string, got {type(id)}: {id!r}")
102+
if not isinstance(id, str) and id is not HIDDEN_PARAM:
103+
raise TypeError(
104+
"Expected id to be a string or a `pytest.HIDDEN_PARAM` sentinel, "
105+
f"got {type(id)}: {id!r}",
106+
)
93107
return cls(values, marks, id)
94108

95109
@classmethod

src/_pytest/python.py

+40-14
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from typing import Any
2626
from typing import final
2727
from typing import Literal
28+
from typing import NoReturn
2829
from typing import TYPE_CHECKING
2930
import warnings
3031

@@ -56,7 +57,9 @@
5657
from _pytest.fixtures import get_scope_node
5758
from _pytest.main import Session
5859
from _pytest.mark import ParameterSet
60+
from _pytest.mark.structures import _HiddenParam
5961
from _pytest.mark.structures import get_unpacked_marks
62+
from _pytest.mark.structures import HIDDEN_PARAM
6063
from _pytest.mark.structures import Mark
6164
from _pytest.mark.structures import MarkDecorator
6265
from _pytest.mark.structures import normalize_mark_list
@@ -473,7 +476,7 @@ def _genfunctions(self, name: str, funcobj) -> Iterator[Function]:
473476
fixtureinfo.prune_dependency_tree()
474477

475478
for callspec in metafunc._calls:
476-
subname = f"{name}[{callspec.id}]"
479+
subname = f"{name}[{callspec.id}]" if callspec._idlist else name
477480
yield Function.from_parent(
478481
self,
479482
name=subname,
@@ -884,7 +887,7 @@ class IdMaker:
884887
# Used only for clearer error messages.
885888
func_name: str | None
886889

887-
def make_unique_parameterset_ids(self) -> list[str]:
890+
def make_unique_parameterset_ids(self) -> list[str | _HiddenParam]:
888891
"""Make a unique identifier for each ParameterSet, that may be used to
889892
identify the parametrization in a node ID.
890893
@@ -905,6 +908,8 @@ def make_unique_parameterset_ids(self) -> list[str]:
905908
# Suffix non-unique IDs to make them unique.
906909
for index, id in enumerate(resolved_ids):
907910
if id_counts[id] > 1:
911+
if id is HIDDEN_PARAM:
912+
self._complain_multiple_hidden_parameter_sets()
908913
suffix = ""
909914
if id and id[-1].isdigit():
910915
suffix = "_"
@@ -919,15 +924,21 @@ def make_unique_parameterset_ids(self) -> list[str]:
919924
)
920925
return resolved_ids
921926

922-
def _resolve_ids(self) -> Iterable[str]:
927+
def _resolve_ids(self) -> Iterable[str | _HiddenParam]:
923928
"""Resolve IDs for all ParameterSets (may contain duplicates)."""
924929
for idx, parameterset in enumerate(self.parametersets):
925930
if parameterset.id is not None:
926931
# ID provided directly - pytest.param(..., id="...")
927-
yield _ascii_escaped_by_config(parameterset.id, self.config)
932+
if parameterset.id is HIDDEN_PARAM:
933+
yield HIDDEN_PARAM
934+
else:
935+
yield _ascii_escaped_by_config(parameterset.id, self.config)
928936
elif self.ids and idx < len(self.ids) and self.ids[idx] is not None:
929937
# ID provided in the IDs list - parametrize(..., ids=[...]).
930-
yield self._idval_from_value_required(self.ids[idx], idx)
938+
if self.ids[idx] is HIDDEN_PARAM:
939+
yield HIDDEN_PARAM
940+
else:
941+
yield self._idval_from_value_required(self.ids[idx], idx)
931942
else:
932943
# ID not provided - generate it.
933944
yield "-".join(
@@ -1001,12 +1012,7 @@ def _idval_from_value_required(self, val: object, idx: int) -> str:
10011012
return id
10021013

10031014
# Fail.
1004-
if self.func_name is not None:
1005-
prefix = f"In {self.func_name}: "
1006-
elif self.nodeid is not None:
1007-
prefix = f"In {self.nodeid}: "
1008-
else:
1009-
prefix = ""
1015+
prefix = self._make_error_prefix()
10101016
msg = (
10111017
f"{prefix}ids contains unsupported value {saferepr(val)} (type: {type(val)!r}) at index {idx}. "
10121018
"Supported types are: str, bytes, int, float, complex, bool, enum, regex or anything with a __name__."
@@ -1019,6 +1025,21 @@ def _idval_from_argname(argname: str, idx: int) -> str:
10191025
and the index of the ParameterSet."""
10201026
return str(argname) + str(idx)
10211027

1028+
def _complain_multiple_hidden_parameter_sets(self) -> NoReturn:
1029+
fail(
1030+
f"{self._make_error_prefix()}multiple instances of HIDDEN_PARAM "
1031+
"cannot be used in the same parametrize call, "
1032+
"because the tests names need to be unique."
1033+
)
1034+
1035+
def _make_error_prefix(self) -> str:
1036+
if self.func_name is not None:
1037+
return f"In {self.func_name}: "
1038+
elif self.nodeid is not None:
1039+
return f"In {self.nodeid}: "
1040+
else:
1041+
return ""
1042+
10221043

10231044
@final
10241045
@dataclasses.dataclass(frozen=True)
@@ -1047,7 +1068,7 @@ def setmulti(
10471068
*,
10481069
argnames: Iterable[str],
10491070
valset: Iterable[object],
1050-
id: str,
1071+
id: str | _HiddenParam,
10511072
marks: Iterable[Mark | MarkDecorator],
10521073
scope: Scope,
10531074
param_index: int,
@@ -1065,7 +1086,7 @@ def setmulti(
10651086
params=params,
10661087
indices=indices,
10671088
_arg2scope=arg2scope,
1068-
_idlist=[*self._idlist, id],
1089+
_idlist=self._idlist if id is HIDDEN_PARAM else [*self._idlist, id],
10691090
marks=[*self.marks, *normalize_mark_list(marks)],
10701091
)
10711092

@@ -1190,6 +1211,11 @@ def parametrize(
11901211
They are mapped to the corresponding index in ``argvalues``.
11911212
``None`` means to use the auto-generated id.
11921213
1214+
.. versionadded:: 8.4
1215+
:ref:`hidden-param` means to hide the parameter set
1216+
from the test name. Can only be used at most 1 time, as
1217+
test names need to be unique.
1218+
11931219
If it is a callable it will be called for each entry in
11941220
``argvalues``, and the return value is used as part of the
11951221
auto-generated id for the whole set (where parts are joined with
@@ -1322,7 +1348,7 @@ def _resolve_parameter_set_ids(
13221348
ids: Iterable[object | None] | Callable[[Any], object | None] | None,
13231349
parametersets: Sequence[ParameterSet],
13241350
nodeid: str,
1325-
) -> list[str]:
1351+
) -> list[str | _HiddenParam]:
13261352
"""Resolve the actual ids for the given parameter sets.
13271353
13281354
:param argnames:

src/pytest/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from _pytest.logging import LogCaptureFixture
3434
from _pytest.main import Dir
3535
from _pytest.main import Session
36+
from _pytest.mark import HIDDEN_PARAM
3637
from _pytest.mark import Mark
3738
from _pytest.mark import MARK_GEN as mark
3839
from _pytest.mark import MarkDecorator
@@ -89,6 +90,7 @@
8990

9091

9192
__all__ = [
93+
"HIDDEN_PARAM",
9294
"Cache",
9395
"CallInfo",
9496
"CaptureFixture",

0 commit comments

Comments
 (0)