diff --git a/changelog/13235.bugfix.rst b/changelog/13235.bugfix.rst new file mode 100644 index 00000000000..88ef80a8e2d --- /dev/null +++ b/changelog/13235.bugfix.rst @@ -0,0 +1,5 @@ +Fixed :func:`pytest.mark.parametrize` with an empty ``argvalues`` list and a +callable ``ids`` parameter leaking the internal ``NOTSET`` string as the test +ID (e.g. ``test_foo[NOTSET]``). The callable is no longer invoked for the +placeholder parameterset, and the generated ID now follows the standard +auto-naming convention (e.g. ``test_foo[x0]``). diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 35fd24f8f96..6a8f3cd84ed 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -237,9 +237,7 @@ def _for_parametrize( # parameter set with NOTSET values, with the "empty parameter set" mark applied to it. mark = get_empty_parameterset_mark(config, argnames, func) parameters.append( - ParameterSet( - values=(NOTSET,) * len(argnames), marks=[mark], id="NOTSET" - ) + ParameterSet(values=(NOTSET,) * len(argnames), marks=[mark], id=None) ) return argnames, parameters diff --git a/src/_pytest/python.py b/src/_pytest/python.py index ad5a2c6a59b..b8e152a2131 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -1013,6 +1013,8 @@ def _idval_from_function(self, val: object, argname: str, idx: int) -> str | Non user-provided id callable, if given.""" if self.idfn is None: return None + if val is NOTSET: + return None try: id = self.idfn(val) except Exception as e: diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 026589d65f5..fa91e433d39 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -1475,6 +1475,21 @@ def test_temp(temp): result = pytester.runpytest() result.stdout.fnmatch_lines(["* 1 skipped *"]) + def test_parametrize_empty_argvalues_callable_ids(self, pytester: Pytester) -> None: + """Callable ids with empty argvalues must not crash or leak 'NOTSET' (#13235).""" + pytester.makepyfile( + """ + import pytest + + @pytest.mark.parametrize("x", [], ids=lambda x: x.id) + def test_foo(x): + pass + """ + ) + result = pytester.runpytest("-v") + result.stdout.no_fnmatch_line("*NOTSET*") + result.stdout.fnmatch_lines(["* 1 skipped *"]) + def test_parametrized_ids_invalid_type(self, pytester: Pytester) -> None: """Test error with non-strings/non-ints, without generator (#1857).""" pytester.makepyfile(