diff --git a/changelog/719.bugfix.rst b/changelog/719.bugfix.rst new file mode 100644 index 00000000000..84d90b00175 --- /dev/null +++ b/changelog/719.bugfix.rst @@ -0,0 +1,3 @@ +Fixed :func:`@pytest.mark.parametrize ` not unpacking single-element tuple values when using a string argnames with a trailing comma (e.g., ``"arg,"``). + +The trailing comma form now correctly behaves like the tuple form ``("arg",)``, treating argvalues as a list of tuples to unpack. diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 9743f673adc..0fa6e8babba 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -170,8 +170,12 @@ def _parse_parametrize_args( **kwargs, ) -> tuple[Sequence[str], bool]: if isinstance(argnames, str): + # A trailing comma indicates tuple-style: "arg," is equivalent to ("arg",) + # In this case, argvalues should be a list of tuples, not wrapped values. + # See https://github.com/pytest-dev/pytest/issues/719 + has_trailing_comma = argnames.rstrip().endswith(",") argnames = [x.strip() for x in argnames.split(",") if x.strip()] - force_tuple = len(argnames) == 1 + force_tuple = len(argnames) == 1 and not has_trailing_comma else: force_tuple = False return argnames, force_tuple diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 7217c80c03d..462976900de 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -76,6 +76,46 @@ def func(arg1, arg2="qwe"): assert metafunc.function is func assert metafunc.cls is None + def test_parametrize_single_arg_trailing_comma(self) -> None: + """Test that trailing comma in string argnames behaves like tuple argnames. + + Regression test for https://github.com/pytest-dev/pytest/issues/719 + + When using a single argument with: + - "arg" (string, no comma): argvalues is a list of values + - "arg," (string, trailing comma): argvalues is a list of tuples (like tuple form) + - ("arg",) (tuple): argvalues is a list of tuples + """ + + def func(arg): + pass + + scenarios = [("a",), ("b",)] + + # Tuple form: argvalues are tuples, unpacked to get the value + metafunc = self.Metafunc(func) + metafunc.parametrize(("arg",), scenarios) + assert metafunc._calls[0].params == {"arg": "a"} + assert metafunc._calls[1].params == {"arg": "b"} + + # String with trailing comma: should behave like tuple form + metafunc = self.Metafunc(func) + metafunc.parametrize("arg,", scenarios) + assert metafunc._calls[0].params == {"arg": "a"} + assert metafunc._calls[1].params == {"arg": "b"} + + # String without comma: argvalues are values directly (tuples are passed as-is) + metafunc = self.Metafunc(func) + metafunc.parametrize("arg", scenarios) + assert metafunc._calls[0].params == {"arg": ("a",)} + assert metafunc._calls[1].params == {"arg": ("b",)} + + # String without comma with plain values: values are used directly + metafunc = self.Metafunc(func) + metafunc.parametrize("arg", ["a", "b"]) + assert metafunc._calls[0].params == {"arg": "a"} + assert metafunc._calls[1].params == {"arg": "b"} + def test_parametrize_error(self) -> None: def func(x, y): pass @@ -1256,6 +1296,41 @@ def test_hello(arg1, arg2): ["*(1, 4)*", "*(1, 5)*", "*(2, 4)*", "*(2, 5)*", "*4 failed*"] ) + def test_parametrize_single_arg_trailing_comma_functional( + self, pytester: Pytester + ) -> None: + """Test that trailing comma in string argnames behaves like tuple argnames. + + Regression test for https://github.com/pytest-dev/pytest/issues/719 + """ + pytester.makepyfile( + """ + import pytest + + scenarios = [('a',), ('b',)] + + @pytest.mark.parametrize(("arg",), scenarios) + def test_tuple_form(arg): + # Tuple argnames: values are unpacked from tuples + assert arg in ('a', 'b') + assert isinstance(arg, str) + + @pytest.mark.parametrize("arg,", scenarios) + def test_string_trailing_comma(arg): + # String with trailing comma: should behave like tuple form + assert arg in ('a', 'b') + assert isinstance(arg, str) + + @pytest.mark.parametrize("arg", scenarios) + def test_string_no_comma(arg): + # String without comma: tuples are passed as-is + assert arg in (('a',), ('b',)) + assert isinstance(arg, tuple) + """ + ) + result = pytester.runpytest("-v") + result.assert_outcomes(passed=6) + def test_parametrize_and_inner_getfixturevalue(self, pytester: Pytester) -> None: p = pytester.makepyfile( """