Skip to content

Commit 189d251

Browse files
committed
Allow hiding a parameter set from test name
Resolves: #13228
1 parent b0caf3d commit 189d251

File tree

7 files changed

+177
-18
lines changed

7 files changed

+177
-18
lines changed

AUTHORS

Lines changed: 1 addition & 0 deletions
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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
``pytest.HIDDEN_PARAM`` can now be used in ``id`` of ``pytest.param`` or in ``ids`` of ``parametrize``. It hides the parameter set from the test name.

src/_pytest/mark/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from .expression import ParseError
1515
from .structures import EMPTY_PARAMETERSET_OPTION
1616
from .structures import get_empty_parameterset_mark
17+
from .structures import HIDDEN_PARAM
1718
from .structures import Mark
1819
from .structures import MARK_GEN
1920
from .structures import MarkDecorator
@@ -33,6 +34,7 @@
3334

3435

3536
__all__ = [
37+
"HIDDEN_PARAM",
3638
"MARK_GEN",
3739
"Mark",
3840
"MarkDecorator",

src/_pytest/mark/structures.py

Lines changed: 14 additions & 3 deletions
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
@@ -65,17 +66,27 @@ def get_empty_parameterset_mark(
6566
return mark
6667

6768

69+
# Singleton type for NOTSET, as described in:
70+
# https://www.python.org/dev/peps/pep-0484/#support-for-singleton-types-in-unions
71+
class _HiddenParam(enum.Enum):
72+
token = 0
73+
74+
75+
#: Can be used as a parameter set id to hide it from the test name.
76+
HIDDEN_PARAM = _HiddenParam.token
77+
78+
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,7 +99,7 @@ def param(
8899
)
89100

90101
if id is not None:
91-
if not isinstance(id, str):
102+
if not isinstance(id, str) and id is not HIDDEN_PARAM:
92103
raise TypeError(f"Expected id to be a string, got {type(id)}: {id!r}")
93104
return cls(values, marks, id)
94105

src/_pytest/python.py

Lines changed: 38 additions & 15 deletions
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

@@ -57,6 +58,7 @@
5758
from _pytest.main import Session
5859
from _pytest.mark import ParameterSet
5960
from _pytest.mark.structures import get_unpacked_marks
61+
from _pytest.mark.structures import HIDDEN_PARAM
6062
from _pytest.mark.structures import Mark
6163
from _pytest.mark.structures import MarkDecorator
6264
from _pytest.mark.structures import normalize_mark_list
@@ -473,13 +475,14 @@ def _genfunctions(self, name: str, funcobj) -> Iterator[Function]:
473475
fixtureinfo.prune_dependency_tree()
474476

475477
for callspec in metafunc._calls:
476-
subname = f"{name}[{callspec.id}]"
478+
params_id = callspec.id
479+
subname = f"{name}[{params_id}]" if params_id else name
477480
yield Function.from_parent(
478481
self,
479482
name=subname,
480483
callspec=callspec,
481484
fixtureinfo=fixtureinfo,
482-
keywords={callspec.id: True},
485+
keywords={params_id: True},
483486
originalname=name,
484487
)
485488

@@ -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 | None]:
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 None:
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 | None]:
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 None
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 None
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 | None,
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, id] if id else self._idlist,
10691090
marks=[*self.marks, *normalize_mark_list(marks)],
10701091
)
10711092

