Skip to content

Commit b3e4bab

Browse files
committed
Merge branch 'release/3.8.0'
2 parents da2524d + 7e390e2 commit b3e4bab

File tree

9 files changed

+106
-17
lines changed

9 files changed

+106
-17
lines changed

.github/workflows/stale.yml

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
name: Close stale issues and pull requests
2+
3+
on:
4+
workflow_dispatch:
5+
schedule:
6+
- cron: '0 0 * * *' # Run every day at midnight
7+
8+
jobs:
9+
stale:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/stale@v8
13+
with:
14+
days-before-stale: 30
15+
exempt-issue-labels: in-progress,help-wanted,pinned,security,enhancement
16+
exempt-all-pr-assignees: true
17+

_python_utils_tests/test_aio.py

+6
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,13 @@ async def async_gen():
3030
yield 2
3131
yield 3
3232

33+
async def empty_gen():
34+
if False:
35+
yield 1
36+
3337
assert await acontainer(async_gen) == [1, 2, 3]
3438
assert await acontainer(async_gen()) == [1, 2, 3]
3539
assert await acontainer(async_gen, set) == {1, 2, 3}
3640
assert await acontainer(async_gen(), set) == {1, 2, 3}
41+
assert await acontainer(empty_gen) == []
42+
assert await acontainer(empty_gen()) == []

_python_utils_tests/test_containers.py

+42
Original file line numberDiff line numberDiff line change
@@ -29,3 +29,45 @@ def test_unique_list_raise() -> None:
2929

3030
del a[10]
3131
del a[5:15]
32+
33+
34+
def test_sliceable_deque() -> None:
35+
d: containers.SlicableDeque[int] = containers.SlicableDeque(range(10))
36+
assert d[0] == 0
37+
assert d[-1] == 9
38+
assert d[1:3] == [1, 2]
39+
assert d[1:3:2] == [1]
40+
assert d[1:3:-1] == []
41+
assert d[3:1] == []
42+
assert d[3:1:-1] == [3, 2]
43+
assert d[3:1:-2] == [3]
44+
with pytest.raises(ValueError):
45+
assert d[1:3:0]
46+
assert d[1:3:1] == [1, 2]
47+
assert d[1:3:2] == [1]
48+
assert d[1:3:-1] == []
49+
50+
51+
def test_sliceable_deque_pop() -> None:
52+
d: containers.SlicableDeque[int] = containers.SlicableDeque(range(10))
53+
54+
assert d.pop() == 9 == 9
55+
assert d.pop(0) == 0
56+
57+
with pytest.raises(IndexError):
58+
assert d.pop(100)
59+
60+
with pytest.raises(IndexError):
61+
assert d.pop(2)
62+
63+
with pytest.raises(IndexError):
64+
assert d.pop(-2)
65+
66+
67+
def test_sliceable_deque_eq() -> None:
68+
d: containers.SlicableDeque[int] = containers.SlicableDeque([1, 2, 3])
69+
assert d == [1, 2, 3]
70+
assert d == (1, 2, 3)
71+
assert d == {1, 2, 3}
72+
assert d == d
73+
assert d == containers.SlicableDeque([1, 2, 3])

_python_utils_tests/test_decorators.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def test_wraps_classmethod(): # type: ignore
6060
some_class.some_classmethod.assert_called_with(123) # type: ignore
6161

6262

