Skip to content
Merged
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
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# pytest-order Release Notes

## Unreleased

### Fixes
* `--order-marker-prefix` uses the complete marker suffix as the order index,
so multi-digit and negative indices work; negative indices can be written
as `neg` (for example `m_neg1` for -1)

## [Version 1.5.0](https://pypi.org/project/pytest-order/1.5.0/) (2026-06-13)
Adds option to abort tests after a failed ordering.

Expand Down
23 changes: 23 additions & 0 deletions docs/source/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -494,6 +494,29 @@ gives you now::
test_module.py:18: test_c PASSED
test_module.py:6: test_a PASSED

The complete suffix after the prefix is used as the integer order index. For
example, ``m11`` has index 11. A marker without an integer suffix is ignored.
An explicit ``order`` marker takes precedence.

A minus sign cannot be written in ``@pytest.mark.<name>``, so for a negative
index write ``neg`` in its place, optionally preceded by an underscore:
``m_neg1`` (or ``mneg1``) has index -1. You can also use the minus sign itself
by looking the marker up by name:

.. code:: python

import pytest


@pytest.mark.m_neg1
def test_last():
assert True


@getattr(pytest.mark, "m-2")
def test_second_to_last():
assert True

.. note::
As usually, you are responsible for registering your own markers, either in the
code or in the ``pytest.ini`` file. If you forget this, pytest will give you warnings about unknown markers.
Expand Down
4 changes: 3 additions & 1 deletion src/pytest_order/sorter.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,10 @@ def mark_binning(
if not has_order and self.settings.marker_prefix:
for key in keys:
if key.startswith(self.settings.marker_prefix):
suffix = key[len(self.settings.marker_prefix) :]
negative = re.fullmatch(r"_?neg(\d+)", suffix)
try:
index = int(key[len(self.settings.marker_prefix)])
index = -int(negative[1]) if negative else int(suffix)
item.order = index
except ValueError:
pass
Expand Down
155 changes: 155 additions & 0 deletions tests/test_marker_prefix.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,3 +161,158 @@ def test_c():
"test_marker.py::test_a PASSED",
]
)


@pytest.mark.parametrize(
"suffix, expected",
[
("11", ["test_b", "test_a"]),
("12", ["test_b", "test_a"]),
("", ["test_b", "test_a"]),
("1special", ["test_b", "test_a"]),
("other", ["test_b", "test_a"]),
("0", ["test_a", "test_b"]),
],
)
def test_marker_prefix_uses_complete_numeric_suffix(test_path, suffix, expected):
test_path.makeini(
f"""
[pytest]
console_output_style = classic
markers =
my{suffix}: custom order
my2: custom order
"""
)
test_path.makepyfile(
test_marker=f"""
import pytest

@getattr(pytest.mark, "my{suffix}")

@mrbean-bremen mrbean-bremen Oct 25, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not very happy that this syntax is needed for negative markers, as this is not intuitive, and most users won't know it. Maybe it would make sense to add some placeholder for the minus (something like _neg or _minus) to make it easier to use (additionally to supporting actual negative numbers).
At least it would make sense to add a note to the documentation describing how negative numbers can be set in a marker.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. I've added a neg placeholder: after the prefix, _neg1 or neg1 gives -1, so @pytest.mark.m_neg1 works without getattr. Literal negative suffixes still work. The configuration docs now explain both, and the release notes have an Unreleased entry. Happy to change the spelling to _minus if you prefer.

def test_a():
pass

@pytest.mark.my2
def test_b():
pass
"""
)
result = test_path.runpytest("-v", "--order-marker-prefix=my")
result.assert_outcomes(passed=2)
result.stdout.fnmatch_lines([f"test_marker.py::{name} PASSED" for name in expected])


def test_marker_prefix_supports_negative_indices(test_path):
test_path.makeini(
"""
[pytest]
console_output_style = classic
markers =
my-1: custom order
my-2: custom order
my2: custom order
"""
)
test_path.makepyfile(
test_marker="""
import pytest

@getattr(pytest.mark, "my-1")
def test_a():
pass

@getattr(pytest.mark, "my-2")
def test_b():
pass

def test_c():
pass

@pytest.mark.my2
def test_d():
pass
"""
)
result = test_path.runpytest("-v", "--order-marker-prefix=my")
result.assert_outcomes(passed=4)
result.stdout.fnmatch_lines(
[
"test_marker.py::test_d PASSED",
"test_marker.py::test_c PASSED",
"test_marker.py::test_b PASSED",
"test_marker.py::test_a PASSED",
]
)


@pytest.mark.parametrize(
"suffix, expected",
[
("_neg1", ["test_c", "test_b", "test_a"]),
("neg1", ["test_c", "test_b", "test_a"]),
("_neg", ["test_c", "test_a", "test_b"]),
("neg-1", ["test_c", "test_a", "test_b"]),
("_negative1", ["test_c", "test_a", "test_b"]),
],
)
def test_marker_prefix_supports_neg_placeholder(test_path, suffix, expected):
test_path.makeini(
f"""
[pytest]
console_output_style = classic
markers =
my{suffix}: custom order
my2: custom order
"""
)
test_path.makepyfile(
test_marker=f"""
import pytest

@getattr(pytest.mark, "my{suffix}")
def test_a():
pass

def test_b():
pass

@pytest.mark.my2
def test_c():
pass
"""
)
result = test_path.runpytest("-v", "--order-marker-prefix=my")
result.assert_outcomes(passed=3)
result.stdout.fnmatch_lines([f"test_marker.py::{name} PASSED" for name in expected])


@pytest.mark.parametrize("suffix", ["", "0", "11", "1special", "-1", "_neg1"])
def test_explicit_order_overrides_marker_prefix(test_path, suffix):
test_path.makeini(
f"""
[pytest]
console_output_style = classic
markers =
my{suffix}: custom order
my2: custom order
"""
)
test_path.makepyfile(
test_marker=f"""
import pytest

@pytest.mark.order(3)
@getattr(pytest.mark, "my{suffix}")
def test_a():
pass

@pytest.mark.my2
def test_b():
pass
"""
)
result = test_path.runpytest("-v", "--order-marker-prefix=my")
result.assert_outcomes(passed=2)
result.stdout.fnmatch_lines(
["test_marker.py::test_b PASSED", "test_marker.py::test_a PASSED"]
)
Loading