diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index 18ebc506abb..a8caabf2910 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -2,6 +2,8 @@ import warnings from collections import namedtuple from collections.abc import MutableMapping +from typing import List +from typing import Optional from typing import Set import attr @@ -144,7 +146,13 @@ class Mark: #: keyword arguments of the mark decorator kwargs = attr.ib() # Dict[str, object] - def combined_with(self, other): + _param_ids_from = attr.ib(type=Optional["Mark"], default=None) + _param_ids_generated = attr.ib(type=Optional[List[str]], default=None) + + def _has_param_ids(self): + return "ids" in self.kwargs or len(self.args) >= 4 + + def combined_with(self, other: "Mark") -> "Mark": """ :param other: the mark to combine with :type other: Mark @@ -153,8 +161,19 @@ def combined_with(self, other): combines by appending args and merging the mappings """ assert self.name == other.name + + param_ids_from = None # type: Optional[Mark] + if self.name == "parametrize": + if other._has_param_ids(): + param_ids_from = other + elif self._has_param_ids(): + param_ids_from = self + return Mark( - self.name, self.args + other.args, dict(self.kwargs, **other.kwargs) + self.name, + self.args + other.args, + dict(self.kwargs, **other.kwargs), + param_ids_from=param_ids_from, ) diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 77954239279..956d75d1c0f 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -132,7 +132,25 @@ def pytest_generate_tests(metafunc): _validate_parametrize_spelling(metafunc) for marker in metafunc.definition.iter_markers(name="parametrize"): - metafunc.parametrize(*marker.args, **marker.kwargs) + args, kwargs = marker.args, marker.kwargs + if marker._param_ids_from: + ids = marker._param_ids_from._param_ids_generated + if ids is None: + save_ids = True + else: + save_ids = False + if len(args) > 3: + args = args.copy() + args[3] = ids + else: + assert "ids" in kwargs + kwargs = kwargs.copy() + kwargs["ids"] = ids + + new_ids = metafunc.parametrize(*args, **kwargs) + + if marker._param_ids_from and save_ids: + object.__setattr__(marker._param_ids_from, "_param_ids_generated", new_ids) def pytest_configure(config): @@ -1024,6 +1042,7 @@ def parametrize(self, argnames, argvalues, indirect=False, ids=None, scope=None) ) newcalls.append(newcallspec) self._calls = newcalls + return ids def _resolve_arg_ids(self, argnames, ids, parameters, item): """Resolves the actual ids for the given argnames, based on the ``ids`` parameter given diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 1d8e90002e6..6e88e2223f1 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -1849,12 +1849,12 @@ def test_converted_to_str(a, b): """ ) result = testdir.runpytest("-vv", "-s") - result.stdout.fnmatch_lines_random( # random for py35. + result.stdout.fnmatch_lines( [ "test_parametrize_iterator.py::test1[param0] PASSED", "test_parametrize_iterator.py::test1[param1] PASSED", - "test_parametrize_iterator.py::test2[param2] PASSED", - "test_parametrize_iterator.py::test2[param3] PASSED", + "test_parametrize_iterator.py::test2[param0] PASSED", + "test_parametrize_iterator.py::test2[param1] PASSED", "test_parametrize_iterator.py::test_converted_to_str[0] PASSED", "test_parametrize_iterator.py::test_converted_to_str[1] PASSED", "*= 6 passed in *",