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

Allow hiding a parameter set from test name #13229

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ Anthony Shaw
Anthony Sottile
Anton Grinevich
Anton Lodder
Anton Zhilin
Antony Lee
Arel Cordero
Arias Emmanuel
Expand Down
3 changes: 3 additions & 0 deletions changelog/13228.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
:ref:`hidden-param` can now be used in ``id`` of :func:`pytest.param` or in
``ids`` of :py:func:`Metafunc.parametrize <pytest.Metafunc.parametrize>`.
It hides the parameter set from the test name.
10 changes: 10 additions & 0 deletions doc/en/reference/reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,16 @@ The current pytest version, as a string::
>>> pytest.__version__
'7.0.0'

.. _`hidden-param`:

pytest.HIDDEN_PARAM
~~~~~~~~~~~~~~~~~~~

.. versionadded:: 8.4

Can be passed to ``ids`` of :py:func:`Metafunc.parametrize <pytest.Metafunc.parametrize>`
or to ``id`` of :func:`pytest.param` to hide a parameter set from the test name.
Can only be used at most 1 time, as test names need to be unique.

.. _`version-tuple`:

Expand Down
14 changes: 12 additions & 2 deletions src/_pytest/mark/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@

from .expression import Expression
from .expression import ParseError
from .structures import _HiddenParam
from .structures import EMPTY_PARAMETERSET_OPTION
from .structures import get_empty_parameterset_mark
from .structures import HIDDEN_PARAM
from .structures import Mark
from .structures import MARK_GEN
from .structures import MarkDecorator
Expand All @@ -33,6 +35,7 @@


