Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions AUTHORS
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ Dheeraj C K
Dhiren Serai
Diego Russo
Dima Gerasimov
Dionisia Koronellou
Dmitry Dygalo
Dmitry Pribysh
Dominic Mortlock
Expand Down
3 changes: 3 additions & 0 deletions changelog/13223.feature.rst
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 5 additions & 0 deletions src/_pytest/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -1265,6 +1265,11 @@ def _initini(self, args: Sequence[str]) -> None:
default=[],
)
self._override_ini = ns.override_ini or ()
self._parser.addini(
"parametrize_order",
"Order for stacked parametrize marks: 'application' (default) or 'declaration'",
default="application",
)

def _consider_importhook(self, args: Sequence[str]) -> None:
"""Install the PEP 302 import hook if using assertion rewriting.
Expand Down
20 changes: 19 additions & 1 deletion src/_pytest/mark/structures.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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(
Expand Down
6 changes: 4 additions & 2 deletions src/_pytest/python.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
36 changes: 36 additions & 0 deletions testing/test_mark.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]",
]
Loading