Coverage for tests/test_sequences.py: 100%

277 statements  

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

1"""Tests for `bunches.sequences`.""" 

2 

3from __future__ import annotations 

4 

5import pytest 

6 

7from bunches import DictList, Listing, settings 

8 

9from .conftest import Another, Something 

10 

11 

12class TestListing: 

13 """Tests for `Listing`.""" 

14 

15 def test_init(self) -> None: 

16 """Tests defaults.""" 

17 listing = Listing() 

18 assert listing.contents == [] 

19 assert len(listing) == 0 

20 

21 def test_add(self) -> None: 

22 """Tests `add` extends with sequences and appends everything else.""" 

23 listing = Listing(['a']) 

24 listing.add('bc') 

25 listing.add(['d', 'e']) 

26 listing.add(('f', 'g')) 

27 listing.add(b'h') 

28 listing.add(7) 

29 listing.add({'set'}) 

30 assert listing.contents == [ 

31 'a', 'bc', 'd', 'e', 'f', 'g', b'h', 7, {'set'}] 

32 

33 def test_delete(self) -> None: 

34 """Tests `delete` and `__delitem__`.""" 

35 listing = Listing(['a', 'b', 'c', 'd']) 

36 listing.delete(0) 

37 del listing[0] 

38 assert listing.contents == ['c', 'd'] 

39 listing.delete(slice(0, 2)) 

40 assert listing.contents == [] 

41 with pytest.raises(IndexError): 

42 listing.delete(0) 

43 

44 def test_insert_and_prepend(self) -> None: 

45 """Tests `insert` and `prepend`.""" 

46 listing = Listing(['c']) 

47 listing.insert(0, 'b') 

48 listing.prepend('a') 

49 assert listing.contents == ['a', 'b', 'c'] 

50 listing.prepend(['x', 'y']) 

51 assert listing.contents == ['x', 'y', 'a', 'b', 'c'] 

52 listing.prepend([['nested']]) 

53 assert listing.contents[0] == ['nested'] 

54 listing.prepend([]) 

55 assert len(listing) == 6 

56 

57 def test_getitem_setitem(self) -> None: 

58 """Tests `__getitem__` and `__setitem__`.""" 

59 listing = Listing(['a', 'b', 'c']) 

60 assert listing[1] == 'b' 

61 assert listing[-1] == 'c' 

62 assert listing[0:2] == ['a', 'b'] 

63 listing[1] = 'z' 

64 assert listing.contents == ['a', 'z', 'c'] 

65 listing[0:2] = ['x'] 

66 assert listing.contents == ['x', 'c'] 

67 with pytest.raises(IndexError): 

68 listing[5] 

69 

70 def test_mutable_sequence_methods(self) -> None: 

71 """Tests methods inherited from `MutableSequence`.""" 

72 listing = Listing(['a', 'b', 'c']) 

73 listing.append('d') 

74 listing.extend(['e', 'f']) 

75 assert listing.contents == ['a', 'b', 'c', 'd', 'e', 'f'] 

76 listing.remove('b') 

77 assert listing.pop() == 'f' 

78 assert listing.index('c') == 1 

79 assert listing.count('a') == 1 

80 listing.reverse() 

81 assert listing.contents == ['e', 'd', 'c', 'a'] 

82 assert 'a' in listing 

83 assert list(reversed(listing)) == ['a', 'c', 'd', 'e'] 

84 listing.clear() 

85 assert listing.contents == [] 

86 

87 def test_subset(self) -> None: 

88 """Tests `subset`.""" 

89 listing = Listing(['a', 'b', 'c', 'd']) 

90 subset = listing.subset(include = ['a', 'b', 'c'], exclude = 'b') 

91 assert isinstance(subset, Listing) 

92 assert subset.contents == ['a', 'c'] 

93 assert listing.subset(include = 'a').contents == ['a'] 

94 assert listing.subset(exclude = ['a', 'b']).contents == ['c', 'd'] 

95 assert listing.contents == ['a', 'b', 'c', 'd'] 

96 with pytest.raises(ValueError, match = 'must not be None'): 

97 listing.subset() 

98 

99 def test_subset_preserves_order_of_contents(self) -> None: 

100 """Tests `subset` follows the order of `contents`, not `include`.""" 

101 listing = Listing(['a', 'b', 'c']) 

102 assert listing.subset(include = ['c', 'a']).contents == ['a', 'c'] 

