Skip to content

Commit a3c9bc0

Browse files
committed
Remove support fo Python 2.7 and 3.5
- add type hints
1 parent d3cf3d2 commit a3c9bc0

File tree

13 files changed

+384
-322
lines changed

13 files changed

+384
-322
lines changed

.github/workflows/pythontests.yml

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,10 +40,8 @@ jobs:
4040
fail-fast: false
4141
matrix:
4242
os: [ubuntu-latest, windows-latest]
43-
python-version: [2.7, 3.5, 3.6, 3.7, 3.8, 3.9, 3.10-dev, pypy2, pypy3]
43+
python-version: [3.6, 3.7, 3.8, 3.9, 3.10-dev, pypy3]
4444
exclude:
45-
- os: windows-latest
46-
python-version: pypy2
4745
- os: windows-latest
4846
python-version: pypy3
4947
env:
@@ -70,7 +68,7 @@ jobs:
7068
python -m pip install pytest pytest-cov tox
7169
- name: Run tests
7270
run: |
73-
TOX_PYTHON_VERSION=$(if [ ${{ matrix.python-version }} = pypy2 ]; then echo "pypy2"; elif [ ${{ matrix.python-version }} = pypy3 ]; then echo "pypy3"; else echo py${{ matrix.python-version }} | tr -d .-; fi)
71+
TOX_PYTHON_VERSION=$(if [ ${{ matrix.python-version }} = pypy3 ]; then echo "pypy3"; else echo py${{ matrix.python-version }} | tr -d .-; fi)
7472
COV_CMD=$(if [ ${{ matrix.python-version }} = 3.8 ]; then echo "--cov=./pytest_order/ --cov-report=xml"; else echo ; fi) tox -e $(tox -l | grep $TOX_PYTHON_VERSION | paste -sd "," -)
7573
- name: Upload coverage to Codecov
7674
uses: codecov/codecov-action@v1

CHANGELOG.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@
33
## Unreleased
44

55
_Breaking changes in next major version_:
6-
Support for Python 2.7 and Python 3.5 will be removed with the release of
7-
version 1.0.0, which is planned for the near future.
86
With that version the notation of relative markers in other modules is also
97
planned to change - instead of using the dot notation, the standard pytest
108
nodeid will be used.
119

10+
### Breaking changes
11+
- removed support for Python 2.7 and 3.5
12+
1213
## [Version 0.11.0](https://pypi.org/project/pytest-order/0.11.0/) (2021-04-11)
1314
Adds support for multiple relative markers for the same test.
1415

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ test shall be run relative to the other tests.
1212
some additional features - see [below](#comparison-with-pytest_ordering) for
1313
details.
1414

15-
`pytest-order` works with Python 2.7 and 3.5 - 3.10, with pytest
15+
`pytest-order` works with Python 3.6 - 3.10, with pytest
1616
versions >= 3.7.0, and runs on Linux, macOS and Windows.
1717

1818
Documentation

docs/source/index.rst

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,9 +38,8 @@ ordering, all configuration options) that are not available in
3838

3939
Supported Python and pytest versions
4040
------------------------------------
41-
``pytest-order`` supports python 2.7, 3.5 - 3.10, and pypy/pypy3, and is
42-
compatible with pytest 3.7.0 or newer. Note that support for Python 2 will
43-
be removed in one of the next versions.
41+
``pytest-order`` supports python 3.6 - 3.10 and pypy3, and is
42+
compatible with pytest 3.7.0 or newer.
4443

4544
All supported combinations of Python and pytest versions are tested in
4645
the CI builds. The plugin shall work under Linux, MacOs and Windows.

pytest_order/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "0.11.0"
1+
__version__ = "1.0.dev0"

