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

1from collections.abc import Callable, Generator, Iterable, Iterator, MutableSequence 

2from typing import Never, cast, overload, override 

3 

4from epublib.exceptions import EPUBError 

5 

6 

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 ) 

20 

21 def raise_predicate_error(self, value: T) -> Never: 

22 raise EPUBError(self.doesnt_satisfy_error_msg(value)) 

23 

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)) 

27 

28 @overload 

29 def __getitem__(self, index: int) -> T: ... 

30 @overload 

31 def __getitem__(self, index: slice) -> MutableSequence[T]: ... 

32 

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]] 

39 

40 @overload 

41 def __setitem__(self, index: int, value: T) -> None: ... 

42 @overload 

43 def __setitem__(self, index: slice, value: Iterable[T]) -> None: ... 

44 

45 @override 

46 def __setitem__(self, index: int | slice, value: T | Iterable[T]) -> None: 

47 indices = list(self._indices()) 

48 

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 

52 

53 for v in iterable: 

54 if not self._predicate(v): 

55 self.raise_predicate_error(v) 

56 items.append(v) 

57 

58 for i, v in zip(indices[index], items): 

59 self._base[i] = v 

60 

61 elif isinstance(index, int): 

62 value = cast(T, value) 

63 if not self._predicate(value): 

64 self.raise_predicate_error(value) 

65 

66 self._base[indices[index]] = value 

67 

68 else: 

69 raise ValueError( 

70 "Setting multiple items to a single index is not supported" 

71 ) 

72 

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]] 

82 

83 @override 

84 def __len__(self) -> int: 

85 return len(list(self._indices())) 

86 

87 @override 

88 def insert(self, index: int, value: T) -> None: 

89 if not self._predicate(value): 

90 self.raise_predicate_error(value) 

91 

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) 

104 

105 @override 

106 def __iter__(self) -> Iterator[T]: 

107 indices = self._indices() 

108 for i in indices: 

109 yield self._base[i] 

110 

111 @override 

112 def __reversed__(self) -> Iterator[T]: 

113 indices = reversed(tuple(self._indices())) 

114 for i in indices: 

115 yield self._base[i]