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
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@
- added official support for Python 3.15

### Fixes
- Class-scoped ordering handles parameter IDs containing `::`, such as IPv6 addresses.
- relative ordering resolves whole-test labels when parameter IDs contain brackets or node separators
(see [#272](https://github.com/pytest-dev/pytest-order/issues/272))
- class-scoped ordering handles parameter IDs containing `::`, such as IPv6 addresses
- `--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)
Expand Down
9 changes: 7 additions & 2 deletions src/pytest_order/item.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,15 @@ def parent_path(self, level) -> str:
@property
def node_id(self) -> str:
if self._node_id is None:
# in pytest < 4 the nodeid has an unwanted ::() part
self._node_id = self.item.nodeid.replace("::()", "")
self._node_id = self.item.nodeid
return self._node_id

@property
def node_id_without_parameters(self) -> str:
if not isinstance(self.item, Function) or self.item.originalname is None:
return self.node_id
return self.node_id.removesuffix(self.item.name) + self.item.originalname


class ItemList:
"""Handles a group of items with the same scope."""
Expand Down
17 changes: 8 additions & 9 deletions src/pytest_order/sorter.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,7 @@ def __init__(self, config: Config, items: list[Function]) -> None:
self.node_id_last: dict[str, list[str]] = {}
for item in self.items:
self.node_ids[item.node_id] = item
last_part = item.node_id.rpartition("::")[2]
if "[" in last_part:
last_part = last_part.rpartition("[")[0]
last_part = item.node_id_without_parameters.rpartition("::")[2]
# save last nodeid component to avoid to iterate over all
# items for each label
self.node_id_last.setdefault(last_part, []).append(item.node_id)
Expand Down Expand Up @@ -190,10 +188,10 @@ def items_from_label(self, label: str, item: Item, is_cls_mark: bool) -> list[It
for node_id in node_ids:
if node_id.endswith(label):
id_start = node_id[:-label_len]
elif node_id.endswith("]") and node_id.rpartition("[")[0].endswith(
label
):
id_start = node_id.rpartition("[")[0][:-label_len]
elif self.node_ids[node_id].node_id_without_parameters.endswith(label):
id_start = self.node_ids[node_id].node_id_without_parameters[
:-label_len
]
else:
continue
if is_cls_mark and id_start.count("::") == 2:
Expand All @@ -207,13 +205,14 @@ def items_from_class_label(self, label: str, item: Item) -> list[Item]:
items = []
item_id = item.node_id
label_len = len(label)
for node_id in self.node_ids:
for candidate in self.node_ids.values():
node_id = candidate.node_id_without_parameters
if node_id.count("::") == 2:
cls_index = node_id.rindex("::")
if node_id[:cls_index].endswith(label):
id_start = node_id[: cls_index - label_len]
if item_id.startswith(id_start):
items.append(self.node_ids[node_id])
items.append(candidate)
return items

def handle_before_or_after_mark(
Expand Down
57 changes: 57 additions & 0 deletions tests/test_relative_ordering.py
Original file line number Diff line number Diff line change
Expand Up @@ -644,3 +644,60 @@ def test_3():
"test_2[ddddd]",
"test_1",
]


@pytest.mark.parametrize("in_class", [False, True])
def test_relative_labels_ignore_parameter_id_syntax(item_names_for, in_class):
tests = dedent("""
import pytest

@pytest.mark.order(after="test_source")
def test_after():
pass

@pytest.mark.parametrize("value", [1, 2, 3, 4, 5],
ids=["outer[inner]", "::1", "path/with[::parts]", "::()", "outer[::()]"])
def test_source(value):
pass

@pytest.mark.order(before="test_source")
def test_before():
pass
""")
prefix = ""
if in_class:
methods = tests.replace("():", "(self):").replace("(value):", "(self, value):")
tests = "import pytest\nclass TestGroup:\n" + "\n".join(
" " + line for line in methods.splitlines()
)
prefix = "TestGroup::"
assert item_names_for(tests) == [
prefix + "test_before",
prefix + "test_source[outer[inner]]",
prefix + "test_source[::1]",
prefix + "test_source[path/with[::parts]]",
prefix + "test_source[::()]",
prefix + "test_source[outer[::()]]",
prefix + "test_after",
]


def test_class_label_ignores_parameter_id_syntax(item_names_for):
tests = """
import pytest

@pytest.mark.order(after="TestSource")
class TestAfter:
def test_after(self):
pass

class TestSource:
@pytest.mark.parametrize("value", [1, 2], ids=["::1", "nested[::id]"])
def test_source(self, value):
pass
"""
assert item_names_for(tests) == [
"TestSource::test_source[::1]",
"TestSource::test_source[nested[::id]]",
"TestAfter::test_after",
]
Loading