__all__ = [
"HIDDEN_PARAM",
"MARK_GEN",
"Mark",
"MarkDecorator",
Expand All @@ -48,7 +51,7 @@
def param(
*values: object,
marks: MarkDecorator | Collection[MarkDecorator | Mark] = (),
id: str | None = None,
id: str | _HiddenParam | None = None,
) -> ParameterSet:
"""Specify a parameter in `pytest.mark.parametrize`_ calls or
:ref:`parametrized fixtures <fixture-parametrize-marks>`.
Expand All @@ -72,7 +75,14 @@ def test_eval(test_input, expected):

:ref:`pytest.mark.usefixtures <pytest.mark.usefixtures ref>` cannot be added via this parameter.

:param id: The id to attribute to this parameter set.
:type id: str | Literal[pytest.HIDDEN_PARAM] | None
:param id:
The id to attribute to this parameter set.

.. versionadded:: 8.4
:ref:`hidden-param` means to hide the parameter set
from the test name. Can only be used at most 1 time, as
test names need to be unique.
"""
return ParameterSet.param(*values, marks=marks, id=id)

Expand Down
17 changes: 14 additions & 3 deletions src/_pytest/mark/structures.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from collections.abc import MutableMapping
from collections.abc import Sequence
import dataclasses
import enum
import inspect
from typing import Any
from typing import final
Expand Down Expand Up @@ -65,17 +66,27 @@ def get_empty_parameterset_mark(
return mark


# Singleton type for HIDDEN_PARAM, as described in:
# https://www.python.org/dev/peps/pep-0484/#support-for-singleton-types-in-unions
class _HiddenParam(enum.Enum):
token = 0


#: Can be used as a parameter set id to hide it from the test name.
HIDDEN_PARAM = _HiddenParam.token


class ParameterSet(NamedTuple):
values: Sequence[object | NotSetType]
marks: Collection[MarkDecorator | Mark]
id: str | None
id: str | _HiddenParam | None

@classmethod
def param(
cls,
*values: object,
marks: MarkDecorator | Collection[MarkDecorator | Mark] = (),
id: str | None = None,
id: str | _HiddenParam | None = None,
) -> ParameterSet:
if isinstance(marks, MarkDecorator):
marks = (marks,)
Expand All @@ -88,7 +99,7 @@ def param(
)

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

Expand Down
53 changes: 39 additions & 14 deletions src/_pytest/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
from typing import Any
from typing import final
from typing import Literal
from typing import NoReturn
from typing import TYPE_CHECKING
import warnings

Expand Down Expand Up @@ -57,6 +58,7 @@
from _pytest.main import Session
from _pytest.mark import ParameterSet
from _pytest.mark.structures import get_unpacked_marks
from _pytest.mark.structures import HIDDEN_PARAM
from _pytest.mark.structures import Mark
from _pytest.mark.structures import MarkDecorator
from _pytest.mark.structures import normalize_mark_list
Expand Down Expand Up @@ -473,7 +475,7 @@ def _genfunctions(self, name: str, funcobj) -> Iterator[Function]:
fixtureinfo.prune_dependency_tree()

for callspec in metafunc._calls:
subname = f"{name}[{callspec.id}]"
subname = f"{name}[{callspec.id}]" if callspec._idlist else name
yield Function.from_parent(
self,
name=subname,
Expand Down Expand Up @@ -884,7 +886,7 @@ class IdMaker:
# Used only for clearer error messages.
func_name: str | None

def make_unique_parameterset_ids(self) -> list[str]:
def make_unique_parameterset_ids(self) -> list[str | None]:
"""Make a unique identifier for each ParameterSet, that may be used to
identify the parametrization in a node ID.

Expand All @@ -905,6 +907,8 @@ def make_unique_parameterset_ids(self) -> list[str]:
# Suffix non-unique IDs to make them unique.
for index, id in enumerate(resolved_ids):
if id_counts[id] > 1:
if id is None:
self._complain_multiple_hidden_parameter_sets()
suffix = ""
if id and id[-1].isdigit():
suffix = "_"
Expand All @@ -919,15 +923,21 @@ def make_unique_parameterset_ids(self) -> list[str]:
)
return resolved_ids

def _resolve_ids(self) -> Iterable[str]:
def _resolve_ids(self) -> Iterable[str | None]:
"""Resolve IDs for all ParameterSets (may contain duplicates)."""
for idx, parameterset in enumerate(self.parametersets):
if parameterset.id is not None:
# ID provided directly - pytest.param(..., id="...")
yield _ascii_escaped_by_config(parameterset.id, self.config)
if parameterset.id is HIDDEN_PARAM:
yield None
else:
yield _ascii_escaped_by_config(parameterset.id, self.config)
elif self.ids and idx < len(self.ids) and self.ids[idx] is not None:
# ID provided in the IDs list - parametrize(..., ids=[...]).
yield self._idval_from_value_required(self.ids[idx], idx)
if self.ids[idx] is HIDDEN_PARAM:
yield None
else:
yield self._idval_from_value_required(self.ids[idx], idx)
else:
# ID not provided - generate it.
yield "-".join(
Expand Down Expand Up @@ -1001,12 +1011,7 @@ def _idval_from_value_required(self, val: object, idx: int) -> str:
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 = ""
prefix = self._make_error_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__."
Expand All @@ -1019,6 +1024,21 @@ def _idval_from_argname(argname: str, idx: int) -> str:
and the index of the ParameterSet."""
return str(argname) + str(idx)

def _complain_multiple_hidden_parameter_sets(self) -> NoReturn:
fail(
f"{self._make_error_prefix()}multiple instances of HIDDEN_PARAM "
"cannot be used in the same parametrize call, "
"because the tests names need to be unique."
)

def _make_error_prefix(self) -> str:
if self.func_name is not None:
return f"In {self.func_name}: "
elif self.nodeid is not None:
return f"In {self.nodeid}: "
else:
return ""


@final
@dataclasses.dataclass(frozen=True)
Expand Down Expand Up @@ -1047,7 +1067,7 @@ def setmulti(
*,
argnames: Iterable[str],
valset: Iterable[object],
id: str,
id: str | None,
marks: Iterable[Mark | MarkDecorator],
scope: Scope,
param_index: int,
Expand All @@ -1065,7 +1085,7 @@ def setmulti(
params=params,
indices=indices,
_arg2scope=arg2scope,
_idlist=[*self._idlist, id],
_idlist=[*self._idlist, id] if id is not None else self._idlist,
marks=[*self.marks, *normalize_mark_list(marks)],
)

Expand Down Expand Up @@ -1190,6 +1210,11 @@ def parametrize(
They are mapped to the corresponding index in ``argvalues``.
``None`` means to use the auto-generated id.

.. versionadded:: 8.4
:ref:`hidden-param` means to hide the parameter set
from the test name. Can only be used at most 1 time, as
test names need to be unique.

If it is a callable it will be called for each entry in
``argvalues``, and the return value is used as part of the
auto-generated id for the whole set (where parts are joined with
Expand Down Expand Up @@ -1322,7 +1347,7 @@ def _resolve_parameter_set_ids(
ids: Iterable[object | None] | Callable[[Any], object | None] | None,
parametersets: Sequence[ParameterSet],
nodeid: str,
) -> list[str]:
) -> list[str | None]:
"""Resolve the actual ids for the given parameter sets.

:param argnames:
Expand Down
2 changes: 2 additions & 0 deletions src/pytest/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
from _pytest.logging import LogCaptureFixture
from _pytest.main import Dir
from _pytest.main import Session
from _pytest.mark import HIDDEN_PARAM
from _pytest.mark import Mark
from _pytest.mark import MARK_GEN as mark
from _pytest.mark import MarkDecorator
Expand Down Expand Up @@ -89,6 +90,7 @@


__all__ = [
"HIDDEN_PARAM",
"Cache",
"CallInfo",
"CaptureFixture",
Expand Down
Loading