Coverage for src/bunches/base.py: 100%

34 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-30 23:02 +0000

1"""Base class for extensible, flexible, lightweight collection types. 

2 

3Contents: 

4 Bunch (Collection, abc.ABC): base class for collections in `bunches`. It 

5 requires subclasses to have `add`, `delete`, and `subset` methods. 

6 

7""" 

8 

9from __future__ import annotations 

10 

11import abc 

12import copy 

13import dataclasses 

14from collections.abc import Collection, Iterator 

15from typing import TYPE_CHECKING, Any, Self 

16 

17from . import settings 

18 

19if TYPE_CHECKING: 

20 from .kinds import SubsetReturns 

21 

22 

23@dataclasses.dataclass 

24class Bunch(Collection, abc.ABC): 

25 """Base for general `bunches` collections. 

26 

27 A Bunch differs from a general python Collection in 5 ways: 

28 1) It must include an `add` method which provides the default mechanism 

29 for adding new items to the collection. `add` allows a subclass to 

30 designate the preferred method of adding to the collection's stored 

31 data without replacing other access methods. 

32 2) It must include a `delete` method which provides the default 

33 mechanism for deleting items in the collection. `delete` is called 

34 by the `__delitem__` dunder method to delete stored items. 

35 3) A subclass must include a `subset` method with optional `include` and 

36 `exclude` parameters for returning a subset of the Bunch subclass. 

37 4) It supports the '+' and '+=' operators being used to join a Bunch 

38 subclass instance with an item of the same python type (mapping, 

39 sequence, etc.). Both operators call the Bunch subclass `add` 

40 method to implement how the added item(s) is/are added. '+' leaves 

41 the original instance unchanged and returns a modified deep copy, 

42 while '+=' modifies the instance in place. 

43 5) It offers an accessible `contents` attribute that contains the native 

44 Python type for the `Bunch` subclass. This allows easy reversion to 

45 the simple type or simple bypassing of the `Bunch` subclass methods. 

46 

47 Args: 

48 contents: stored collection of items. 

49 

50 """ 

51 

52 contents: Collection[Any] 

53 

54 """ Required Methods """ 

55 

56 @abc.abstractmethod 

57 def add(self, item: Any, *args: Any, **kwargs: Any) -> None: 

58 """Adds `item` to `contents`. 

59 

60 Args: 

61 item: item to add to `contents`. 

62 *args: positional arguments. 

63 **kwargs: keyword arguments. 

64 

65 """ 

66 

67 @abc.abstractmethod 

68 def delete(self, item: Any, *args: Any, **kwargs: Any) -> None: 

69 """Deletes `item` from `contents`. 

70 

71 Args: 

72 item: item or key to delete in `contents`. 

73 *args: positional arguments. 

74 **kwargs: keyword arguments. 

75 

76 Raises: 

77 KeyError: if `item` is not in `contents`. Subclasses should 

78 implement this error. 

79 

80 """ 

81 

82 @abc.abstractmethod 

83 def subset( 

84 self, 

85 include: Collection[Any] | Any | None = None, 

86 exclude: Collection[Any] | Any | None = None, 

87 returns: SubsetReturns | None = None) -> Any: 

88 """Returns a new instance with a subset of `contents`. 

89 

90 This method applies `include` before `exclude` if both are passed. If 

91 `include` is None, all existing items will be added to the new subset 

92 class instance before `exclude` is applied. 

93 

94 Args: 

95 include: item(s) to include in the new `Bunch`. Defaults to `None`. 

96 exclude: item(s) to exclude from the new `Bunch`. Defaults to 

97 `None`. 

98 returns: whether to return a new instance of the `Bunch` subclass 

99 ('class'), a deep copy of the subclass instance ('copy') or the 

100 simple native Python type ('simple'). Defaults to `None`, which 

101 uses the global setting stored in `settings._SUBSET_RETURN`. 

102 

103 Returns: 

104 A subset of the stored data in the form dictated by `returns`. 

105 

106 """ 

107 

108 """ Dunder Methods """ 

109 

110 def __add__(self, other: Any) -> Self: 

111 """Returns a deep copy with `other` combined using the `add` method. 

112 

113 Args: 

114 other: item to add to the copy's `contents` using the `add` method. 

115 

116 Returns: 

117 A new instance. The original instance is not modified. 

118 

119 """ 

120 new_instance = copy.deepcopy(self) 

121 new_instance.add(item = other) 

122 return new_instance 

123 

124 def __iadd__(self, other: Any) -> Self: 

125 """Combines argument with `contents` using the `add` method. 

126 

127 Args: 

128 other: item to add to `contents` using the `add` method. 

129 

130 Returns: 

131 The instance, modified in place. 

132 

133 """ 

134 self.add(item = other) 

135 return self 

136 

137 def __delitem__(self, item: Any) -> None: 

138 """Deletes `item` from `contents`. 

139 

140 Args: 

141 item: item or key to delete in `contents`. 

142 

143 Raises: 

144 KeyError: if `item` is not in `contents`. 

145 

146 """ 

147 self.delete(item = item) 

148 

149 def __iter__(self) -> Iterator[Any]: 

150 """Returns iterator of `contents`. 

151 

152 Returns: 

153 Iterator of `contents`. 

154 

155 """ 

156 return iter(self.contents) 

157 

158 def __len__(self) -> int: 

159 """Returns length of `contents`. 

160 

161 Returns: 

162 Length of `contents`. 

163 

164 """ 

165 return len(self.contents) 

166 

167 """ Private Methods """ 

168 

169 def _resolve_returns(self, returns: SubsetReturns | None) -> SubsetReturns: 

170 """Returns `returns` or the global default if it is `None`. 

171 

172 Args: 

173 returns: `returns` argument passed to a `subset` method. 

174 

175 Returns: 

176 'returns' or the value of `settings._SUBSET_RETURN`. 

177 

178 """ 

179 return settings._SUBSET_RETURN if returns is None else returns