diff --git a/changelog/12628.feature.rst b/changelog/12628.feature.rst new file mode 100644 index 00000000000..b565cb89938 --- /dev/null +++ b/changelog/12628.feature.rst @@ -0,0 +1,3 @@ +Existing parameters (argname and case index) are passed to hook and idfn for parameterized test ID creation. + +-- by :user:`nicoloborghi` diff --git a/src/_pytest/fixtures.py b/src/_pytest/fixtures.py index 7d0b40b150a..6baf9e76e2a 100644 --- a/src/_pytest/fixtures.py +++ b/src/_pytest/fixtures.py @@ -955,7 +955,9 @@ def __init__( func: _FixtureFunc[FixtureValue], scope: Scope | _ScopeName | Callable[[str, Config], _ScopeName] | None, params: Sequence[object] | None, - ids: tuple[object | None, ...] | Callable[[Any], object | None] | None = None, + ids: tuple[object | None, ...] + | Callable[[Any, str, int], object | None] + | None = None, *, _ispytest: bool = False, ) -> None: @@ -1180,7 +1182,9 @@ class FixtureFunctionMarker: scope: _ScopeName | Callable[[str, Config], _ScopeName] params: tuple[object, ...] | None autouse: bool = False - ids: tuple[object | None, ...] | Callable[[Any], object | None] | None = None + ids: tuple[object | None, ...] | Callable[[Any, str, int], object | None] | None = ( + None + ) name: str | None = None _ispytest: dataclasses.InitVar[bool] = False @@ -1222,7 +1226,9 @@ def fixture( scope: _ScopeName | Callable[[str, Config], _ScopeName] = ..., params: Iterable[object] | None = ..., autouse: bool = ..., - ids: Sequence[object | None] | Callable[[Any], object | None] | None = ..., + ids: Sequence[object | None] + | Callable[[Any, str, int], object | None] + | None = ..., name: str | None = ..., ) -> FixtureFunction: ... @@ -1234,7 +1240,9 @@ def fixture( scope: _ScopeName | Callable[[str, Config], _ScopeName] = ..., params: Iterable[object] | None = ..., autouse: bool = ..., - ids: Sequence[object | None] | Callable[[Any], object | None] | None = ..., + ids: Sequence[object | None] + | Callable[[Any, str, int], object | None] + | None = ..., name: str | None = None, ) -> FixtureFunctionMarker: ... @@ -1245,7 +1253,9 @@ def fixture( scope: _ScopeName | Callable[[str, Config], _ScopeName] = "function", params: Iterable[object] | None = None, autouse: bool = False, - ids: Sequence[object | None] | Callable[[Any], object | None] | None = None, + ids: Sequence[object | None] + | Callable[[Any, str, int], object | None] + | None = None, name: str | None = None, ) -> FixtureFunctionMarker | FixtureFunction: """Decorator to mark a fixture factory function. @@ -1633,7 +1643,9 @@ def _register_fixture( nodeid: str | None, scope: Scope | _ScopeName | Callable[[str, Config], _ScopeName] = "function", params: Sequence[object] | None = None, - ids: tuple[object | None, ...] | Callable[[Any], object | None] | None = None, + ids: tuple[object | None, ...] + | Callable[[Any, str, int], object | None] + | None = None, autouse: bool = False, ) -> None: """Register a fixture diff --git a/src/_pytest/hookspec.py b/src/_pytest/hookspec.py index 99614899994..791c75046b4 100644 --- a/src/_pytest/hookspec.py +++ b/src/_pytest/hookspec.py @@ -588,7 +588,9 @@ def pytest_generate_tests(metafunc: Metafunc) -> None: @hookspec(firstresult=True) -def pytest_make_parametrize_id(config: Config, val: object, argname: str) -> str | None: +def pytest_make_parametrize_id( + config: Config, val: object, argname: str, idx: int +) -> str | None: """Return a user-friendly string representation of the given ``val`` that will be used by @pytest.mark.parametrize calls, or None if the hook doesn't know about ``val``. @@ -600,6 +602,7 @@ def pytest_make_parametrize_id(config: Config, val: object, argname: str) -> str :param config: The pytest config object. :param val: The parametrized value. :param argname: The automatic parameter name produced by pytest. + :param idx: The iteration number of parameterized tests as calculated by pytest. Use in conftest plugins ======================= diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 9182ce7dfe9..6ff114d2285 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -869,7 +869,7 @@ class IdMaker: parametersets: Sequence[ParameterSet] # Optionally, a user-provided callable to make IDs for parameters in a # ParameterSet. - idfn: Callable[[Any], object | None] | None + idfn: Callable[[Any, str, int], object | None] | None # Optionally, explicit IDs for ParameterSets by index. ids: Sequence[object | None] | None # Optionally, the pytest config. @@ -939,7 +939,7 @@ def _idval(self, val: object, argname: str, idx: int) -> str: idval = self._idval_from_function(val, argname, idx) if idval is not None: return idval - idval = self._idval_from_hook(val, argname) + idval = self._idval_from_hook(val, argname, idx) if idval is not None: return idval idval = self._idval_from_value(val) @@ -953,7 +953,7 @@ def _idval_from_function(self, val: object, argname: str, idx: int) -> str | Non if self.idfn is None: return None try: - id = self.idfn(val) + id = self.idfn(val, argname, idx) except Exception as e: prefix = f"{self.nodeid}: " if self.nodeid is not None else "" msg = "error raised while trying to determine id of parameter '{}' at position {}" @@ -963,12 +963,12 @@ def _idval_from_function(self, val: object, argname: str, idx: int) -> str | Non return None return self._idval_from_value(id) - def _idval_from_hook(self, val: object, argname: str) -> str | None: + def _idval_from_hook(self, val: object, argname: str, idx: int) -> str | None: """Try to make an ID for a parameter in a ParameterSet by calling the :hook:`pytest_make_parametrize_id` hook.""" if self.config: id: str | None = self.config.hook.pytest_make_parametrize_id( - config=self.config, val=val, argname=argname + config=self.config, val=val, argname=argname, idx=idx ) return id return None @@ -1136,7 +1136,9 @@ def parametrize( argnames: str | Sequence[str], argvalues: Iterable[ParameterSet | Sequence[object] | object], indirect: bool | Sequence[str] = False, - ids: Iterable[object | None] | Callable[[Any], object | None] | None = None, + ids: Iterable[object | None] + | Callable[[Any, str, int], object | None] + | None = None, scope: _ScopeName | None = None, *, _param_mark: Mark | None = None, @@ -1315,7 +1317,7 @@ def parametrize( def _resolve_parameter_set_ids( self, argnames: Sequence[str], - ids: Iterable[object | None] | Callable[[Any], object | None] | None, + ids: Iterable[object | None] | Callable[[Any, str, int], object | None] | None, parametersets: Sequence[ParameterSet], nodeid: str, ) -> list[str]: diff --git a/src/_pytest/setuponly.py b/src/_pytest/setuponly.py index de297f408d3..c9bac4bfa66 100644 --- a/src/_pytest/setuponly.py +++ b/src/_pytest/setuponly.py @@ -41,7 +41,11 @@ def pytest_fixture_setup( # display it now and during the teardown (in .finish()). if fixturedef.ids: if callable(fixturedef.ids): - param = fixturedef.ids(request.param) + param = fixturedef.ids( + request.param, + request.fixturename or "", + request.param_index, + ) else: param = fixturedef.ids[request.param_index] else: diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 2dd85607e71..3ffb843e462 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -514,7 +514,7 @@ def test_idmaker_enum(self) -> None: def test_idmaker_idfn(self) -> None: """#351""" - def ids(val: object) -> str | None: + def ids(val: object, argname: str, idx: int) -> str | None: if isinstance(val, Exception): return repr(val) return None @@ -537,7 +537,7 @@ def ids(val: object) -> str | None: def test_idmaker_idfn_unique_names(self) -> None: """#351""" - def ids(val: object) -> str: + def ids(val: object, argname: str, idx: int) -> str: return "a" result = IdMaker( @@ -585,7 +585,7 @@ def getini(self, name): result = IdMaker( ("a",), [pytest.param("string")], - lambda _: "ação", + lambda _, __, ___: "ação", None, config, None, diff --git a/testing/typing_checks.py b/testing/typing_checks.py index d4d6a97aea6..e575fbe7d63 100644 --- a/testing/typing_checks.py +++ b/testing/typing_checks.py @@ -8,6 +8,9 @@ from __future__ import annotations import contextlib +from typing import Any +from typing import Callable +from typing import cast from typing import Optional from typing_extensions import assert_type @@ -23,7 +26,12 @@ def check_mark_xfail_raises() -> None: # Issue #7494. -@pytest.fixture(params=[(0, 0), (1, 1)], ids=lambda x: str(x[0])) +@pytest.fixture( + params=[(0, 0), (1, 1)], + ids=cast( + Callable[[Any, str, int], object | None] | None, lambda x, _, __: str(x[0]) + ), +) def check_fixture_ids_callable() -> None: pass