From 0929ffd7435d3ad8db5e619526c83a3647c07be5 Mon Sep 17 00:00:00 2001 From: Dionisia Date: Sat, 27 Sep 2025 19:16:55 +0300 Subject: [PATCH 1/3] Update AUTHORS and add changelog entry --- AUTHORS | 1 + changelog/13223.feature.rst | 3 +++ 2 files changed, 4 insertions(+) create mode 100644 changelog/13223.feature.rst diff --git a/AUTHORS b/AUTHORS index 52363a177bb..c5904e194a5 100644 --- a/AUTHORS +++ b/AUTHORS @@ -135,6 +135,7 @@ Dheeraj C K Dhiren Serai Diego Russo Dima Gerasimov +Dionisia Koronellou Dmitry Dygalo Dmitry Pribysh Dominic Mortlock diff --git a/changelog/13223.feature.rst b/changelog/13223.feature.rst new file mode 100644 index 00000000000..7a0efbce59b --- /dev/null +++ b/changelog/13223.feature.rst @@ -0,0 +1,3 @@ +Add `parametrize_order` config option to control the order of multiple parametrize marks. + +By default, stacked `@pytest.mark.parametrize` decorators are applied in application order (from bottom to top). With this new config option set to `declaration`, they are applied in the order they are declared in the source code, which can make test case generation more intuitive. From ae0407f1f400d5cc037a9001459356c9475ee343 Mon Sep 17 00:00:00 2001 From: Dionisia Date: Sat, 27 Sep 2025 19:26:12 +0300 Subject: [PATCH 2/3] Implement stacked parametrize order feature --- src/_pytest/config/__init__.py | 5 +++++ src/_pytest/mark/structures.py | 20 ++++++++++++++++++- src/_pytest/python.py | 6 ++++-- testing/test_mark.py | 36 ++++++++++++++++++++++++++++++++++ 4 files changed, 64 insertions(+), 3 deletions(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index 38fb1ee6d27..f75a42c2070 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1264,6 +1264,11 @@ def _initini(self, args: Sequence[str]) -> None: type="args", default=[], ) + self._parser.addini( + "parametrize_order", + "Order for stacked parametrize marks: 'application' (default) or 'declaration'", + default="application", + ) self._override_ini = ns.override_ini or () def _consider_importhook(self, args: Sequence[str]) -> None: diff --git a/src/_pytest/mark/structures.py b/src/_pytest/mark/structures.py index d0280e26945..6cb81d6a1e7 100644 --- a/src/_pytest/mark/structures.py +++ b/src/_pytest/mark/structures.py @@ -407,12 +407,22 @@ def get_unpacked_marks( obj: object | type, *, consider_mro: bool = True, + config: Config | None = None, ) -> list[Mark]: """Obtain the unpacked marks that are stored on an object. If obj is a class and consider_mro is true, return marks applied to this class and all of its super-classes in MRO order. If consider_mro is false, only return marks applied directly to this class. + + If a pytest Config is provided, the ordering of multiple + @pytest.mark.parametrize decorators can be controlled via the + parametrize_order ini option: + - application (default): honors the natural application order of + decorators (bottom-to-top in Python). + - declaration: reverses the order of consecutive parametrize + marks so they are applied in the order they are written in the + source code (top-to-bottom). """ if isinstance(obj, type): if not consider_mro: @@ -433,7 +443,15 @@ def get_unpacked_marks( mark_list = mark_attribute else: mark_list = [mark_attribute] - return list(normalize_mark_list(mark_list)) + + unpacked = list(normalize_mark_list(mark_list)) + + if config and config.getini("parametrize_order") == "declaration": + parametrize_marks = [m for m in unpacked if m.name == "parametrize"] + other_marks = [m for m in unpacked if m.name != "parametrize"] + unpacked = parametrize_marks[::-1] + other_marks + + return unpacked def normalize_mark_list( diff --git a/src/_pytest/python.py b/src/_pytest/python.py index 3f9da026799..249c24d9cce 100644 --- a/src/_pytest/python.py +++ b/src/_pytest/python.py @@ -281,7 +281,9 @@ def obj(self): # XXX evil hack # used to avoid Function marker duplication if self._ALLOW_MARKERS: - self.own_markers.extend(get_unpacked_marks(self.obj)) + self.own_markers.extend( + get_unpacked_marks(self.obj, config=self.config) + ) # This assumes that `obj` is called before there is a chance # to add custom keys to `self.keywords`, so no fear of overriding. self.keywords.update((mark.name, mark) for mark in self.own_markers) @@ -1598,7 +1600,7 @@ def __init__( # Note: when FunctionDefinition is introduced, we should change ``originalname`` # to a readonly property that returns FunctionDefinition.name. - self.own_markers.extend(get_unpacked_marks(self.obj)) + self.own_markers.extend(get_unpacked_marks(self.obj, config=self.config)) if callspec: self.callspec = callspec self.own_markers.extend(callspec.marks) diff --git a/testing/test_mark.py b/testing/test_mark.py index 9c067c3e430..90ff5c7cd45 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -1291,3 +1291,39 @@ def test_staticmethod_wrapper_on_top(value: int): ) result = pytester.runpytest() result.assert_outcomes(passed=8) + + +def test_stacked_parametrize_order_declaration(pytester: Pytester): + """Check that stacked parametrize marks works in the order that are declared (instead of the one that are applied + if the parametrize_order has been set to declaration in the pytest.ini file. + Test for #13223. + """ + pytester.makeini( + """ + [pytest] + parametrize_order = declaration + """ + ) + test_file = pytester.makepyfile( + """ + import pytest + + @pytest.mark.parametrize("x", ["a1", "a2"]) + @pytest.mark.parametrize("y", ["b1", "b2"]) + def test_permutations(x, y): + assert True + """ + ) + reprec = pytester.inline_run(test_file) + + node_ids = [] + for item in reprec.listoutcomes()[0]: + node_ids.append(item.nodeid.rsplit("::", 1)[-1]) + print(node_ids) + + assert node_ids == [ + "test_permutations[a1-b1]", + "test_permutations[a1-b2]", + "test_permutations[a2-b1]", + "test_permutations[a2-b2]", + ] From 5d1a12542857ad6ca3562c34f584cf9c03a360fb Mon Sep 17 00:00:00 2001 From: DIONISIA KORONELLOU <147346731+DionisiaK4@users.noreply.github.com> Date: Sun, 28 Sep 2025 12:08:09 +0300 Subject: [PATCH 3/3] Update __init__.py Change the position of "parametrize_order" ini option inside the function, in order to pass the test in the pull request --- src/_pytest/config/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/_pytest/config/__init__.py b/src/_pytest/config/__init__.py index f75a42c2070..9e5465c0ad9 100644 --- a/src/_pytest/config/__init__.py +++ b/src/_pytest/config/__init__.py @@ -1264,12 +1264,12 @@ def _initini(self, args: Sequence[str]) -> None: type="args", default=[], ) + self._override_ini = ns.override_ini or () self._parser.addini( "parametrize_order", "Order for stacked parametrize marks: 'application' (default) or 'declaration'", default="application", ) - self._override_ini = ns.override_ini or () def _consider_importhook(self, args: Sequence[str]) -> None: """Install the PEP 302 import hook if using assertion rewriting.