103 

104 def test_subset_returns(self) -> None: 

105 """Tests the `returns` argument of `subset`.""" 

106 listing = Listing(['a', 'b']) 

107 assert listing.subset(include = 'a', returns = 'simple') == ['a'] 

108 copied = listing.subset(include = 'a', returns = 'copy') 

109 assert isinstance(copied, Listing) 

110 with pytest.raises(ValueError, match = 'returns argument'): 

111 listing.subset( 

112 include = 'a', returns = 'other') # type: ignore[arg-type] 

113 settings.set_subset_return('simple') 

114 assert listing.subset(include = 'a') == ['a'] 

115 

116 def test_add_operators(self) -> None: 

117 """Tests `+` and `+=`.""" 

118 listing = Listing(['a']) 

119 combined = listing + ['b', 'c'] 

120 assert combined.contents == ['a', 'b', 'c'] 

121 assert listing.contents == ['a'] 

122 assert (listing + 'z').contents == ['a', 'z'] 

123 listing += ['b'] 

124 listing += 'c' 

125 assert listing.contents == ['a', 'b', 'c'] 

126 

127 def test_equality(self) -> None: 

128 """Tests equality.""" 

129 assert Listing([1, 2]) == Listing([1, 2]) 

130 assert Listing([1, 2]) != Listing([2, 1]) 

131 

132 def test_add_operator_is_deep_copy(self) -> None: 

133 """Tests that `+` does not share mutable items with the original.""" 

134 listing = Listing([[1]]) 

135 combined = listing + [2] 

136 combined[0].append(3) 

137 assert listing.contents == [[1]] 

138 

139 def test_add_listing(self) -> None: 

140 """Tests that adding a `Listing` extends with its items.""" 

141 listing = Listing([1]) 

142 listing += Listing([2, 3]) 

143 assert listing.contents == [1, 2, 3] 

144 listing.add(range(4, 6)) 

145 assert listing.contents == [1, 2, 3, 4, 5] 

146 

147 def test_prepend_strings_and_tuples(self) -> None: 

148 """Tests that `prepend` keeps `str` and `bytes` items whole.""" 

149 listing = Listing(['c']) 

150 listing.prepend('ab') 

151 listing.prepend(b'x') 

152 listing.prepend(('y', 'z')) 

153 assert listing.contents == ['y', 'z', b'x', 'ab', 'c'] 

154 

155 def test_subset_unhashable_items(self) -> None: 

156 """Tests `subset` with unhashable items.""" 

157 listing = Listing([[1], [2], [3]]) 

158 assert listing.subset(include = [[1], [3]]).contents == [[1], [3]] 

159 assert listing.subset(exclude = [[2]]).contents == [[1], [3]] 

160 

161 def test_subset_include_is_deep_copy(self) -> None: 

162 """Tests that including items does not share mutable items.""" 

163 listing = Listing([[1], [2]]) 

164 subset = listing.subset(include = [[1]]) 

165 subset[0].append(5) 

166 assert listing.contents == [[1], [2]] 

167 

168 

169class TestDictList: 

170 """Tests for `DictList`.""" 

171 

172 @pytest.fixture 

173 def dictlist(self) -> DictList: 

174 """Returns a `DictList` of strings. 

175 

176 Returns: 

177 `DictList` of strings. 

178 

179 """ 

180 return DictList(['a', 'b', 'c']) 

181 

182 def test_init(self) -> None: 

183 """Tests defaults.""" 

184 dictlist = DictList() 

185 assert dictlist.contents == [] 

186 assert dictlist.default_factory is None 

187 

188 def test_list_interface(self, dictlist: DictList) -> None: 

189 """Tests list behavior inherited from `Listing`. 

190 

191 Args: 

192 dictlist: `DictList` of strings. 

193 

194 """ 

195 dictlist.add('d') 

196 assert dictlist[3] == 'd' 

197 dictlist.insert(2, 'zebra') 

198 assert dictlist[2] == 'zebra' 

199 dictlist.append('e') 

200 dictlist.extend(['f', 'g']) 

201 assert dictlist.contents == [ 

202 'a', 'b', 'zebra', 'c', 'd', 'e', 'f', 'g'] 

203 dictlist.prepend('first') 

204 assert dictlist[0] == 'first' 

205 assert dictlist[1:3] == ['a', 'b'] 

206 dictlist[0] = 'start' 

