Skip to content

Commit ae0407f

Browse files
committed
Implement stacked parametrize order feature
1 parent 0929ffd commit ae0407f

File tree

4 files changed

+64
-3
lines changed

4 files changed

+64
-3
lines changed

src/_pytest/config/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1264,6 +1264,11 @@ def _initini(self, args: Sequence[str]) -> None:
12641264
type="args",
12651265
default=[],
12661266
)
1267+
self._parser.addini(
1268+
"parametrize_order",
1269+
"Order for stacked parametrize marks: 'application' (default) or 'declaration'",
1270+
default="application",
1271+
)
12671272
self._override_ini = ns.override_ini or ()
12681273

12691274
def _consider_importhook(self, args: Sequence[str]) -> None:

src/_pytest/mark/structures.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -407,12 +407,22 @@ def get_unpacked_marks(
407407
obj: object | type,
408408
*,
409409
consider_mro: bool = True,
410+
config: Config | None = None,
410411
) -> list[Mark]:
411412
"""Obtain the unpacked marks that are stored on an object.
412413
413414
If obj is a class and consider_mro is true, return marks applied to
414415
this class and all of its super-classes in MRO order. If consider_mro
415416
is false, only return marks applied directly to this class.
417+
418+
If a pytest Config is provided, the ordering of multiple
419+
@pytest.mark.parametrize decorators can be controlled via the
420+
parametrize_order ini option:
421+
- application (default): honors the natural application order of
422+
decorators (bottom-to-top in Python).
423+
- declaration: reverses the order of consecutive parametrize
424+
marks so they are applied in the order they are written in the
425+
source code (top-to-bottom).
416426
"""
417427
if isinstance(obj, type):
418428
if not consider_mro:
@@ -433,7 +443,15 @@ def get_unpacked_marks(
433443
mark_list = mark_attribute
434444
else:
435445
mark_list = [mark_attribute]
436-
return list(normalize_mark_list(mark_list))
446+
447+
unpacked = list(normalize_mark_list(mark_list))
448+
449+
if config and config.getini("parametrize_order") == "declaration":
450+
parametrize_marks = [m for m in unpacked if m.name == "parametrize"]
451+
other_marks = [m for m in unpacked if m.name != "parametrize"]
452+
unpacked = parametrize_marks[::-1] + other_marks
453+
454+
return unpacked
437455

438456

439457
def normalize_mark_list(

src/_pytest/python.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,9 @@ def obj(self):
281281
# XXX evil hack
282282
# used to avoid Function marker duplication
283283
if self._ALLOW_MARKERS:
284-
self.own_markers.extend(get_unpacked_marks(self.obj))
284+
self.own_markers.extend(
285+
get_unpacked_marks(self.obj, config=self.config)
286+
)
285287
# This assumes that `obj` is called before there is a chance
286288
# to add custom keys to `self.keywords`, so no fear of overriding.
287289
self.keywords.update((mark.name, mark) for mark in self.own_markers)
@@ -1598,7 +1600,7 @@ def __init__(
15981600
# Note: when FunctionDefinition is introduced, we should change ``originalname``
15991601
# to a readonly property that returns FunctionDefinition.name.
16001602

1601-
self.own_markers.extend(get_unpacked_marks(self.obj))
1603+
self.own_markers.extend(get_unpacked_marks(self.obj, config=self.config))
16021604
if callspec:
16031605
self.callspec = callspec
16041606
self.own_markers.extend(callspec.marks)

testing/test_mark.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1291,3 +1291,39 @@ def test_staticmethod_wrapper_on_top(value: int):
12911291
)
12921292
result = pytester.runpytest()
12931293
result.assert_outcomes(passed=8)
1294+
1295+
1296+
def test_stacked_parametrize_order_declaration(pytester: Pytester):
1297+
"""Check that stacked parametrize marks works in the order that are declared (instead of the one that are applied
1298+
if the parametrize_order has been set to declaration in the pytest.ini file.
1299+
Test for #13223.
1300+
"""
1301+
pytester.makeini(
1302+
"""
1303+
[pytest]
1304+
parametrize_order = declaration
1305+
"""
1306+
)
1307+
test_file = pytester.makepyfile(
1308+
"""
1309+
import pytest
1310+
1311+
@pytest.mark.parametrize("x", ["a1", "a2"])
1312+
@pytest.mark.parametrize("y", ["b1", "b2"])
1313+
def test_permutations(x, y):
1314+
assert True
1315+
"""
1316+
)
1317+
reprec = pytester.inline_run(test_file)
1318+
1319+
node_ids = []
1320+
for item in reprec.listoutcomes()[0]:
1321+
node_ids.append(item.nodeid.rsplit("::", 1)[-1])
1322+
print(node_ids)
1323+
1324+
assert node_ids == [
1325+
"test_permutations[a1-b1]",
1326+
"test_permutations[a1-b2]",
1327+
"test_permutations[a2-b1]",
1328+
"test_permutations[a2-b2]",
1329+
]

0 commit comments

Comments
 (0)