Skip to content

Commit 30a55a0

Browse files
committed
Replace black and flake8 with ruff and pre-commit
Add Python 3.13 support
1 parent 6a83767 commit 30a55a0

File tree

10 files changed

+158
-139
lines changed

10 files changed

+158
-139
lines changed

.coveragerc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
[report]
2+
exclude_lines =
3+
pragma: no cover
4+
if TYPE_CHECKING:

.github/workflows/test.yml

Lines changed: 19 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -5,61 +5,44 @@ on:
55
- pull_request
66

77
jobs:
8-
black:
8+
lint:
99
runs-on: ubuntu-latest
1010
steps:
11-
- uses: actions/checkout@v4
12-
- name: Set up Python
13-
uses: actions/setup-python@v5
14-
with:
15-
python-version: '3.x'
16-
- name: Install black
17-
run: python -m pip install black
18-
- name: black
19-
run: black --check --diff .
20-
21-
flake8:
22-
runs-on: ubuntu-latest
23-
steps:
24-
- uses: actions/checkout@v4
25-
- name: Set up Python
26-
uses: actions/setup-python@v5
27-
with:
28-
python-version: '3.x'
29-
- name: Install flake8
30-
run: python -m pip install flake8
31-
- name: flake8
32-
run: flake8 .
33-
34-
mypy:
35-
runs-on: ubuntu-latest
36-
steps:
37-
- uses: actions/checkout@v4
38-
- name: Set up Python
39-
uses: actions/setup-python@v5
40-
with:
41-
python-version: '3.x'
42-
- name: Install dependencies
43-
run: python -m pip install mypy types-setuptools .
44-
- name: mypy
45-
run: mypy .
11+
- uses: actions/checkout@v4
12+
- name: Set up Python
13+
uses: actions/setup-python@v5
14+
with:
15+
python-version: '3.x'
16+
- name: Install pre-commit
17+
run: python -m pip install pre-commit
18+
- name: Run pre-commit
19+
run: pre-commit run --all-files
4620

4721
test:
4822
strategy:
4923
fail-fast: false
5024
matrix:
5125
python-version:
26+
- "3.13"
5227
- "3.12"
5328
- "3.11"
5429
- "3.10"
5530
- "3.9"
5631
- "3.8"
32+
- "pypy3.8"
5733
- "pypy3.9"
34+
- "pypy3.10"
35+
- "pypy3.11"
5836
pytest-version:
5937
- "pytest<8"
6038
- "pytest<9"
6139
- "pytest"
6240
- "git+https://github.com/pytest-dev/pytest.git@main"
41+
exclude:
42+
- python-version: "3.8"
43+
pytest-version: "git+https://github.com/pytest-dev/pytest.git@main"
44+
- python-version: "pypy3.8"
45+
pytest-version: "git+https://github.com/pytest-dev/pytest.git@main"
6346
runs-on: ubuntu-latest
6447
steps:
6548
- uses: actions/checkout@v4

.pre-commit-config.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
repos:
2+
- repo: https://github.com/astral-sh/ruff-pre-commit
3+
rev: v0.11.6
4+
hooks:
5+
- id: ruff-format
6+
args: [., --check]
7+
- id: ruff-format
8+
args: [.]
9+
- id: ruff
10+
args: [., --fix, --exit-non-zero-on-fix, --show-fixes]
11+
12+
- repo: https://github.com/pre-commit/mirrors-mypy
13+
rev: v1.15.0
14+
hooks:
15+
- id: mypy

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
# Changelog
22

33

