From 4c0540e098509d77df1fca5315a06044d11b2410 Mon Sep 17 00:00:00 2001 From: Brendan Cazier <520246+cazier@users.noreply.github.com> Date: Sun, 9 Nov 2025 14:58:50 +0000 Subject: [PATCH 1/4] Add an ids attribute to callspec with parametrization names --- src/_pytest/python.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 257dd78f462..1a0bc4398fb 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -1113,6 +1113,8 @@ class CallSpec2: _idlist: Sequence[str] = dataclasses.field(default_factory=tuple) # Marks which will be applied to the item. marks: list[Mark] = dataclasses.field(default_factory=list) + # Parametrization mapping for each argname to an ID. + ids: dict[str | tuple[str, ...], str] = dataclasses.field(default_factory=dict) def setmulti( self, @@ -1142,6 +1144,14 @@ def setmulti( _arg2scope=arg2scope, _idlist=self._idlist if id is HIDDEN_PARAM else [*self._idlist, id], marks=[*self.marks, *normalize_mark_list(marks)], + ids=self.ids + if id is HIDDEN_PARAM + else { + **self.ids, + _argnames[0] + if len(_argnames := tuple(argnames)) == 1 + else _argnames: id, + }, ) def getparam(self, name: str) -> object: From 9515bdff35c0b6464e5601afdd0a192d28ad868c Mon Sep 17 00:00:00 2001 From: Brendan Cazier <520246+cazier@users.noreply.github.com> Date: Sun, 9 Nov 2025 15:11:21 +0000 Subject: [PATCH 2/4] Adding tests for new ids attribute --- testing/test_mark.py | 44 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/testing/test_mark.py b/testing/test_mark.py index 8d76ea310eb..f6c5d614940 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -1090,6 +1090,50 @@ def test_parameterset_for_parametrize_bad_markname(pytester: Pytester) -> None: test_parameterset_for_parametrize_marks(pytester, "bad") +def test_parametrization_callspec_ids(pytester: Pytester): + pytester.makepyfile( + """ +import pytest + +class CustomClass: + pass + +def custom_function(): + return None + +all_ids = [ + {("tuple_one", "tuple_two"): "t1a-t2a", "id_function": "11", "id_explicit": "one", "name": "first"}, + {("tuple_one", "tuple_two"): "t1a-t2a", "id_function": "11", "id_explicit": "one", "name": "2"}, + {("tuple_one", "tuple_two"): "t1a-t2a", "id_function": "11", "id_explicit": "one", "name": "3.5"}, + {("tuple_one", "tuple_two"): "t1a-t2a", "id_function": "11", "id_explicit": "one", "name": "True"}, + {("tuple_one", "tuple_two"): "t1a-t2a", "id_function": "11", "id_explicit": "one", "name": ""}, + {("tuple_one", "tuple_two"): "t1a-t2a", "id_function": "11", "id_explicit": "one", "name": "CustomClass"}, + {("tuple_one", "tuple_two"): "t1a-t2a", "id_function": "11", "id_explicit": "one", "name": "custom_function"}, + {("tuple_one", "tuple_two"): "t1b-t2b", "id_function": "11", "id_explicit": "one", "name": "first"}, + {("tuple_one", "tuple_two"): "t1b-t2b", "id_function": "11", "id_explicit": "one", "name": "2"}, + {("tuple_one", "tuple_two"): "t1b-t2b", "id_function": "11", "id_explicit": "one", "name": "3.5"}, + {("tuple_one", "tuple_two"): "t1b-t2b", "id_function": "11", "id_explicit": "one", "name": "True"}, + {("tuple_one", "tuple_two"): "t1b-t2b", "id_function": "11", "id_explicit": "one", "name": ""}, + {("tuple_one", "tuple_two"): "t1b-t2b", "id_function": "11", "id_explicit": "one", "name": "CustomClass"}, + {("tuple_one", "tuple_two"): "t1b-t2b", "id_function": "11", "id_explicit": "one", "name": "custom_function"} +] + +@pytest.mark.parametrize("name", ("first", 2, 3.5, True, lambda k: k, CustomClass, custom_function)) +@pytest.mark.parametrize("id_explicit", (1,), ids=("one",)) +@pytest.mark.parametrize("id_function", (1,), ids=lambda k: f"{k}{k}") +@pytest.mark.parametrize(("tuple_one", "tuple_two"), (("t1a", "t2a"), ("t1b", "t2b"))) +def test_more(name, id_explicit, id_function, tuple_one, tuple_two, request): + ids = request.node.callspec.ids + assert ids in all_ids +""" + ) + + reprec = pytester.inline_run() + passed, skipped, failed = reprec.countoutcomes() + assert passed == 14 + assert skipped == failed == 0 + + def test_mark_expressions_no_smear(pytester: Pytester) -> None: pytester.makepyfile( """ From 56e686872767f1b3ba3450635fbfd633a9e0f696 Mon Sep 17 00:00:00 2001 From: Brendan Cazier <520246+cazier@users.noreply.github.com> Date: Sun, 9 Nov 2025 15:45:50 +0000 Subject: [PATCH 3/4] Bookkeeping --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index 83509281035..ad68b12bee7 100644 --- a/AUTHORS +++ b/AUTHORS @@ -70,6 +70,7 @@ Benjamin Schubert Bernard Pratz Bo Wu Bob Ippolito +Brendan Cazier Brian Dorsey Brian Larsen Brian Maissy From 2b7719a897d9ee55cbd10d1dd4d65f6aa7b6e562 Mon Sep 17 00:00:00 2001 From: Brendan Cazier <520246+cazier@users.noreply.github.com> Date: Sun, 9 Nov 2025 16:01:20 +0000 Subject: [PATCH 4/4] Adding changelog --- changelog/13907.improvement.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog/13907.improvement.rst diff --git a/changelog/13907.improvement.rst b/changelog/13907.improvement.rst new file mode 100644 index 00000000000..d96eefaeffe --- /dev/null +++ b/changelog/13907.improvement.rst @@ -0,0 +1 @@ +Added a new attribute to the Callspec2 class with the stringified (either automatically or with an explicit `ids` parameter) ID of parametrization values.