207 dictlist[1:3] = ['x', 'y'] 

208 assert dictlist.contents[:4] == ['start', 'x', 'y', 'zebra'] 

209 

210 def test_getitem_by_name(self) -> None: 

211 """Tests `__getitem__` with names, attributes and multiple matches.""" 

212 something = Something() 

213 dictlist = DictList([something, Another(), 'plain', Something('x')]) 

214 assert dictlist['something'] is something 

215 assert dictlist['another'] == Another() 

216 assert dictlist['plain'] == 'plain' 

217 assert dictlist['x'] == Something('x') 

218 with pytest.raises(KeyError, match = 'missing'): 

219 dictlist['missing'] 

220 with pytest.raises(IndexError): 

221 dictlist[10] 

222 

223 def test_getitem_multiple_matches(self) -> None: 

224 """Tests that duplicate names return a `DictList` of matches.""" 

225 dictlist = DictList(['a', 'b', 'a'], default_factory = 'nope') 

226 matches = dictlist['a'] 

227 assert isinstance(matches, DictList) 

228 assert matches.contents == ['a', 'a'] 

229 assert matches.default_factory == 'nope' 

230 assert dictlist['b'] == 'b' 

231 

232 def test_setitem(self, dictlist: DictList) -> None: 

233 """Tests `__setitem__` with an index and with a name. 

234 

235 Args: 

236 dictlist: `DictList` of strings. 

237 

238 """ 

239 dictlist[0] = 'z' 

240 assert dictlist.contents == ['z', 'b', 'c'] 

241 dictlist['ignored'] = 'd' 

242 assert dictlist.contents == ['z', 'b', 'c', 'd'] 

243 dictlist['ignored'] = ['e', 'f'] 

244 assert dictlist.contents == ['z', 'b', 'c', 'd', 'e', 'f'] 

245 

246 def test_get(self, dictlist: DictList) -> None: 

247 """Tests `get` with and without defaults. 

248 

249 Args: 

250 dictlist: `DictList` of strings. 

251 

252 """ 

253 assert dictlist.get('a') == 'a' 

254 assert dictlist.get(1) == 'b' 

255 assert dictlist.get('tree', 'No') == 'No' 

256 with pytest.raises(KeyError, match = 'tree'): 

257 dictlist.get('tree') 

258 with pytest.raises(KeyError): 

259 dictlist.get(10) 

260 dictlist.setdefault(value = 'No') 

261 assert dictlist.default_factory == 'No' 

262 assert dictlist.get('tree') == 'No' 

263 assert dictlist.get(10) == 'No' 

264 assert dictlist.get('tree', 'Other') == 'Other' 

265 dictlist.default_factory = list 

266 assert dictlist.get('tree') == [] 

267 

268 def test_keys_values_items(self) -> None: 

269 """Tests `keys`, `values` and `items`.""" 

270 something = Something() 

271 dictlist = DictList(['a', something, 'a']) 

272 assert dictlist.keys() == ('a', 'something', 'a') 

273 assert dictlist.values() == ('a', something, 'a') 

274 assert dictlist.items() == ( 

275 ('a', 'a'), ('something', something), ('a', 'a')) 

276 

277 def test_update(self, dictlist: DictList) -> None: 

278 """Tests `update`, which discards the keys. 

279 

280 Args: 

281 dictlist: `DictList` of strings. 

282 

283 """ 

284 dictlist.update({'ignored': 'd', 'also_ignored': 'e'}) 

285 assert dictlist.contents == ['a', 'b', 'c', 'd', 'e'] 

286 

287 def test_delete(self) -> None: 

288 """Tests `delete` by index, by slice, and by name.""" 

289 dictlist = DictList(['a', 'b', 'a', 'c', 'd', 'e']) 

290 dictlist.delete(0) 

291 assert dictlist.contents == ['b', 'a', 'c', 'd', 'e'] 

292 dictlist.delete('a') 

293 assert dictlist.contents == ['b', 'c', 'd', 'e'] 

294 del dictlist['b'] 

295 assert dictlist.contents == ['c', 'd', 'e'] 

296 dictlist.delete(slice(0, 2)) 

297 assert dictlist.contents == ['e'] 

298 with pytest.raises(KeyError, match = 'missing'): 

299 dictlist.delete('missing') 

300 with pytest.raises(IndexError): 

301 dictlist.delete(5) 

302 