@@ -1189,6 +1210,8 @@ def parametrize(
11891210
``bool``, or ``None``.
11901211
They are mapped to the corresponding index in ``argvalues``.
11911212
``None`` means to use the auto-generated id.
1213+
``pytest.HIDDEN_PARAM`` means to hide the parameter set
1214+
from the test name.
11921215
11931216
If it is a callable it will be called for each entry in
11941217
``argvalues``, and the return value is used as part of the
@@ -1322,7 +1345,7 @@ def _resolve_parameter_set_ids(
13221345
ids: Iterable[object | None] | Callable[[Any], object | None] | None,
13231346
parametersets: Sequence[ParameterSet],
13241347
nodeid: str,
1325-
) -> list[str]:
1348+
) -> list[str | None]:
13261349
"""Resolve the actual ids for the given parameter sets.
13271350
13281351
:param argnames:

src/pytest/__init__.py

Lines changed: 2 additions & 0 deletions
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",

testing/python/metafunc.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2143,3 +2143,122 @@ def test_converted_to_str(a, b):
21432143
"*= 6 passed in *",
21442144
]
21452145
)
2146+
2147+
2148+
class TestHiddenParam:
2149+
"""Test that pytest.HIDDEN_PARAM works"""
2150+
2151+
def test_parametrize_ids(self, pytester: Pytester) -> None:
2152+
pytester.makepyfile(
2153+
"""
2154+
import pytest
2155+
2156+
@pytest.mark.parametrize(
2157+
("foo", "bar"),
2158+
[
2159+
("a", "x"),
2160+
("b", "y"),
2161+
("c", "z"),
2162+
],
2163+
ids=["paramset1", pytest.HIDDEN_PARAM, "paramset3"],
2164+
)
2165+
def test_func(foo, bar):
2166+
pass
2167+
"""
2168+
)
2169+
result = pytester.runpytest("-vv", "-s")
2170+
result.stdout.fnmatch_lines(
2171+
[
2172+
"test_parametrize_ids.py::test_func[paramset1] PASSED",
2173+
"test_parametrize_ids.py::test_func PASSED",
2174+
"test_parametrize_ids.py::test_func[paramset3] PASSED",
2175+
"*= 3 passed in *",
2176+
]
2177+
)
2178+
2179+
def test_param_id(self, pytester: Pytester) -> None:
2180+
pytester.makepyfile(
2181+
"""
2182+
import pytest
2183+
2184+
@pytest.mark.parametrize(
2185+
("foo", "bar"),
2186+
[
2187+
pytest.param("a", "x", id="paramset1"),
2188+
pytest.param("b", "y", id=pytest.HIDDEN_PARAM),
2189+
("c", "z"),
2190+
],
2191+
)
2192+
def test_func(foo, bar):
2193+
pass
2194+
"""
2195+
)
2196+
result = pytester.runpytest("-vv", "-s")
2197+
result.stdout.fnmatch_lines(
2198+
[
2199+
"test_param_id.py::test_func[paramset1] PASSED",
2200+
"test_param_id.py::test_func PASSED",
2201+
"test_param_id.py::test_func[c-z] PASSED",
2202+
"*= 3 passed in *",
2203+
]
2204+
)
2205+
2206+
def test_multiple_hidden_param_is_forbidden(self, pytester: Pytester) -> None:
2207+
pytester.makepyfile(
2208+
"""
2209+
import pytest
2210+
2211+
@pytest.mark.parametrize(
2212+
("foo", "bar"),
2213+
[
2214+
("a", "x"),
2215+
("b", "y"),
2216+
],
2217+
ids=[pytest.HIDDEN_PARAM, pytest.HIDDEN_PARAM],
2218+
)
2219+
def test_func(foo, bar):
2220+
pass
2221+
"""
2222+
)
2223+
result = pytester.runpytest("--collect-only")
2224+
result.stdout.fnmatch_lines(
2225+
[
2226+
"collected 0 items / 1 error",
2227+
"",
2228+
"*= ERRORS =*",
2229+
"*_ ERROR collecting test_multiple_hidden_param_is_forbidden.py _*",
2230+
"E Failed: In test_func: multiple instances of HIDDEN_PARAM cannot be used "
2231+
"in the same parametrize call, because the tests names need to be unique.",
2232+
"*! Interrupted: 1 error during collection !*",
2233+
"*= no tests collected, 1 error in *",
2234+
]
2235+
)
2236+
2237+
def test_multiple_parametrize(self, pytester: Pytester) -> None:
2238+
pytester.makepyfile(
2239+
"""
2240+
import pytest
2241+
2242+
@pytest.mark.parametrize(
2243+
"bar",
2244+
["x", "y"],
2245+
)
2246+
@pytest.mark.parametrize(
2247+
"foo",
2248+
["a", "b"],
2249+
ids=["a", pytest.HIDDEN_PARAM],
2250+
)
2251+
def test_func(foo, bar):
2252+
pass
2253+
"""
2254+
)
2255+
result = pytester.runpytest("-vv", "-s")
2256+
result.stdout.fnmatch_lines(
2257+
[
2258+
"test_multiple_parametrize.py::test_func[a-x] PASSED",
2259+
"test_multiple_parametrize.py::test_func[a-y] PASSED",
2260+
"test_multiple_parametrize.py::test_func[x] PASSED",
2261+
"test_multiple_parametrize.py::test_func[y] PASSED",
2262+
"*= 4 passed in *",
2263+
]
2264+
)

0 commit comments

Comments
 (0)