Coverage for tests/test_base.py: 97%

70 statements  

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

1"""Tests for `bunches.base`.""" 

2 

3from __future__ import annotations 

4 

5import copy 

6import dataclasses 

7from typing import Any 

8 

9import pytest 

10 

11from bunches import ( 

12 Bunch, 

13 Catalog, 

14 ChainDict, 

15 DictList, 

16 Dictionary, 

17 Listing, 

18 Repository, 

19 settings, 

20) 

21 

22 

23@dataclasses.dataclass 

24class MinimalBunch(Bunch): 

25 """Smallest concrete `Bunch` (a list wrapper) for testing base behavior.""" 

26 

27 contents: list[Any] = dataclasses.field(default_factory = list) 

28 

29 def add(self, item: Any) -> None: 

30 """Appends `item`. 

31 

32 Args: 

33 item: item to append to `contents`. 

34 

35 """ 

36 self.contents.append(item) 

37 

38 def delete(self, item: int) -> None: 

39 """Deletes the item at index `item`. 

40 

41 Args: 

42 item: index of the item to delete. 

43 

44 """ 

45 del self.contents[item] 

46 

47 def subset( 

48 self, 

49 include: Any = None, 

50 exclude: Any = None, 

51 returns: Any = None) -> Any: 

52 """Returns a new instance with `include` items. 

53 

54 Args: 

55 include: items to include. 

56 exclude: ignored. 

57 returns: ignored. 

58 

59 Returns: 

60 A new instance with the items in `include`. 

61 

62 """ 

63 return self.__class__([i for i in self.contents if i in include]) 

64 

65 def __contains__(self, item: object) -> bool: 

66 """Returns if `item` is in `contents`. 

67 

68 Args: 

69 item: item to look for. 

70 

71 Returns: 

72 Whether `item` is in `contents`. 

73 

74 """ 

75 return item in self.contents 

76 

77 

78def test_bunch_is_abstract() -> None: 

79 """Tests that `Bunch` cannot be instantiated without required methods.""" 

80 with pytest.raises(TypeError): 

81 Bunch([1, 2]) # type: ignore[abstract] 

82 

83 @dataclasses.dataclass 

84 class NoSubset(Bunch): 

85 """`Bunch` without the required `subset` method.""" 

86 

87 contents: list[Any] = dataclasses.field(default_factory = list) 

88 

89 def add(self, item: Any) -> None: 

90 """Does nothing. 

91 

92 Args: 

93 item: ignored. 

94 

95 """ 

96 

97 def delete(self, item: Any) -> None: 

98 """Does nothing. 

99 

100 Args: 

101 item: ignored. 

102 

103 """ 

104 

105 def __contains__(self, item: object) -> bool: 

106 """Returns `False`. 

107 

108 Args: 

109 item: ignored. 

110 

111 Returns: 

112 `False`. 

113 

114 """ 

115 return False 

116 

117 with pytest.raises(TypeError): 

118 NoSubset() # type: ignore[abstract] 

119 

120 

121def test_collection_dunders() -> None: 

122 """Tests `__iter__`, `__len__`, and `__contains__`.""" 

123 bunch = MinimalBunch([1, 2, 3]) 

124 assert list(bunch) == [1, 2, 3] 

125 assert len(bunch) == 3 

126 assert 2 in bunch 

127 assert 4 not in bunch 

128 

129 

130def test_add_operators() -> None: 

131 """Tests `__add__` (copies) and `__iadd__` (in place).""" 

132 bunch = MinimalBunch([1]) 

133 combined = bunch + 2 

134 assert combined.contents == [1, 2] 

135 assert bunch.contents == [1] 

136 assert combined is not bunch 

137 same = bunch 

138 bunch += 3 

139 assert bunch is same 

140 assert same.contents == [1, 3] 

141 

142 

143def test_delitem() -> None: 

144 """Tests `__delitem__`, which calls `delete`.""" 

145 bunch = MinimalBunch([1, 2, 3]) 

146 del bunch[0] 

147 assert bunch.contents == [2, 3] 

148 

149 

150def test_resolve_returns() -> None: 

151 """Tests `_resolve_returns`.""" 

152 bunch = MinimalBunch() 

153 assert bunch._resolve_returns(None) == 'class' 

154 assert bunch._resolve_returns('copy') == 'copy' 

155 settings.set_subset_return('simple') 

156 assert bunch._resolve_returns(None) == 'simple' 

157 assert bunch._resolve_returns('class') == 'class' 

158 

159 

160def test_concrete_subclasses_are_bunches() -> None: 

161 """Tests that package classes are `Bunch` instances.""" 

162 assert isinstance(Dictionary(), Bunch) 

163 assert isinstance(Listing(), Bunch) 

164 

165 

166@pytest.mark.parametrize(('instance', 'other'), [ 

167 (Dictionary({'a': [1]}), {'b': [2]}), 

168 (Catalog({'a': [1]}), {'b': [2]}), 

169 (ChainDict([Dictionary({'a': [1]})]), {'b': [2]}), 

170 (Repository({'a': [1]}), [2]), 

171 (Listing([[1]]), [[2]]), 

172 (DictList(['a']), ['b'])]) 

173def test_add_operators_all_classes(instance: Bunch, other: Any) -> None: 

174 """Tests that `+` copies and `+=` modifies in place for every class. 

175 

176 Args: 

177 instance: instance of a `bunches` class. 

178 other: item to add to `instance`. 

179 

180 """ 

181 original = copy.deepcopy(instance) 

182 combined = instance + other 

183 assert type(combined) is type(instance) 

184 assert instance == original 

185 assert combined != original 

186 same = instance 

187 instance += other 

188 assert instance is same 

189 assert instance == combined