303 def test_delete_preserves_container(self) -> None: 

304 """Tests that deleting by name modifies `contents` in place.""" 

305 contents = ['a', 'b', 'a'] 

306 dictlist = DictList(contents) 

307 dictlist.delete('a') 

308 assert dictlist.contents is contents 

309 assert contents == ['b'] 

310 

311 def test_remove(self) -> None: 

312 """Tests `remove` inherited from `MutableSequence`.""" 

313 dictlist = DictList(['a', 'b', 'c']) 

314 dictlist.remove('b') 

315 assert dictlist.contents == ['a', 'c'] 

316 

317 def test_subset(self) -> None: 

318 """Tests `subset`.""" 

319 dictlist = DictList(['a', 'b', 'c', 'd', 'zebra']) 

320 subset = dictlist.subset( 

321 include = ['a', 'b', 'c', 'zebra'], exclude = 'c') 

322 assert isinstance(subset, DictList) 

323 assert subset.contents == ['a', 'b', 'zebra'] 

324 assert subset['zebra'] == 'zebra' 

325 copied = dictlist.subset(exclude = 'a', returns = 'copy') 

326 assert copied.contents == ['b', 'c', 'd', 'zebra'] 

327 assert dictlist.contents == ['a', 'b', 'c', 'd', 'zebra'] 

328 

329 def test_hybrid_use(self) -> None: 

330 """Tests a mix of dict and list access on the same instance.""" 

331 dictlist = DictList(['a', 'b', 'c']) 

332 dictlist.setdefault(value = 'No') 

333 assert dictlist.get('tree') == 'No' 

334 assert dictlist[1] == 'b' 

335 dictlist.append('b') 

336 assert dictlist.values() == ('a', 'b', 'c', 'b') 

337 assert dictlist.keys() == ('a', 'b', 'c', 'b') 

338 dictlist.clear() 

339 item = Something() 

340 dictlist.add(item) 

341 assert dictlist.keys() == ('something',) 

342 assert dictlist.values() == (item,) 

343 

344 def test_getitem_slice_and_negative_index( 

345 self, dictlist: DictList) -> None: 

346 """Tests that slices return lists and negative indexes work. 

347 

348 Args: 

349 dictlist: `DictList` of strings. 

350 

351 """ 

352 assert dictlist[-1] == 'c' 

353 assert dictlist[::2] == ['a', 'c'] 

354 assert dictlist.get(slice(0, 1)) == ['a'] 

355 

356 def test_custom_key_namer(self) -> None: 

357 """Tests that `DictList` uses the global key namer.""" 

358 settings.set_key_namer(lambda item: getattr(item, 'name', 'unnamed')) 

359 dictlist = DictList([Something(), 'plain']) 

360 assert dictlist.keys() == ('something', 'unnamed') 

361 assert dictlist['unnamed'] == 'plain' 

362 dictlist.delete('unnamed') 

363 assert dictlist.keys() == ('something',) 

364 

365 def test_add_operators(self, dictlist: DictList) -> None: 

366 """Tests `+` and `+=`. 

367 

368 Args: 

369 dictlist: `DictList` of strings. 

370 

371 """ 

372 combined = dictlist + Something() 

373 assert isinstance(combined, DictList) 

374 assert combined['something'] == Something() 

375 assert 'something' not in dictlist.keys() 

376 dictlist += ['d'] 

377 assert dictlist.keys() == ('a', 'b', 'c', 'd') 

378 

379 def test_items_with_duplicate_names(self) -> None: 

380 """Tests that `items` keeps every item with a duplicate name.""" 

381 first = Something('same') 

382 second = Another('same') 

383 dictlist = DictList([first, second]) 

384 assert dictlist.items() == (('same', first), ('same', second)) 

385 assert dictlist['same'].contents == [first, second] 

386 

387 def test_subset_by_name(self) -> None: 

388 """Tests that `subset` selects items by name.""" 

389 first = Something() 

390 second = Another() 

391 third = Something('third') 

392 dictlist = DictList([first, second, third]) 

393 subset = dictlist.subset(include = ['something', 'third']) 

394 assert subset.keys() == ('something', 'third') 

395 assert subset.values() == (first, third) 

396 assert subset['third'] is not third 

397 subset = dictlist.subset(exclude = 'another') 

398 assert subset.keys() == ('something', 'third') 

399 assert dictlist.subset(include = first).contents == []