pytest_order/item.py

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
import sys
2+
from typing import List, Optional, Union, Dict, Tuple
3+
4+
from _pytest.python import Function
5+
6+
from pytest_order.settings import Scope, Settings
7+
8+
9+
class Item:
10+
"""Represents a single test item."""
11+
12+
def __init__(self, item: Function) -> None:
13+
self.item: Function = item
14+
self.nr_rel_items: int = 0
15+
self.order: Optional[int] = None
16+
self._node_id: Optional[str] = None
17+
18+
def inc_rel_marks(self) -> None:
19+
if self.order is None:
20+
self.nr_rel_items += 1
21+
22+
def dec_rel_marks(self) -> None:
23+
if self.order is None:
24+
self.nr_rel_items -= 1
25+
26+
@property
27+
def module_path(self) -> str:
28+
return self.item.nodeid[:self.node_id.index("::")]
29+
30+
def parent_path(self, level) -> str:
31+
return "/".join(self.module_path.split("/")[:level])
32+
33+
@property
34+
def node_id(self) -> str:
35+
if self._node_id is None:
36+
# in pytest < 4 the nodeid has an unwanted ::() part
37+
self._node_id = self.item.nodeid.replace("::()", "")
38+
return self._node_id
39+
40+
@property
41+
def label(self) -> str:
42+
return self.node_id.replace(".py::", ".").replace("/", ".")
43+
44+
45+
class ItemList:
46+
"""Handles a group of items with the same scope."""
47+
48+
def __init__(self, items: List[Item],
49+
settings: Settings, scope: Scope,
50+
rel_marks: List["RelativeMark"],
51+
dep_marks: List["RelativeMark"]) -> None:
52+
self.items = items
53+
self.settings = settings
54+
self.scope = scope
55+
self.start_items: List[Tuple[int, List[Item]]] = []
56+
self.end_items: List[Tuple[int, List[Item]]] = []
57+
self.unordered_items: List[Item] = []
58+
self._start_items: Dict[int, List[Item]] = {}
59+
self._end_items: Dict[int, List[Item]] = {}
60+
self.all_rel_marks = rel_marks
61+
self.all_dep_marks = dep_marks
62+
self.rel_marks = filter_marks(rel_marks, items)
63+
self.dep_marks = filter_marks(dep_marks, items)
64+
65+
def collect_markers(self, item):
66+
if item.order is not None:
67+
self.handle_order_mark(item)
68+
if item.nr_rel_items or item.order is None:
69+
self.unordered_items.append(item)
70+
71+
def handle_order_mark(self, item):
72+
if item.order < 0:
73+
self._end_items.setdefault(item.order, []).append(item)
74+
else:
75+
self._start_items.setdefault(item.order, []).append(item)
76+
77+
def sort_numbered_items(self) -> List[Item]:
78+
self.start_items = sorted(self._start_items.items())
79+
self.end_items = sorted(self._end_items.items())
80+
sorted_list = []
81+
index = 0
82+
for entries in self.start_items:
83+
if self.settings.sparse_ordering:
84+
while entries[0] > index and self.unordered_items:
85+
sorted_list.append(self.unordered_items.pop(0))
86+
index += 1
87+
sorted_list += entries[1]
88+
index += len(entries[1])
89+
mid_index = len(sorted_list)
90+
index = -1
91+
for entries in reversed(self.end_items):
92+
if self.settings.sparse_ordering:
93+
while entries[0] < index and self.unordered_items:
94+
sorted_list.insert(mid_index, self.unordered_items.pop())
95+
index -= 1
96+
sorted_list[mid_index:mid_index] = entries[1]
97+
index -= len(entries[1])
98+
sorted_list[mid_index:mid_index] = self.unordered_items
99+
return sorted_list
100+
101+
def print_unhandled_items(self):
102+
msg = " ".join([mark.item.label for mark in self.rel_marks] +
103+
[mark.item.label for mark in self.dep_marks])
104+
if msg:
105+
sys.stdout.write(
106+
"\nWARNING: cannot execute test relative to others: ")
107+
sys.stdout.write(msg)
108+
sys.stdout.write("- ignoring the marker.\n")
109+
sys.stdout.flush()
110+
111+
def number_of_rel_groups(self):
112+
return len(self.rel_marks) + len(self.dep_marks)
113+
114+
def handle_rel_marks(self, sorted_list):
115+
self.handle_relative_marks(self.rel_marks, sorted_list,
116+
self.all_rel_marks)
117+
118+
def handle_dep_marks(self, sorted_list):
119+
self.handle_relative_marks(self.dep_marks, sorted_list,
120+
self.all_dep_marks)
121+
122+
@staticmethod
123+
def handle_relative_marks(marks, sorted_list, all_marks):
124+
for mark in reversed(marks):
125+
if move_item(mark, sorted_list):
126+
marks.remove(mark)
127+
all_marks.remove(mark)
128+
129+
def group_order(self):
130+
if self.start_items:
131+
return self.start_items[0][0]
132+
if self.end_items:
133+
return self.end_items[-1][0]
134+
135+
136+
class ItemGroup:
137+
"""Holds a group of sorted items with the same group order scope.
138+
Used for sorting groups similar to Item for sorting items.
139+
"""
140+
141+
def __init__(self, items=None, order=None):
142+
self.items = items or []
143+
self.order = order
144+
self.nr_rel_items = 0
145+
146+
def inc_rel_marks(self) -> None:
147+
if self.order is None:
148+
self.nr_rel_items += 1
149+
150+
def dec_rel_marks(self) -> None:
151+
if self.order is None:
152+
self.nr_rel_items -= 1
153+
154+
def extend(self, groups: List["ItemGroup"], order: Optional[int]) -> None:
155+
for group in groups:
156+
self.items.extend(group.items)
157+
self.order = order
158+
159+
160+
class RelativeMark:
161+
"""Represents a marker for an item or an item group.
162+
Holds two related items or groups and their relationship.
163+
"""
164+
165+
def __init__(self, item: Union[Item, ItemGroup],
166+
item_to_move: Union[Item, ItemGroup],
167+
move_after: bool) -> None:
168+
self.item: Item = item
169+
self.item_to_move: Item = item_to_move
170+
self.move_after: bool = move_after
171+
172+
173+
def filter_marks(
174+
marks: List[RelativeMark],
175+
all_items: List[Item]) -> List[RelativeMark]:
176+
result = []
177+
for mark in marks:
178+
if mark.item in all_items and mark.item_to_move in all_items:
179+
result.append(mark)
180+
else:
181+
mark.item_to_move.dec_rel_marks()
182+
return result
183+
184+
185+
def move_item(mark: RelativeMark,
186+
sorted_items: List[Union[Item, ItemGroup]]) -> bool:
187+
if (mark.item not in sorted_items or
188+
mark.item_to_move not in sorted_items or
189+
mark.item.nr_rel_items):
190+
return False
191+
pos_item = sorted_items.index(mark.item)
192+
pos_item_to_move = sorted_items.index(mark.item_to_move)
193+
if mark.item_to_move.order is not None and mark.item.order is None:
194+
# if the item to be moved has already been ordered numerically,
195+
# and the other item is not ordered, we move that one instead
196+
mark.move_after = not mark.move_after
197+
mark.item, mark.item_to_move = mark.item_to_move, mark.item
198+
pos_item, pos_item_to_move = pos_item_to_move, pos_item
199+
mark.item_to_move.dec_rel_marks()
200+
if mark.move_after:
201+
if pos_item_to_move < pos_item + 1:
202+
del sorted_items[pos_item_to_move]
203+
sorted_items.insert(pos_item, mark.item_to_move)
204+
else:
205+
if pos_item_to_move > pos_item:
206+
del sorted_items[pos_item_to_move]
207+
pos_item -= 1
208+
sorted_items.insert(pos_item + 1, mark.item_to_move)
209+
return True