4+
## [0.7.0]
5+
- Add Python 3.13 support
6+
- Add deep unordered for nested data structures (#17)
7+
- Switch to ruff for linting and formatting
8+
49
## [0.6.1] - 2024-07-05
510
- Fix matching with `mock.ANY` (#16)
611

pyproject.toml

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,36 @@
1-
[tool.black]
1+
[tool.ruff]
22
line-length = 100
3-
target-version = ['py37']
3+
target-version = "py38"
4+
exclude =[
5+
"__pycache__",
6+
".git",
7+
".venv*/*",
8+
"venv*/*",
9+
"*/site-packages/*",
10+
]
11+
12+
[tool.ruff.format]
13+
indent-style = "space"
14+
15+
[tool.ruff.lint]
16+
select = ["ALL"]
17+
ignore = [
18+
"ANN401", # Dynamically typed expressions (typing.Any) are disallowed
19+
"COM812", # missing-trailing-comma: May conflict with the formatter
20+
"D", # pydocstyle
21+
"Q", # flake8-quotes (may conflict with the formatter)
22+
"PLR2004", # Magic value used in comparison, consider replacing with a constant variable
23+
"S101", # Use of `assert` detected
24+
"SIM201", # negate-equal-op
25+
"SIM202", # negate-not-equal-op
26+
"UP038", # non-pep604-isinstance: This rule is deprecated
27+
]
28+
29+
[tool.ruff.lint.isort]
30+
force-single-line = true
431

532
[tool.mypy]
6-
disallow_untyped_defs = 1
33+
disallow_untyped_defs = true
734
exclude = [
835
'\.eggs',
936
'\.git',

pytest_unordered/__init__.py

Lines changed: 44 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,34 @@
1+
from __future__ import annotations
2+
3+
from collections.abc import Generator
4+
from collections.abc import Iterable
5+
from collections.abc import Mapping
6+
from typing import TYPE_CHECKING
17
from typing import Any
2-
from typing import Generator
3-
from typing import Iterable
4-
from typing import List
5-
from typing import Mapping
6-
from typing import Optional
7-
from typing import Tuple
88

99
import pytest
1010
from _pytest._io.saferepr import saferepr
1111
from _pytest.assertion.util import _compare_eq_any
12-
from _pytest.config import Config
12+
13+
if TYPE_CHECKING:
14+
from _pytest.config import Config
1315

1416

1517
class UnorderedList(list):
16-
def __init__(self, expected: Iterable, check_type: bool = True):
18+
def __init__(self, expected: Iterable, *, check_type: bool = True) -> None:
1719
if not isinstance(expected, Iterable):
20+
msg = f"cannot make unordered comparisons to non-iterable: {expected!r}"
1821
raise TypeError(
19-
"cannot make unordered comparisons to non-iterable: {!r}".format(expected)
22+
msg,
2023
)
2124
if isinstance(expected, Mapping):
22-
raise TypeError("cannot make unordered comparisons to mapping: {!r}".format(expected))
25+
msg = f"cannot make unordered comparisons to mapping: {expected!r}"
26+
raise TypeError(msg)
2327
super().__init__(expected)
24-
self._expected_type = type(expected) if check_type else None
28+
self.expected_type = type(expected) if check_type else None
2529

2630
def __eq__(self, actual: object) -> bool:
27-
if self._expected_type is not None and self._expected_type != type(actual):
31+
if self.expected_type is not None and self.expected_type is not type(actual):
2832
return False
2933
if not isinstance(actual, Iterable):
3034
return self.copy() == actual
@@ -37,16 +41,16 @@ def __eq__(self, actual: object) -> bool:
3741
def __ne__(self, actual: object) -> bool:
3842
return not (self == actual)
3943

40-
def compare_to(self, other: List) -> Tuple[List, List]:
44+
def compare_to(self, other: list) -> tuple[list, list]:
4145
extra_left = list(self)
42-
extra_right = []
43-
reordered = []
46+
extra_right: list[Any] = []
47+
reordered: list[Any] = []
4448
placeholder = object()
4549
for elem in other:
46-
try:
50+
if elem in extra_left:
4751
i = extra_left.index(elem)
4852
reordered.append(extra_left.pop(i))
49-
except ValueError:
53+
else:
5054
extra_right.append(elem)
5155
reordered.append(placeholder)
5256
placeholder_fillers = extra_left.copy()
@@ -59,7 +63,7 @@ def compare_to(self, other: List) -> Tuple[List, List]:
5963
return extra_left, extra_right
6064

6165

62-
def unordered(*args: Any, check_type: Optional[bool] = None) -> UnorderedList:
66+
def unordered(*args: Any, check_type: bool | None = None) -> UnorderedList:
6367
if len(args) == 1:
6468
if check_type is None:
6569
check_type = not isinstance(args[0], Generator)
@@ -69,60 +73,61 @@ def unordered(*args: Any, check_type: Optional[bool] = None) -> UnorderedList:
6973

7074
def unordered_deep(obj: Any) -> Any:
7175
if isinstance(obj, dict):
72-
return dict((k, unordered_deep(v)) for k, v in obj.items())
73-
if isinstance(obj, list) or isinstance(obj, tuple):
74-
return unordered((unordered_deep(x) for x in obj))
76+
return {k: unordered_deep(v) for k, v in obj.items()}
77+
if isinstance(obj, (list, tuple)):
78+
return unordered(unordered_deep(x) for x in obj)
7579
return obj
7680

7781

78-
def _compare_eq_unordered(left: Iterable, right: Iterable) -> Tuple[List, List]:
79-
extra_left = []
82+
def _compare_eq_unordered(left: Iterable, right: Iterable) -> tuple[list, list]:
83+
extra_left: list[Any] = []
8084
extra_right = list(right)
8185
for elem in left:
82-
try:
86+
if elem in extra_right:
8387
extra_right.remove(elem)
84-
except ValueError:
88+
else:
8589
extra_left.append(elem)
8690
return extra_left, extra_right
8791

8892

8993
def pytest_assertrepr_compare(
90-
config: Config, op: str, left: Any, right: Any
91-
) -> Optional[List[str]]:
94+
config: Config,
95+
op: str,
96+
left: Any,
97+
right: Any,
98+
) -> list[str] | None:
9299
if (isinstance(left, UnorderedList) or isinstance(right, UnorderedList)) and op == "==":
93100
verbose = config.getoption("verbose")
94101
left_repr = saferepr(left)
95102
right_repr = saferepr(right)
96-
result = ["{} {} {}".format(left_repr, op, right_repr)]
97-
left_type = left._expected_type if isinstance(left, UnorderedList) else type(left)
98-
right_type = right._expected_type if isinstance(right, UnorderedList) else type(right)
103+
result = [f"{left_repr} {op} {right_repr}"]
104+
left_type = left.expected_type if isinstance(left, UnorderedList) else type(left)
105+
right_type = right.expected_type if isinstance(right, UnorderedList) else type(right)
99106
if left_type and right_type and left_type != right_type:
100107
result.append("Type mismatch:")
101-
result.append("{} != {}".format(left_type, right_type))
108+
result.append(f"{left_type} != {right_type}")
102109
extra_left, extra_right = _compare_eq_unordered(left, right)
103110
if len(extra_left) == 1 and len(extra_right) == 1:
104111
result.append("One item replaced:")
105-
if pytest.version_tuple < (8, 0, 0):
112+
if pytest.version_tuple < (8, 0, 0): # pragma: no cover
106113
result.extend(
107-
_compare_eq_any(extra_left[0], extra_right[0], verbose=verbose) # type: ignore
114+
_compare_eq_any(extra_left[0], extra_right[0], verbose=verbose), # type: ignore[call-arg]
108115
)
109116
else:
110117
result.extend(
111118
_compare_eq_any(
112119
extra_left[0],
113120
extra_right[0],
114-
highlighter=config.get_terminal_writer()._highlight,
121+
highlighter=config.get_terminal_writer()._highlight, # noqa: SLF001
115122
verbose=verbose,
116-
)
123+
),
117124
)
118125
else:
119126
if extra_left:
120127
result.append("Extra items in the left sequence:")
121-
for item in extra_left:
122-
result.append(saferepr(item))
128+
result.extend(saferepr(item) for item in extra_left)
123129
if extra_right:
124130
result.append("Extra items in the right sequence:")
125-
for item in extra_right:
126-
result.append(saferepr(item))
131+
result.extend(saferepr(item) for item in extra_right)
127132
return result
128133
return None

setup.py

100644100755
Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,12 @@
11
#!/usr/bin/env python
2-
# -*- coding: utf-8 -*-
32

4-
import codecs
5-
import os
3+
from pathlib import Path
64

75
from setuptools import setup
86

97

108
def read(fname: str) -> str:
11-
file_path = os.path.join(os.path.dirname(__file__), fname)
12-
return codecs.open(file_path, encoding="utf-8").read()
9+
return (Path(__file__).parent / fname).read_text(encoding="utf-8")
1310

1411

1512
setup(

tests/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)