Skip to content

Commit

Permalink
Added slicable deque
Browse files Browse the repository at this point in the history
  • Loading branch information
wolph committed Sep 24, 2023
1 parent 156d0ea commit b868921
Show file tree
Hide file tree
Showing 4 changed files with 80 additions and 14 deletions.
42 changes: 42 additions & 0 deletions _python_utils_tests/test_containers.py
Expand Up @@ -29,3 +29,45 @@ def test_unique_list_raise() -> None:

del a[10]
del a[5:15]


def test_sliceable_deque() -> None:
d: containers.SlicableDeque[int] = containers.SlicableDeque(range(10))
assert d[0] == 0
assert d[-1] == 9
assert d[1:3] == [1, 2]
assert d[1:3:2] == [1]
assert d[1:3:-1] == []
assert d[3:1] == []
assert d[3:1:-1] == [3, 2]
assert d[3:1:-2] == [3]
with pytest.raises(ValueError):
assert d[1:3:0]
assert d[1:3:1] == [1, 2]
assert d[1:3:2] == [1]
assert d[1:3:-1] == []


def test_sliceable_deque_pop() -> None:
d: containers.SlicableDeque[int] = containers.SlicableDeque(range(10))

assert d.pop() == 9 == 9
assert d.pop(0) == 0

with pytest.raises(IndexError):
assert d.pop(100)

with pytest.raises(IndexError):
assert d.pop(2)

with pytest.raises(IndexError):
assert d.pop(-2)


def test_sliceable_deque_eq() -> None:
d: containers.SlicableDeque[int] = containers.SlicableDeque([1, 2, 3])
assert d == [1, 2, 3]
assert d == (1, 2, 3)
assert d == {1, 2, 3}
assert d == d
assert d == containers.SlicableDeque([1, 2, 3])
2 changes: 1 addition & 1 deletion _python_utils_tests/test_decorators.py
Expand Up @@ -60,7 +60,7 @@ def test_wraps_classmethod(): # type: ignore
some_class.some_classmethod.assert_called_with(123) # type: ignore


def test_wraps_classmethod(): # type: ignore
def test_wraps_annotated_classmethod(): # type: ignore
some_class = SomeClass()
some_class.some_annotated_classmethod = MagicMock()
wrapped_method = wraps_classmethod(SomeClass.some_annotated_classmethod)(
Expand Down
40 changes: 33 additions & 7 deletions python_utils/containers.py
@@ -1,7 +1,7 @@
# pyright: reportIncompatibleMethodOverride=false
import abc
import typing
import collections
import typing

from . import types

Expand Down Expand Up @@ -238,7 +238,7 @@ def __init__(
def insert(self, index: types.SupportsIndex, value: HT) -> None:
if value in self._set:
if self.on_duplicate == 'raise':
raise ValueError('Duplicate value: %s' % value)
raise ValueError(f'Duplicate value: {value}')
else:
return

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

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

@types.overload
@typing.overload
def __setitem__(self, indices: types.SupportsIndex, values: HT) -> None:
...

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

Expand Down Expand Up @@ -310,12 +310,14 @@ def __delitem__(
super().__delitem__(index)


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

@types.overload
@typing.overload
def __getitem__(self, index: slice) -> 'SlicableDeque[T]':
...

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

def __eq__(self, other: types.Any) -> bool:
# Allow for comparison with a list or tuple
if isinstance(other, list):
return list(self) == other
elif isinstance(other, tuple):
return tuple(self) == other
elif isinstance(other, set):
return set(self) == other
else:
return super().__eq__(other)

def pop(self, index: int = -1) -> T:
# We need to allow for an index but a deque only allows the removal of
# the first or last item.
if index == 0:
return typing.cast(T, super().popleft())
elif index in {-1, len(self) - 1}:
return typing.cast(T, super().pop())
else:
raise IndexError(
'Only index 0 and the last index (`N-1` or `-1`) '
'are supported'
)


if __name__ == '__main__':
import doctest
Expand Down
10 changes: 4 additions & 6 deletions python_utils/decorators.py
@@ -1,3 +1,4 @@
import contextlib
import functools
import logging
import random
Expand Down Expand Up @@ -175,7 +176,9 @@ def wraps_classmethod(
def _wraps_classmethod(
wrapper: types.Callable[types.Concatenate[types.Any, _P], _T],
) -> types.Callable[types.Concatenate[types.Type[_S], _P], _T]:
try: # pragma: no cover
# For some reason `functools.update_wrapper` fails on some test
# runs but not while running actual code
with contextlib.suppress(AttributeError):
wrapper = functools.update_wrapper(
wrapper,
wrapped,
Expand All @@ -185,11 +188,6 @@ def _wraps_classmethod(
if a != '__annotations__'
),
)
except AttributeError: # pragma: no cover
# For some reason `functools.update_wrapper` fails on some test
# runs but not while running actual code
pass

if annotations := getattr(wrapped, '__annotations__', {}):
annotations.pop('self', None)
wrapper.__annotations__ = annotations
Expand Down

0 comments on commit b868921

Please sign in to comment.