Coverage for src/epublib/resources/window.py: 100%
69 statements
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-07 09:38 -0300
« prev ^ index » next coverage.py v7.10.7, created at 2025-10-07 09:38 -0300
1from collections.abc import Callable, Generator, Iterable, Iterator, MutableSequence
2from typing import Never, cast, overload, override
4from epublib.exceptions import EPUBError
7class Window[B, T](MutableSequence[T]):
8 def __init__(
9 self,
10 base: MutableSequence[B],
11 predicate: Callable[[B], bool],
12 doesnt_satisfy_error_msg: Callable[[B], str],
13 ):
14 self._base: MutableSequence[T] = cast(MutableSequence[T], base)
15 self._predicate: Callable[[T], bool] = cast(Callable[[T], bool], predicate)
16 self.doesnt_satisfy_error_msg: Callable[[T], str] = cast(
17 Callable[[T], str],
18 doesnt_satisfy_error_msg,
19 )
21 def raise_predicate_error(self, value: T) -> Never:
22 raise EPUBError(self.doesnt_satisfy_error_msg(value))
24 def _indices(self) -> Generator[int]:
25 """Indices of base elements that satisfy the predicate."""
26 return (i for i, v in enumerate(self._base) if self._predicate(v))
28 @overload
29 def __getitem__(self, index: int) -> T: ...
30 @overload
31 def __getitem__(self, index: slice) -> MutableSequence[T]: ...
33 @override
34 def __getitem__(self, index: int | slice) -> T | MutableSequence[T]:
35 indices = list(self._indices())
36 if isinstance(index, slice):
37 return [self._base[i] for i in indices[index]]
38 return self._base[indices[index]]
40 @overload
41 def __setitem__(self, index: int, value: T) -> None: ...
42 @overload
43 def __setitem__(self, index: slice, value: Iterable[T]) -> None: ...
45 @override
46 def __setitem__(self, index: int | slice, value: T | Iterable[T]) -> None:
47 indices = list(self._indices())
49 if isinstance(index, slice) and isinstance(value, Iterable):
50 iterable = iter(cast(Iterable[T], value))
51 items: list[T] = [] # Collect to check all before inserting
53 for v in iterable:
54 if not self._predicate(v):
55 self.raise_predicate_error(v)
56 items.append(v)
58 for i, v in zip(indices[index], items):
59 self._base[i] = v
61 elif isinstance(index, int):
62 value = cast(T, value)
63 if not self._predicate(value):
64 self.raise_predicate_error(value)
66 self._base[indices[index]] = value
68 else:
69 raise ValueError(
70 "Setting multiple items to a single index is not supported"
71 )
73 @override
74 def __delitem__(self, index: int | slice) -> None:
75 indices = list(self._indices())
76 if isinstance(index, slice):
77 indices = sorted(indices[index], reverse=True)
78 for i in indices:
79 del self._base[i]
80 else:
81 del self._base[indices[index]]
83 @override
84 def __len__(self) -> int:
85 return len(list(self._indices()))
87 @override
88 def insert(self, index: int, value: T) -> None:
89 if not self._predicate(value):
90 self.raise_predicate_error(value)
92 indices = list(self._indices())
93 if not indices:
94 # no matching elements yet → just append to base
95 self._base.append(value)
96 elif index >= len(indices):
97 # append after last matching element
98 last_match = indices[-1]
99 self._base.insert(last_match + 1, value)
100 else:
101 # insert before the `index`th matching element
102 print(indices, index)
103 self._base.insert(indices[index], value)
105 @override
106 def __iter__(self) -> Iterator[T]:
107 indices = self._indices()
108 for i in indices:
109 yield self._base[i]
111 @override
112 def __reversed__(self) -> Iterator[T]:
113 indices = reversed(tuple(self._indices()))
114 for i in indices:
115 yield self._base[i]