pytest-split 0.10.0__tar.gz → 0.11.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pytest_split-0.10.0 → pytest_split-0.11.0}/PKG-INFO +8 -8
- {pytest_split-0.10.0 → pytest_split-0.11.0}/README.md +1 -1
- {pytest_split-0.10.0 → pytest_split-0.11.0}/pyproject.toml +7 -8
- {pytest_split-0.10.0 → pytest_split-0.11.0}/src/pytest_split/algorithms.py +21 -23
- {pytest_split-0.10.0 → pytest_split-0.11.0}/src/pytest_split/cli.py +1 -5
- {pytest_split-0.10.0 → pytest_split-0.11.0}/src/pytest_split/ipynb_compatibility.py +2 -4
- {pytest_split-0.10.0 → pytest_split-0.11.0}/src/pytest_split/plugin.py +3 -5
- {pytest_split-0.10.0 → pytest_split-0.11.0}/LICENSE +0 -0
- {pytest_split-0.10.0 → pytest_split-0.11.0}/src/pytest_split/__init__.py +0 -0
- {pytest_split-0.10.0 → pytest_split-0.11.0}/src/pytest_split/py.typed +0 -0
|
@@ -1,29 +1,29 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: pytest-split
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.11.0
|
|
4
4
|
Summary: Pytest plugin which splits the test suite to equally sized sub suites based on test execution time.
|
|
5
|
-
Home-page: https://jerry-git.github.io/pytest-split
|
|
6
5
|
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
7
|
Keywords: pytest,plugin,split,tests
|
|
8
8
|
Author: Jerry Pussinen
|
|
9
9
|
Author-email: jerry.pussinen@gmail.com
|
|
10
|
-
Requires-Python: >=3.
|
|
10
|
+
Requires-Python: >=3.10,<4.0
|
|
11
11
|
Classifier: Development Status :: 4 - Beta
|
|
12
12
|
Classifier: Intended Audience :: Developers
|
|
13
13
|
Classifier: License :: OSI Approved :: MIT License
|
|
14
14
|
Classifier: Operating System :: OS Independent
|
|
15
15
|
Classifier: Programming Language :: Python
|
|
16
16
|
Classifier: Programming Language :: Python :: 3
|
|
17
|
-
Classifier: Programming Language :: Python :: 3.9
|
|
18
17
|
Classifier: Programming Language :: Python :: 3.10
|
|
19
18
|
Classifier: Programming Language :: Python :: 3.11
|
|
20
19
|
Classifier: Programming Language :: Python :: 3.12
|
|
21
20
|
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
-
Classifier: Programming Language :: Python :: 3.
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
22
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
23
|
Classifier: Typing :: Typed
|
|
25
|
-
Requires-Dist: pytest (>=5,<
|
|
24
|
+
Requires-Dist: pytest (>=5,<10)
|
|
26
25
|
Project-URL: Documentation, https://jerry-git.github.io/pytest-split
|
|
26
|
+
Project-URL: Homepage, https://jerry-git.github.io/pytest-split
|
|
27
27
|
Project-URL: Repository, https://github.com/jerry-git/pytest-split
|
|
28
28
|
Description-Content-Type: text/markdown
|
|
29
29
|
|
|
@@ -125,7 +125,7 @@ The `least_duration` algorithm walks the list of tests and assigns each test to
|
|
|
125
125
|
* Clone this repository
|
|
126
126
|
* Requirements:
|
|
127
127
|
* [Poetry](https://python-poetry.org/)
|
|
128
|
-
* Python 3.
|
|
128
|
+
* Python 3.10+
|
|
129
129
|
* Create a virtual environment and install the dependencies
|
|
130
130
|
|
|
131
131
|
```sh
|
|
@@ -96,7 +96,7 @@ The `least_duration` algorithm walks the list of tests and assigns each test to
|
|
|
96
96
|
* Clone this repository
|
|
97
97
|
* Requirements:
|
|
98
98
|
* [Poetry](https://python-poetry.org/)
|
|
99
|
-
* Python 3.
|
|
99
|
+
* Python 3.10+
|
|
100
100
|
* Create a virtual environment and install the dependencies
|
|
101
101
|
|
|
102
102
|
```sh
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "pytest-split"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.11.0"
|
|
4
4
|
description = "Pytest plugin which splits the test suite to equally sized sub suites based on test execution time."
|
|
5
5
|
authors = [
|
|
6
6
|
"Jerry Pussinen <jerry.pussinen@gmail.com>",
|
|
@@ -18,12 +18,11 @@ classifiers = [
|
|
|
18
18
|
"Operating System :: OS Independent",
|
|
19
19
|
"Programming Language :: Python",
|
|
20
20
|
"Programming Language :: Python :: 3",
|
|
21
|
-
"Programming Language :: Python :: 3.8",
|
|
22
|
-
"Programming Language :: Python :: 3.9",
|
|
23
21
|
"Programming Language :: Python :: 3.10",
|
|
24
22
|
"Programming Language :: Python :: 3.11",
|
|
25
23
|
"Programming Language :: Python :: 3.12",
|
|
26
24
|
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Programming Language :: Python :: 3.14",
|
|
27
26
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
28
27
|
"Typing :: Typed",
|
|
29
28
|
]
|
|
@@ -33,11 +32,11 @@ packages = [{ include = 'pytest_split', from = 'src' }]
|
|
|
33
32
|
|
|
34
33
|
|
|
35
34
|
[tool.poetry.dependencies]
|
|
36
|
-
python = ">=3.
|
|
37
|
-
pytest = "^5 | ^6 | ^7 | ^8"
|
|
35
|
+
python = ">=3.10, <4.0"
|
|
36
|
+
pytest = "^5 | ^6 | ^7 | ^8 | ^9"
|
|
38
37
|
|
|
39
38
|
|
|
40
|
-
[tool.poetry.dev
|
|
39
|
+
[tool.poetry.group.dev.dependencies]
|
|
41
40
|
importlib-metadata = "==4.11.*"
|
|
42
41
|
mkdocstrings = {version = ">=0.18", extras = ["python"]}
|
|
43
42
|
mkdocs-material = "*"
|
|
@@ -61,7 +60,7 @@ slowest-tests = "pytest_split.cli:list_slowest_tests"
|
|
|
61
60
|
pytest-split = "pytest_split.plugin"
|
|
62
61
|
|
|
63
62
|
[tool.black]
|
|
64
|
-
target-version = ["
|
|
63
|
+
target-version = ["py310", "py311", "py312", "py313", "py314"]
|
|
65
64
|
include = '\.pyi?$'
|
|
66
65
|
|
|
67
66
|
[tool.pytest.ini_options]
|
|
@@ -93,7 +92,7 @@ disallow_untyped_calls = false
|
|
|
93
92
|
|
|
94
93
|
|
|
95
94
|
[tool.ruff]
|
|
96
|
-
target-version = "
|
|
95
|
+
target-version = "py310" # The lowest supported version
|
|
97
96
|
|
|
98
97
|
[tool.ruff.lint]
|
|
99
98
|
# By default, enable all the lint rules.
|
|
@@ -5,14 +5,12 @@ from operator import itemgetter
|
|
|
5
5
|
from typing import TYPE_CHECKING, NamedTuple
|
|
6
6
|
|
|
7
7
|
if TYPE_CHECKING:
|
|
8
|
-
from typing import Dict, List, Tuple
|
|
9
|
-
|
|
10
8
|
from _pytest import nodes
|
|
11
9
|
|
|
12
10
|
|
|
13
11
|
class TestGroup(NamedTuple):
|
|
14
|
-
selected: "
|
|
15
|
-
deselected: "
|
|
12
|
+
selected: "list[nodes.Item]"
|
|
13
|
+
deselected: "list[nodes.Item]"
|
|
16
14
|
duration: float
|
|
17
15
|
|
|
18
16
|
|
|
@@ -21,8 +19,8 @@ class AlgorithmBase(ABC):
|
|
|
21
19
|
|
|
22
20
|
@abstractmethod
|
|
23
21
|
def __call__(
|
|
24
|
-
self, splits: int, items: "
|
|
25
|
-
) -> "
|
|
22
|
+
self, splits: int, items: "list[nodes.Item]", durations: "dict[str, float]"
|
|
23
|
+
) -> "list[TestGroup]":
|
|
26
24
|
pass
|
|
27
25
|
|
|
28
26
|
def __hash__(self) -> int:
|
|
@@ -52,8 +50,8 @@ class LeastDurationAlgorithm(AlgorithmBase):
|
|
|
52
50
|
"""
|
|
53
51
|
|
|
54
52
|
def __call__(
|
|
55
|
-
self, splits: int, items: "
|
|
56
|
-
) -> "
|
|
53
|
+
self, splits: int, items: "list[nodes.Item]", durations: "dict[str, float]"
|
|
54
|
+
) -> "list[TestGroup]":
|
|
57
55
|
items_with_durations = _get_items_with_durations(items, durations)
|
|
58
56
|
|
|
59
57
|
# add index of item in list
|
|
@@ -71,12 +69,12 @@ class LeastDurationAlgorithm(AlgorithmBase):
|
|
|
71
69
|
items_with_durations_indexed, key=lambda tup: tup[1], reverse=True
|
|
72
70
|
)
|
|
73
71
|
|
|
74
|
-
selected:
|
|
75
|
-
deselected:
|
|
76
|
-
duration:
|
|
72
|
+
selected: list[list[tuple[nodes.Item, int]]] = [[] for _ in range(splits)]
|
|
73
|
+
deselected: list[list[nodes.Item]] = [[] for _ in range(splits)]
|
|
74
|
+
duration: list[float] = [0 for _ in range(splits)]
|
|
77
75
|
|
|
78
76
|
# create a heap of the form (summed_durations, group_index)
|
|
79
|
-
heap:
|
|
77
|
+
heap: list[tuple[float, int]] = [(0, i) for i in range(splits)]
|
|
80
78
|
heapq.heapify(heap)
|
|
81
79
|
for item, item_duration, original_index in sorted_items_with_durations:
|
|
82
80
|
# get group with smallest sum
|
|
@@ -122,14 +120,14 @@ class DurationBasedChunksAlgorithm(AlgorithmBase):
|
|
|
122
120
|
"""
|
|
123
121
|
|
|
124
122
|
def __call__(
|
|
125
|
-
self, splits: int, items: "
|
|
126
|
-
) -> "
|
|
123
|
+
self, splits: int, items: "list[nodes.Item]", durations: "dict[str, float]"
|
|
124
|
+
) -> "list[TestGroup]":
|
|
127
125
|
items_with_durations = _get_items_with_durations(items, durations)
|
|
128
126
|
time_per_group = sum(map(itemgetter(1), items_with_durations)) / splits
|
|
129
127
|
|
|
130
|
-
selected:
|
|
131
|
-
deselected:
|
|
132
|
-
duration:
|
|
128
|
+
selected: list[list[nodes.Item]] = [[] for i in range(splits)]
|
|
129
|
+
deselected: list[list[nodes.Item]] = [[] for i in range(splits)]
|
|
130
|
+
duration: list[float] = [0 for i in range(splits)]
|
|
133
131
|
|
|
134
132
|
group_idx = 0
|
|
135
133
|
for item, item_duration in items_with_durations:
|
|
@@ -151,8 +149,8 @@ class DurationBasedChunksAlgorithm(AlgorithmBase):
|
|
|
151
149
|
|
|
152
150
|
|
|
153
151
|
def _get_items_with_durations(
|
|
154
|
-
items: "
|
|
155
|
-
) -> "
|
|
152
|
+
items: "list[nodes.Item]", durations: "dict[str, float]"
|
|
153
|
+
) -> "list[tuple[nodes.Item, float]]":
|
|
156
154
|
durations = _remove_irrelevant_durations(items, durations)
|
|
157
155
|
avg_duration_per_test = _get_avg_duration_per_test(durations)
|
|
158
156
|
items_with_durations = [
|
|
@@ -161,7 +159,7 @@ def _get_items_with_durations(
|
|
|
161
159
|
return items_with_durations
|
|
162
160
|
|
|
163
161
|
|
|
164
|
-
def _get_avg_duration_per_test(durations: "
|
|
162
|
+
def _get_avg_duration_per_test(durations: "dict[str, float]") -> float:
|
|
165
163
|
if durations:
|
|
166
164
|
avg_duration_per_test = sum(durations.values()) / len(durations)
|
|
167
165
|
else:
|
|
@@ -171,8 +169,8 @@ def _get_avg_duration_per_test(durations: "Dict[str, float]") -> float:
|
|
|
171
169
|
|
|
172
170
|
|
|
173
171
|
def _remove_irrelevant_durations(
|
|
174
|
-
items: "
|
|
175
|
-
) -> "
|
|
172
|
+
items: "list[nodes.Item]", durations: "dict[str, float]"
|
|
173
|
+
) -> "dict[str, float]":
|
|
176
174
|
# Filtering down durations to relevant ones ensures the avg isn't skewed by irrelevant data
|
|
177
175
|
test_ids = [item.nodeid for item in items]
|
|
178
176
|
durations = {name: durations[name] for name in test_ids if name in durations}
|
|
@@ -184,5 +182,5 @@ class Algorithms(enum.Enum):
|
|
|
184
182
|
least_duration = LeastDurationAlgorithm()
|
|
185
183
|
|
|
186
184
|
@staticmethod
|
|
187
|
-
def names() -> "
|
|
185
|
+
def names() -> "list[str]":
|
|
188
186
|
return [x.name for x in Algorithms]
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
import argparse
|
|
2
2
|
import json
|
|
3
|
-
from typing import TYPE_CHECKING
|
|
4
|
-
|
|
5
|
-
if TYPE_CHECKING:
|
|
6
|
-
from typing import Dict
|
|
7
3
|
|
|
8
4
|
|
|
9
5
|
def list_slowest_tests() -> None:
|
|
@@ -28,7 +24,7 @@ def list_slowest_tests() -> None:
|
|
|
28
24
|
return _list_slowest_tests(json.load(args.durations_path), args.count)
|
|
29
25
|
|
|
30
26
|
|
|
31
|
-
def _list_slowest_tests(durations: "
|
|
27
|
+
def _list_slowest_tests(durations: "dict[str, float]", count: int) -> None:
|
|
32
28
|
slowest_tests = tuple(
|
|
33
29
|
sorted(durations.items(), key=lambda item: item[1], reverse=True)
|
|
34
30
|
)[:count]
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
from typing import TYPE_CHECKING
|
|
2
2
|
|
|
3
3
|
if TYPE_CHECKING:
|
|
4
|
-
from typing import List
|
|
5
|
-
|
|
6
4
|
from pytest_split.algorithms import TestGroup
|
|
7
5
|
|
|
8
6
|
|
|
@@ -45,8 +43,8 @@ def ensure_ipynb_compatibility(group: "TestGroup", items: list) -> None: # type
|
|
|
45
43
|
|
|
46
44
|
|
|
47
45
|
def _find_sibiling_ipynb_cells(
|
|
48
|
-
ipynb_node_id: str, item_node_ids: "
|
|
49
|
-
) -> "
|
|
46
|
+
ipynb_node_id: str, item_node_ids: "list[str]"
|
|
47
|
+
) -> "list[str]":
|
|
50
48
|
"""
|
|
51
49
|
Returns all sibling IPyNb cells given an IPyNb cell nodeid.
|
|
52
50
|
"""
|
|
@@ -10,8 +10,6 @@ from pytest_split import algorithms
|
|
|
10
10
|
from pytest_split.ipynb_compatibility import ensure_ipynb_compatibility
|
|
11
11
|
|
|
12
12
|
if TYPE_CHECKING:
|
|
13
|
-
from typing import Dict, List, Optional, Union
|
|
14
|
-
|
|
15
13
|
from _pytest import nodes
|
|
16
14
|
from _pytest.config import Config
|
|
17
15
|
from _pytest.config.argparsing import Parser
|
|
@@ -77,7 +75,7 @@ def pytest_addoption(parser: "Parser") -> None:
|
|
|
77
75
|
|
|
78
76
|
|
|
79
77
|
@pytest.hookimpl(tryfirst=True)
|
|
80
|
-
def pytest_cmdline_main(config: "Config") -> "
|
|
78
|
+
def pytest_cmdline_main(config: "Config") -> "int | ExitCode | None":
|
|
81
79
|
"""
|
|
82
80
|
Validate options.
|
|
83
81
|
"""
|
|
@@ -153,7 +151,7 @@ class PytestSplitPlugin(Base):
|
|
|
153
151
|
|
|
154
152
|
@hookimpl(trylast=True)
|
|
155
153
|
def pytest_collection_modifyitems(
|
|
156
|
-
self, config: "Config", items: "
|
|
154
|
+
self, config: "Config", items: "list[nodes.Item]"
|
|
157
155
|
) -> None:
|
|
158
156
|
"""
|
|
159
157
|
Collect and select the tests we want to run, and deselect the rest.
|
|
@@ -193,7 +191,7 @@ class PytestSplitCachePlugin(Base):
|
|
|
193
191
|
https://github.com/pytest-dev/pytest/blob/main/src/_pytest/main.py#L308
|
|
194
192
|
"""
|
|
195
193
|
terminal_reporter = self.config.pluginmanager.get_plugin("terminalreporter")
|
|
196
|
-
test_durations:
|
|
194
|
+
test_durations: dict[str, float] = {}
|
|
197
195
|
|
|
198
196
|
for test_reports in terminal_reporter.stats.values(): # type: ignore[union-attr]
|
|
199
197
|
for test_report in test_reports:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|