pytest_order/plugin.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
# -*- coding: utf-8 -*-
2+
from typing import List
23

34
import pytest
5+
from _pytest.config import Config
6+
from _pytest.config.argparsing import Parser
7+
from _pytest.main import Session
8+
from _pytest.python import Function
49

510
from pytest_order.sorter import Sorter
611

712

8-
def pytest_configure(config):
13+
def pytest_configure(config: Config) -> None:
914
"""Register the "order" marker and configure the plugin depending
1015
on the CLI options"""
1116

@@ -37,7 +42,7 @@ def pytest_configure(config):
3742
config.pluginmanager.register(OrderingPlugin(), "orderingplugin")
3843

3944

40-
def pytest_addoption(parser):
45+
def pytest_addoption(parser: Parser) -> None:
4146
"""Set up CLI option for pytest"""
4247
group = parser.getgroup("order")
4348
group.addoption("--indulgent-ordering", action="store_true",
@@ -73,7 +78,7 @@ def pytest_addoption(parser):
7378
"be ordered if needed.")
7479

7580

76-
class OrderingPlugin(object):
81+
class OrderingPlugin:
7782
"""
7883
Plugin implementation
7984
@@ -82,6 +87,7 @@ class OrderingPlugin(object):
8287
"""
8388

8489

85-
def modify_items(session, config, items):
90+
def modify_items(
91+
session: Session, config: Config, items: List[Function]) -> None:
8692
sorter = Sorter(config, items)
8793
items[:] = sorter.sort_items()

pytest_order/settings.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
from _warnings import warn
2+
from enum import Enum
3+
4+
from _pytest.config import Config
5+
6+
7+
class Scope(Enum):
8+
CLASS = 1
9+
MODULE = 2
10+
SESSION = 3
11+
12+
13+
class Settings:
14+
valid_scopes = {
15+
"class": Scope.CLASS,
16+
"module": Scope.MODULE,
17+
"session": Scope.SESSION
18+
}
19+
20+
def __init__(self, config: Config) -> None:
21+
self.sparse_ordering: bool = config.getoption("sparse_ordering")
22+
self.order_dependencies: bool = config.getoption("order_dependencies")
23+
scope: str = config.getoption("order_scope")
24+
if scope in self.valid_scopes:
25+
self.scope: Scope = self.valid_scopes[scope]
26+
else:
27+
if scope is not None:
28+
warn("Unknown order scope '{}', ignoring it. "
29+
"Valid scopes are 'session', 'module' and 'class'."
30+
.format(scope))
31+
self.scope = Scope.SESSION
32+
scope_level: int = config.getoption("order_scope_level") or 0
33+
if scope_level != 0 and self.scope != Scope.SESSION:
34+
warn("order-scope-level cannot be used together with "
35+
"--order-scope={}".format(scope))
36+
scope_level = 0
37+
self.scope_level: int = scope_level
38+
group_scope: str = config.getoption("order_group_scope")
39+
if group_scope in self.valid_scopes:
40+
self.group_scope: Scope = self.valid_scopes[group_scope]
41+
else:
42+
if group_scope is not None:
43+
warn("Unknown order group scope '{}', ignoring it. "
44+
"Valid scopes are 'session', 'module' and 'class'."
45+
.format(group_scope))
46+
self.group_scope = self.scope
47+
if self.group_scope.value > self.scope.value:
48+
warn("Group scope is larger than order scope, ignoring it.")
49+
self.group_scope = self.scope

0 commit comments

Comments
 (0)