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
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v4
- uses: ./.github/actions/python-poetry-env
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
### Changed
- Migrated `poetry.dev-dependencies` to `poetry.group.dev.dependencies`

### Removed
- Support for Python 3.8 and 3.9 (end-of-life)

## [0.10.0] - 2024-10-16
### Added
- Support for Python 3.13.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ The `least_duration` algorithm walks the list of tests and assigns each test to
* Clone this repository
* Requirements:
* [Poetry](https://python-poetry.org/)
* Python 3.8+
* Python 3.10+
* Create a virtual environment and install the dependencies

```sh
Expand Down
65 changes: 4 additions & 61 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 3 additions & 5 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,6 @@ classifiers = [
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
Expand All @@ -34,7 +32,7 @@ packages = [{ include = 'pytest_split', from = 'src' }]


[tool.poetry.dependencies]
python = ">=3.8.1, <4.0"
python = ">=3.10, <4.0"
pytest = "^5 | ^6 | ^7 | ^8 | ^9"


Expand Down Expand Up @@ -62,7 +60,7 @@ slowest-tests = "pytest_split.cli:list_slowest_tests"
pytest-split = "pytest_split.plugin"

[tool.black]
target-version = ["py37", "py38", "py39"]
target-version = ["py310", "py311", "py312", "py313", "py314"]
include = '\.pyi?$'

[tool.pytest.ini_options]
Expand Down Expand Up @@ -94,7 +92,7 @@ disallow_untyped_calls = false


[tool.ruff]
target-version = "py38" # The lowest supported version
target-version = "py310" # The lowest supported version

[tool.ruff.lint]
# By default, enable all the lint rules.
Expand Down
44 changes: 21 additions & 23 deletions src/pytest_split/algorithms.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,12 @@
from typing import TYPE_CHECKING, NamedTuple

if TYPE_CHECKING:
from typing import Dict, List, Tuple

from _pytest import nodes


class TestGroup(NamedTuple):
selected: "List[nodes.Item]"
deselected: "List[nodes.Item]"
selected: "list[nodes.Item]"
deselected: "list[nodes.Item]"
duration: float


Expand All @@ -21,8 +19,8 @@ class AlgorithmBase(ABC):

@abstractmethod
def __call__(
self, splits: int, items: "List[nodes.Item]", durations: "Dict[str, float]"
) -> "List[TestGroup]":
self, splits: int, items: "list[nodes.Item]", durations: "dict[str, float]"
) -> "list[TestGroup]":
pass

def __hash__(self) -> int:
Expand Down Expand Up @@ -52,8 +50,8 @@ class LeastDurationAlgorithm(AlgorithmBase):
"""

def __call__(
self, splits: int, items: "List[nodes.Item]", durations: "Dict[str, float]"
) -> "List[TestGroup]":
self, splits: int, items: "list[nodes.Item]", durations: "dict[str, float]"
) -> "list[TestGroup]":
items_with_durations = _get_items_with_durations(items, durations)

# add index of item in list
Expand All @@ -71,12 +69,12 @@ def __call__(
items_with_durations_indexed, key=lambda tup: tup[1], reverse=True
)

selected: List[List[Tuple[nodes.Item, int]]] = [[] for _ in range(splits)]
deselected: List[List[nodes.Item]] = [[] for _ in range(splits)]
duration: List[float] = [0 for _ in range(splits)]
selected: list[list[tuple[nodes.Item, int]]] = [[] for _ in range(splits)]
deselected: list[list[nodes.Item]] = [[] for _ in range(splits)]
duration: list[float] = [0 for _ in range(splits)]

# create a heap of the form (summed_durations, group_index)
heap: List[Tuple[float, int]] = [(0, i) for i in range(splits)]
heap: list[tuple[float, int]] = [(0, i) for i in range(splits)]
heapq.heapify(heap)
for item, item_duration, original_index in sorted_items_with_durations:
# get group with smallest sum
Expand Down Expand Up @@ -122,14 +120,14 @@ class DurationBasedChunksAlgorithm(AlgorithmBase):
"""

def __call__(
self, splits: int, items: "List[nodes.Item]", durations: "Dict[str, float]"
) -> "List[TestGroup]":
self, splits: int, items: "list[nodes.Item]", durations: "dict[str, float]"
) -> "list[TestGroup]":
items_with_durations = _get_items_with_durations(items, durations)
time_per_group = sum(map(itemgetter(1), items_with_durations)) / splits

selected: List[List[nodes.Item]] = [[] for i in range(splits)]
deselected: List[List[nodes.Item]] = [[] for i in range(splits)]
duration: List[float] = [0 for i in range(splits)]
selected: list[list[nodes.Item]] = [[] for i in range(splits)]
deselected: list[list[nodes.Item]] = [[] for i in range(splits)]
duration: list[float] = [0 for i in range(splits)]

group_idx = 0
for item, item_duration in items_with_durations:
Expand All @@ -151,8 +149,8 @@ def __call__(


def _get_items_with_durations(
items: "List[nodes.Item]", durations: "Dict[str, float]"
) -> "List[Tuple[nodes.Item, float]]":
items: "list[nodes.Item]", durations: "dict[str, float]"
) -> "list[tuple[nodes.Item, float]]":
durations = _remove_irrelevant_durations(items, durations)
avg_duration_per_test = _get_avg_duration_per_test(durations)
items_with_durations = [
Expand All @@ -161,7 +159,7 @@ def _get_items_with_durations(
return items_with_durations


def _get_avg_duration_per_test(durations: "Dict[str, float]") -> float:
def _get_avg_duration_per_test(durations: "dict[str, float]") -> float:
if durations:
avg_duration_per_test = sum(durations.values()) / len(durations)
else:
Expand All @@ -171,8 +169,8 @@ def _get_avg_duration_per_test(durations: "Dict[str, float]") -> float:


def _remove_irrelevant_durations(
items: "List[nodes.Item]", durations: "Dict[str, float]"
) -> "Dict[str, float]":
items: "list[nodes.Item]", durations: "dict[str, float]"
) -> "dict[str, float]":
# Filtering down durations to relevant ones ensures the avg isn't skewed by irrelevant data
test_ids = [item.nodeid for item in items]
durations = {name: durations[name] for name in test_ids if name in durations}
Expand All @@ -184,5 +182,5 @@ class Algorithms(enum.Enum):
least_duration = LeastDurationAlgorithm()

@staticmethod
def names() -> "List[str]":
def names() -> "list[str]":
return [x.name for x in Algorithms]
6 changes: 1 addition & 5 deletions src/pytest_split/cli.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import argparse
import json
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from typing import Dict


def list_slowest_tests() -> None:
Expand All @@ -28,7 +24,7 @@ def list_slowest_tests() -> None:
return _list_slowest_tests(json.load(args.durations_path), args.count)


def _list_slowest_tests(durations: "Dict[str, float]", count: int) -> None:
def _list_slowest_tests(durations: "dict[str, float]", count: int) -> None:
slowest_tests = tuple(
sorted(durations.items(), key=lambda item: item[1], reverse=True)
)[:count]
Expand Down
6 changes: 2 additions & 4 deletions src/pytest_split/ipynb_compatibility.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from typing import List

from pytest_split.algorithms import TestGroup


Expand Down Expand Up @@ -45,8 +43,8 @@ def ensure_ipynb_compatibility(group: "TestGroup", items: list) -> None: # type


def _find_sibiling_ipynb_cells(
ipynb_node_id: str, item_node_ids: "List[str]"
) -> "List[str]":
ipynb_node_id: str, item_node_ids: "list[str]"
) -> "list[str]":
"""
Returns all sibling IPyNb cells given an IPyNb cell nodeid.
"""
Expand Down
Loading