63-
def test_wraps_classmethod(): # type: ignore
63+
def test_wraps_annotated_classmethod(): # type: ignore
6464
some_class = SomeClass()
6565
some_class.some_annotated_classmethod = MagicMock()
6666
wrapped_method = wraps_classmethod(SomeClass.some_annotated_classmethod)(

_python_utils_tests/test_generators.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ async def test_abatcher():
1616

1717

1818
@pytest.mark.asyncio
19-
async def test_abatcher_timed():
19+
async def test_abatcher_timed() -> None:
2020
batches: types.List[types.List[int]] = []
2121
async for batch in python_utils.abatcher(
2222
python_utils.acount(stop=10, delay=0.08), interval=0.1

python_utils/__about__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@
77
)
88
__url__: str = 'https://github.com/WoLpH/python-utils'
99
# Omit type info due to automatic versioning script
10-
__version__ = '3.7.0'
10+
__version__ = '3.8.0'

python_utils/aio.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ async def acontainer(
4848

4949
item: _T
5050
items: types.List[_T] = []
51-
async for item in iterable_:
51+
async for item in iterable_: # pragma: no branch
5252
items.append(item)
5353

5454
return container(items)

python_utils/containers.py

+33-7
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# pyright: reportIncompatibleMethodOverride=false
22
import abc
3-
import typing
43
import collections
4+
import typing
55

66
from . import types
77

@@ -238,7 +238,7 @@ def __init__(
238238
def insert(self, index: types.SupportsIndex, value: HT) -> None:
239239
if value in self._set:
240240
if self.on_duplicate == 'raise':
241-
raise ValueError('Duplicate value: %s' % value)
241+
raise ValueError(f'Duplicate value: {value}')
242242
else:
243243
return
244244

@@ -248,7 +248,7 @@ def insert(self, index: types.SupportsIndex, value: HT) -> None:
248248
def append(self, value: HT) -> None:
249249
if value in self._set:
250250
if self.on_duplicate == 'raise':
251-
raise ValueError('Duplicate value: %s' % value)
251+
raise ValueError(f'Duplicate value: {value}')
252252
else:
253253
return
254254

@@ -258,11 +258,11 @@ def append(self, value: HT) -> None:
258258
def __contains__(self, item: HT) -> bool: # type: ignore
259259
return item in self._set
260260

261-
@types.overload
261+
@typing.overload
262262
def __setitem__(self, indices: types.SupportsIndex, values: HT) -> None:
263263
...
264264

265-
@types.overload
265+
@typing.overload
266266
def __setitem__(self, indices: slice, values: types.Iterable[HT]) -> None:
267267
...
268268

@@ -310,12 +310,14 @@ def __delitem__(
310310
super().__delitem__(index)
311311

312312

313+
# Type hinting `collections.deque` does not work consistently between Python
314+
# runtime, mypy and pyright currently so we have to ignore the errors
313315
class SlicableDeque(types.Generic[T], collections.deque): # type: ignore
314-
@types.overload
316+
@typing.overload
315317
def __getitem__(self, index: types.SupportsIndex) -> T:
316318
...
317319

318-
@types.overload
320+
@typing.overload
319321
def __getitem__(self, index: slice) -> 'SlicableDeque[T]':
320322
...
321323

@@ -340,6 +342,30 @@ def __getitem__(
340342
else:
341343
return types.cast(T, super().__getitem__(index))
342344

345+
def __eq__(self, other: types.Any) -> bool:
346+
# Allow for comparison with a list or tuple
347+
if isinstance(other, list):
348+
return list(self) == other
349+
elif isinstance(other, tuple):
350+
return tuple(self) == other
351+
elif isinstance(other, set):
352+
return set(self) == other
353+
else:
354+
return super().__eq__(other)
355+
356+
def pop(self, index: int = -1) -> T:
357+
# We need to allow for an index but a deque only allows the removal of
358+
# the first or last item.
359+
if index == 0:
360+
return typing.cast(T, super().popleft())
361+
elif index in {-1, len(self) - 1}:
362+
return typing.cast(T, super().pop())
363+
else:
364+
raise IndexError(
365+
'Only index 0 and the last index (`N-1` or `-1`) '
366+
'are supported'
367+
)
368+
343369

344370
if __name__ == '__main__':
345371
import doctest

python_utils/decorators.py

+4-6
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import contextlib
12
import functools
23
import logging
34
import random
@@ -175,7 +176,9 @@ def wraps_classmethod(
175176
def _wraps_classmethod(
176177
wrapper: types.Callable[types.Concatenate[types.Any, _P], _T],
177178
) -> types.Callable[types.Concatenate[types.Type[_S], _P], _T]:
178-
try: # pragma: no cover
179+
# For some reason `functools.update_wrapper` fails on some test
180+
# runs but not while running actual code
181+
with contextlib.suppress(AttributeError):
179182
wrapper = functools.update_wrapper(
180183
wrapper,
181184
wrapped,
@@ -185,11 +188,6 @@ def _wraps_classmethod(
185188
if a != '__annotations__'
186189
),
187190
)
188-
except AttributeError: # pragma: no cover
189-
# For some reason `functools.update_wrapper` fails on some test
190-
# runs but not while running actual code
191-
pass
192-
193191
if annotations := getattr(wrapped, '__annotations__', {}):
194192
annotations.pop('self', None)
195193
wrapper.__annotations__ = annotations

0 commit comments

Comments
 (0)