Coverage for tests/test_mappings.py: 100%

505 statements  

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

1"""Tests for `bunches.mappings`.""" 

2 

3from __future__ import annotations 

4 

5import pytest 

6 

7from bunches import Catalog, ChainDict, Dictionary, Repository, settings 

8 

9from .conftest import Another, Something, Unnamed 

10 

11 

12class TestDictionary: 

13 """Tests for `Dictionary`.""" 

14 

15 def test_init(self) -> None: 

16 """Tests defaults.""" 

17 dictionary = Dictionary() 

18 assert dictionary.contents == {} 

19 assert dictionary.default_factory is None 

20 assert len(dictionary) == 0 

21 

22 def test_fromkeys(self) -> None: 

23 """Tests `fromkeys`.""" 

24 dictionary = Dictionary.fromkeys(['a', 'b'], 'tree') 

25 assert isinstance(dictionary, Dictionary) 

26 assert dictionary.contents == {'a': 'tree', 'b': 'tree'} 

27 dictionary = Dictionary.fromkeys(['a'], 1, default_factory = 5) 

28 assert dictionary.default_factory == 5 

29 

30 def test_add(self) -> None: 

31 """Tests `add`.""" 

32 dictionary = Dictionary({'a': 1}) 

33 dictionary.add({'b': 2}) 

34 dictionary.add({'c': 3}, d = 4) 

35 assert dictionary.contents == {'a': 1, 'b': 2, 'c': 3, 'd': 4} 

36 

37 def test_delete(self) -> None: 

38 """Tests `delete` and `__delitem__`.""" 

39 dictionary = Dictionary({'a': 1, 'b': 2, 'c': 3}) 

40 dictionary.delete('a') 

41 del dictionary['b'] 

42 assert dictionary.contents == {'c': 3} 

43 with pytest.raises(KeyError): 

44 dictionary.delete('missing') 

45 with pytest.raises(KeyError): 

46 del dictionary['missing'] 

47 

48 def test_get(self) -> None: 

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

50 dictionary = Dictionary({'a': 1}) 

51 assert dictionary.get('a') == 1 

52 assert dictionary.get('b', 3) == 3 

53 with pytest.raises(KeyError): 

54 dictionary.get('b') 

55 with pytest.raises(KeyError): 

56 dictionary.get(['unhashable']) 

57 dictionary.default_factory = 'Nada' 

58 assert dictionary.get('b') == 'Nada' 

59 assert dictionary.get('b', 3) == 3 

60 dictionary.default_factory = list 

61 assert dictionary.get('b') == [] 

62 assert dictionary.get('b') is not dictionary.get('b') 

63 assert dictionary.get('a') == 1 

64 

65 def test_setdefault(self) -> None: 

66 """Tests `setdefault`.""" 

67 dictionary = Dictionary() 

68 dictionary.setdefault('No') 

69 assert dictionary.default_factory == 'No' 

70 assert dictionary.get('anything') == 'No' 

71 

72 def test_keys_values_items(self) -> None: 

73 """Tests that `keys`, `values` and `items` return tuples.""" 

74 dictionary = Dictionary({'a': 1, 'b': 2}) 

75 assert dictionary.keys() == ('a', 'b') 

76 assert dictionary.values() == (1, 2) 

77 assert dictionary.items() == (('a', 1), ('b', 2)) 

78 assert dict(dictionary) == {'a': 1, 'b': 2} 

79 

80 def test_getitem_setitem(self) -> None: 

81 """Tests `__getitem__` and `__setitem__`.""" 

82 dictionary = Dictionary({'a': 1}) 

83 dictionary['b'] = 2 

84 assert dictionary['a'] == 1 

85 assert dictionary['b'] == 2 

86 assert 'b' in dictionary 

87 assert 'c' not in dictionary 

88 with pytest.raises(KeyError): 

89 dictionary['c'] 

90 

91 def test_mutable_mapping_methods(self) -> None: 

92 """Tests methods inherited from `MutableMapping`.""" 

93 dictionary = Dictionary({'a': 1, 'b': 2}) 

94 assert list(dictionary) == ['a', 'b'] 

95 assert dictionary.pop('a') == 1 

96 dictionary.update({'z': 26}) 

97 assert dictionary.contents == {'b': 2, 'z': 26} 

98 dictionary.clear() 

99 assert dictionary.contents == {} 

100 

101 def test_subset(self) -> None: 

102 """Tests `subset` with `include` and `exclude`.""" 

103 dictionary = Dictionary({'a': 1, 'b': 2, 'c': 3}, default_factory = 0) 

104 subset = dictionary.subset(include = ['a', 'b']) 

105 assert isinstance(subset, Dictionary) 

106 assert subset.contents == {'a': 1, 'b': 2} 

107 assert dictionary.subset(include = 'c').contents == {'c': 3} 

108 assert dictionary.subset(exclude = 'a').contents == {'b': 2, 'c': 3} 

109 assert dictionary.subset( 

110 include = ['a', 'b'], exclude = ['b']).contents == {'a': 1} 

111 assert dictionary.contents == {'a': 1, 'b': 2, 'c': 3} 

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

113 dictionary.subset() 

114 with pytest.raises(KeyError): 

115 dictionary.subset(include = 'missing') 

116 

117 def test_subset_returns(self) -> None: 

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

119 dictionary = Dictionary({'a': 1, 'b': 2}, default_factory = 0) 

120 assert dictionary.subset(include = 'a', returns = 'simple') == {'a': 1} 

121 copied = dictionary.subset(include = 'a', returns = 'copy') 

122 assert isinstance(copied, Dictionary) 

123 assert copied.default_factory == 0 

124 assert copied.contents == {'a': 1} 

125 assert dictionary.subset(include = 'a').default_factory is None 

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

127 dictionary.subset( 

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

129 settings.set_subset_return('simple') 

130 assert dictionary.subset(include = 'a') == {'a': 1} 

131 

132 def test_subset_exclude_is_deep_copy(self) -> None: 

133 """Tests that excluding only does not share mutable values.""" 

134 dictionary = Dictionary({'a': [1], 'b': [2]}) 

135 subset = dictionary.subset(exclude = 'b') 

136 subset['a'].append(5) 

137 assert dictionary['a'] == [1] 

138 

139 def test_add_operators(self) -> None: 

140 """Tests `+` and `+=`.""" 

141 dictionary = Dictionary({'a': 1}) 

142 combined = dictionary + {'b': 2} 

143 assert combined.contents == {'a': 1, 'b': 2} 

144 assert dictionary.contents == {'a': 1} 

145 assert (dictionary + Dictionary({'c': 3})).contents == {'a': 1, 'c': 3} 

146 dictionary += {'b': 2} 

147 assert dictionary.contents == {'a': 1, 'b': 2} 

148 

149 def test_equality_and_repr(self) -> None: 

150 """Tests equality and `repr`.""" 

151 assert Dictionary({'a': 1}) == Dictionary({'a': 1}) 

152 assert Dictionary({'a': 1}) != Dictionary({'a': 2}) 

153 assert 'contents' in repr(Dictionary({'a': 1})) 

154 

155 def test_fromkeys_default_value(self) -> None: 

156 """Tests that `fromkeys` matches `dict.fromkeys` without a value.""" 

157 dictionary = Dictionary.fromkeys(['a', 'b']) 

158 assert dictionary.contents == {'a': None, 'b': None} 

159 dictionary = Dictionary.fromkeys(key for key in 'xy') 

160 assert dictionary.keys() == ('x', 'y') 

161 

162 def test_get_stored_falsy_values(self) -> None: 

163 """Tests that `get` returns stored falsy values, not the default.""" 

164 dictionary = Dictionary({'a': 0, 'b': None}, default_factory = 5) 

165 assert dictionary.get('a') == 0 

166 assert dictionary.get('b') is None 

167 assert dictionary.get('c') == 5 

168 

169 def test_add_operator_is_deep_copy(self) -> None: 

170 """Tests that `+` does not share mutable values with the original.""" 

171 dictionary = Dictionary({'a': [1]}) 

172 combined = dictionary + {'b': 2} 

173 combined['a'].append(2) 

174 assert dictionary['a'] == [1] 

175 

176 def test_add_operator_keeps_class_and_fields(self) -> None: 

177 """Tests that `+` returns the same class with the same fields.""" 

178 catalog = Catalog({'a': 1}, default = 'a', always_return_list = True) 

179 combined = catalog + {'b': 2} 

180 assert isinstance(combined, Catalog) 

181 assert combined.default == 'a' 

182 assert combined.always_return_list is True 

183 

184 def test_popitem(self) -> None: 

185 """Tests `popitem` inherited from `MutableMapping`.""" 

186 dictionary = Dictionary({'a': 1}) 

187 assert dictionary.popitem() == ('a', 1) 

188 assert len(dictionary) == 0 

189 with pytest.raises(KeyError): 

190 dictionary.popitem() 

191 

192 def test_tuple_keys(self) -> None: 

193 """Tests that `tuple` keys work as in a `dict`.""" 

194 dictionary = Dictionary() 

195 dictionary[('a', 'b')] = 1 

196 assert dictionary[('a', 'b')] == 1 

197 assert dictionary.subset(include = [('a', 'b')]).contents == { 

198 ('a', 'b'): 1} 

199 del dictionary[('a', 'b')] 

200 assert len(dictionary) == 0 

201 

202 def test_contains(self) -> None: 

203 """Tests that `in` only checks the stored keys.""" 

204 dictionary = Dictionary({'a': 1, ('b', 'c'): 2}) 

205 assert 'a' in dictionary 

206 assert ('b', 'c') in dictionary 

207 assert 'b' not in dictionary 

208 assert ['a'] not in dictionary 

209 

210 def test_subset_include_is_deep_copy(self) -> None: 

211 """Tests that including keys does not share mutable values.""" 

212 dictionary = Dictionary({'a': [1], 'b': [2]}) 

213 subset = dictionary.subset(include = 'a') 

214 subset['a'].append(5) 

215 assert dictionary['a'] == [1] 

216 simple = dictionary.subset(include = 'a', returns = 'simple') 

217 simple['a'].append(5) 

218 assert dictionary['a'] == [1] 

219 

220 def test_subset_keeps_shared_values(self) -> None: 

221 """Tests that values shared within the subset stay shared.""" 

222 shared = [1] 

223 dictionary = Dictionary({'a': shared, 'b': shared, 'c': 3}) 

224 subset = dictionary.subset(include = ['a', 'b']) 

225 assert subset['a'] is subset['b'] 

226 assert subset['a'] is not shared 

227 

228 

229class TestCatalog: 

230 """Tests for `Catalog`.""" 

231 

232 @pytest.fixture 

233 def catalog(self) -> Catalog: 

234 """Returns a `Catalog` with three items. 

235 

236 Returns: 

237 `Catalog` with three items. 

238 

239 """ 

240 return Catalog(contents = { 

241 'tester': Something, 

242 'another': Another, 

243 'a_third': Unnamed()}) 

244 

245 def test_init(self) -> None: 

246 """Tests defaults.""" 

247 catalog = Catalog() 

248 assert catalog.contents == {} 

249 assert catalog.default == 'all' 

250 assert catalog.always_return_list is False 

251 

252 def test_direct_access(self, catalog: Catalog) -> None: 

253 """Tests direct key access and membership. 

254 

255 Args: 

256 catalog: `Catalog` with three items. 

257 

258 """ 

259 assert catalog['tester'] is Something 

260 assert 'tester' in catalog 

261 assert 'another' in catalog 

262 assert 'a_third' in catalog 

263 assert len(catalog) == 3 

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

265 catalog['missing'] 

266 

267 def test_all(self, catalog: Catalog) -> None: 

268 """Tests the `all` wildcard. 

269 

270 Args: 

271 catalog: `Catalog` with three items. 

272 

273 """ 

274 values = catalog['all'] 

275 assert isinstance(values, list) 

276 assert len(values) == 3 

277 assert catalog['All'] == values 

278 assert catalog[['all']] == values 

279 

280 def test_default(self, catalog: Catalog) -> None: 

281 """Tests the `default` wildcard. 

282 

283 Args: 

284 catalog: `Catalog` with three items. 

285 

286 """ 

287 assert catalog['default'] == catalog['all'] 

288 catalog.default = 'tester' 

289 assert catalog['default'] is Something 

290 catalog.default = ['tester', 'another'] 

291 assert catalog['default'] == [Something, Another] 

292 assert catalog['Defaults'] == [Something, Another] 

293 

294 def test_none(self, catalog: Catalog) -> None: 

295 """Tests the `none` wildcard. 

296 

297 Args: 

298 catalog: `Catalog` with three items. 

299 

300 """ 

301 assert catalog['none'] is None 

302 assert catalog['None'] is None 

303 catalog.always_return_list = True 

304 assert catalog['none'] == [] 

305 catalog.always_return_list = False 

306 catalog.default_factory = 'nothing' 

307 assert catalog['none'] == 'nothing' 

308 catalog.default_factory = list 

309 assert catalog['none'] == [] 

310 

311 def test_list_of_keys(self, catalog: Catalog) -> None: 

312 """Tests accessing a list of keys. 

313 

314 Args: 

315 catalog: `Catalog` with three items. 

316 

317 """ 

318 assert catalog[['tester', 'another']] == [Something, Another] 

319 assert catalog[('tester',)] == [Something] 

320 assert catalog[['tester', 'missing']] == [Something] 

321 

322 def test_always_return_list(self, catalog: Catalog) -> None: 

323 """Tests `always_return_list`. 

324 

325 Args: 

326 catalog: `Catalog` with three items. 

327 

328 """ 

329 catalog.always_return_list = True 

330 assert catalog['tester'] == [Something] 

331 with pytest.raises(KeyError): 

332 catalog['missing'] 

333 

334 def test_setitem(self, catalog: Catalog) -> None: 

335 """Tests `__setitem__` with a key and with a list of keys. 

336 

337 Args: 

338 catalog: `Catalog` with three items. 

339 

340 """ 

341 catalog['fourth'] = 4 

342 assert catalog['fourth'] == 4 

343 catalog[['fifth', 'sixth']] = [5, 6] 

344 assert catalog['fifth'] == 5 

345 assert catalog['sixth'] == 6 

346 with pytest.raises(ValueError, match = 'shorter|longer'): 

347 catalog[['a', 'b']] = [1] 

348 

349 def test_add_and_get(self, catalog: Catalog) -> None: 

350 """Tests `add` and `get`. 

351 

352 Args: 

353 catalog: `Catalog` with three items. 

354 

355 """ 

356 catalog.add({'new': 1}) 

357 assert catalog['new'] == 1 

358 assert catalog.get('new') == 1 

359 assert catalog.get('missing', 'x') == 'x' 

360 catalog.default_factory = 'nothing' 

361 assert catalog.get('missing') == 'nothing' 

362 

363 def test_delete(self, catalog: Catalog) -> None: 

364 """Tests `delete` with one and several keys. 

365 

366 Args: 

367 catalog: `Catalog` with three items. 

368 

369 """ 

370 catalog.delete('a_third') 

371 assert 'a_third' not in catalog 

372 del catalog[['tester', 'another']] 

373 assert 'tester' not in catalog 

374 assert len(catalog) == 0 

375 

376 def test_delete_missing(self, catalog: Catalog) -> None: 

377 """Tests that `delete` raises and does not change anything. 

378 

379 Args: 

380 catalog: `Catalog` with three items. 

381 

382 """ 

383 with pytest.raises(KeyError, match = 'not found'): 

384 catalog.delete(['tester', 'missing']) 

385 assert len(catalog) == 3 

386 with pytest.raises(KeyError): 

387 catalog.delete('missing') 

388 

389 def test_delete_preserves_container(self) -> None: 

390 """Tests that `delete` modifies `contents` in place.""" 

391 contents = {'a': 1, 'b': 2} 

392 catalog = Catalog(contents = contents) 

393 catalog.delete('a') 

394 assert catalog.contents is contents 

395 assert contents == {'b': 2} 

396 

397 def test_subset(self, catalog: Catalog) -> None: 

398 """Tests `subset`. 

399 

400 Args: 

401 catalog: `Catalog` with three items. 

402 

403 """ 

404 subset = catalog.subset(include = ['tester', 'another']) 

405 assert isinstance(subset, Catalog) 

406 assert subset.contents == {'tester': Something, 'another': Another} 

407 simple = catalog.subset(exclude = 'tester', returns = 'simple') 

408 assert isinstance(simple, dict) 

409 assert list(simple) == ['another', 'a_third'] 

410 

411 def test_tuple_key(self, catalog: Catalog) -> None: 

412 """Tests that a stored `tuple` key is used as a single key. 

413 

414 Args: 

415 catalog: `Catalog` with three items. 

416 

417 """ 

418 catalog[('x', 'y')] = 'pair' 

419 assert catalog[('x', 'y')] == 'pair' 

420 assert catalog.get(('x', 'y')) == 'pair' 

421 assert catalog[('tester', 'another')] == [Something, Another] 

422 catalog.delete(('x', 'y')) 

423 assert ('x', 'y') not in catalog.contents 

424 assert len(catalog) == 3 

425 

426 def test_default_with_missing_key(self, catalog: Catalog) -> None: 

427 """Tests that missing keys in `default` are skipped. 

428 

429 Args: 

430 catalog: `Catalog` with three items. 

431 

432 """ 

433 catalog.default = ['tester', 'missing'] 

434 assert catalog['default'] == [Something] 

435 

436 def test_iteration(self, catalog: Catalog) -> None: 

437 """Tests that iterating a `Catalog` returns its keys. 

438 

439 Args: 

440 catalog: `Catalog` with three items. 

441 

442 """ 

443 assert list(catalog) == ['tester', 'another', 'a_third'] 

444 assert catalog.keys() == ('tester', 'another', 'a_third') 

445 

446 def test_contains_wildcards(self, catalog: Catalog) -> None: 

447 """Tests that wildcards and key lists are not stored keys. 

448 

449 Args: 

450 catalog: `Catalog` with three items. 

451 

452 """ 

453 assert 'all' not in catalog 

454 assert 'none' not in catalog 

455 assert 'default' not in catalog 

456 assert ['tester'] not in catalog 

457 assert catalog['all'] 

458 catalog['all'] = 'stored' 

459 assert 'all' in catalog 

460 assert len(catalog['all']) == 4 

461 

462 

463class TestChainDict: 

464 """Tests for `ChainDict`.""" 

465 

466 @pytest.fixture 

467 def chain(self) -> ChainDict: 

468 """Returns a `ChainDict` with two mappings that share a key. 

469 

470 Returns: 

471 `ChainDict` with two mappings that share a key. 

472 

473 """ 

474 return ChainDict(contents = [ 

475 Dictionary({'a': 1, 'b': 2}), 

476 Dictionary({'a': 10, 'c': 30})]) 

477 

478 def test_init(self) -> None: 

479 """Tests defaults.""" 

480 chain = ChainDict() 

481 assert chain.contents == [] 

482 assert chain.return_first is True 

483 assert len(chain) == 0 

484 assert list(chain) == [] 

485 

486 def test_maps(self, chain: ChainDict) -> None: 

487 """Tests the `maps` property. 

488 

489 Args: 

490 chain: `ChainDict` with two mappings that share a key. 

491 

492 """ 

493 assert chain.maps is chain.contents 

494 chain.maps = [Dictionary({'z': 1})] 

495 assert chain.contents == [Dictionary({'z': 1})] 

496 del chain.maps 

497 assert chain.contents == [] 

498 

499 def test_fromkeys(self) -> None: 

500 """Tests `fromkeys`.""" 

501 chain = ChainDict.fromkeys(['a', 'b'], 1) 

502 assert isinstance(chain, ChainDict) 

503 assert chain.keys() == ('a', 'b') 

504 assert chain['a'] == 1 

505 assert len(chain.contents) == 1 

506 

507 def test_getitem_return_first(self, chain: ChainDict) -> None: 

508 """Tests `__getitem__` when returning only the first match. 

509 

510 Args: 

511 chain: `ChainDict` with two mappings that share a key. 

512 

513 """ 

514 assert chain['a'] == 1 

515 assert chain['b'] == 2 

516 assert chain['c'] == 30 

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

518 chain['missing'] 

519 

520 def test_getitem_all_matches(self, chain: ChainDict) -> None: 

521 """Tests `__getitem__` when returning all matches. 

522 

523 Args: 

524 chain: `ChainDict` with two mappings that share a key. 

525 

526 """ 

527 chain.return_first = False 

528 assert chain['a'] == [1, 10] 

529 assert chain['b'] == 2 

530 assert chain['c'] == 30 

531 with pytest.raises(KeyError): 

532 chain['missing'] 

533 

534 def test_setitem(self, chain: ChainDict) -> None: 

535 """Tests `__setitem__`, including on an empty `ChainDict`. 

536 

537 Args: 

538 chain: `ChainDict` with two mappings that share a key. 

539 

540 """ 

541 chain['z'] = 26 

542 assert chain.contents[0]['z'] == 26 

543 assert 'z' not in chain.contents[1] 

544 empty = ChainDict() 

545 empty['a'] = 1 

546 assert empty.contents == [Dictionary({'a': 1})] 

547 assert empty['a'] == 1 

548 

549 def test_keys_values_items(self, chain: ChainDict) -> None: 

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

551 

552 Args: 

553 chain: `ChainDict` with two mappings that share a key. 

554 

555 """ 

556 assert chain.keys() == ('a', 'b', 'a', 'c') 

557 assert chain.values() == (1, 2, 10, 30) 

558 assert chain.items() == (('a', 1), ('b', 2), ('a', 10), ('c', 30)) 

559 

560 def test_mapping_interface(self, chain: ChainDict) -> None: 

561 """Tests iteration and length use keys, as in any mapping. 

562 

563 Args: 

564 chain: `ChainDict` with two mappings that share a key. 

565 

566 """ 

567 assert list(chain) == ['a', 'b', 'a', 'c'] 

568 assert len(chain) == 4 

569 assert 'c' in chain 

570 assert 'z' not in chain 

571 assert dict(chain) == {'a': 1, 'b': 2, 'c': 30} 

572 

573 def test_add(self, chain: ChainDict) -> None: 

574 """Tests `add` with `Dictionary` instances and plain mappings. 

575 

576 Args: 

577 chain: `ChainDict` with two mappings that share a key. 

578 

579 """ 

580 chain.add(Dictionary({'x': 1})) 

581 chain.add({'y': 2}) 

582 chain.add({'z': 3}, w = 4) 

583 chain.add(Dictionary({'v': 5}), u = 6) 

584 assert len(chain.contents) == 6 

585 assert all(isinstance(d, Dictionary) for d in chain.contents) 

586 assert chain['y'] == 2 

587 assert chain.contents[4].contents == {'z': 3, 'w': 4} 

588 assert chain.contents[5].contents == {'v': 5, 'u': 6} 

589 

590 def test_delete(self, chain: ChainDict) -> None: 

591 """Tests `delete` removes a key from every stored mapping. 

592 

593 Args: 

594 chain: `ChainDict` with two mappings that share a key. 

595 

596 """ 

597 chain.delete('a') 

598 assert chain.keys() == ('b', 'c') 

599 del chain['b'] 

600 assert chain.keys() == ('c',) 

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

602 chain.delete('missing') 

603 

604 def test_new_child_and_parents(self, chain: ChainDict) -> None: 

605 """Tests `new_child` and `parents`. 

606 

607 Args: 

608 chain: `ChainDict` with two mappings that share a key. 

609 

610 """ 

611 chain.default_factory = 'nothing' 

612 chain.return_first = False 

613 chain.new_child(Dictionary({'a': 0})) 

614 assert chain['a'] == [0, 1, 10] 

615 parents = chain.parents 

616 assert isinstance(parents, ChainDict) 

617 assert len(parents.contents) == 2 

618 assert parents.default_factory == 'nothing' 

619 assert parents.return_first is False 

620 assert len(chain.contents) == 3 

621 

622 def test_subset(self, chain: ChainDict) -> None: 

623 """Tests `subset`, including keys missing from some mappings. 

624 

625 Args: 

626 chain: `ChainDict` with two mappings that share a key. 

627 

628 """ 

629 subset = chain.subset(include = ['a', 'c']) 

630 assert isinstance(subset, ChainDict) 

631 assert subset.keys() == ('a', 'a', 'c') 

632 assert subset.values() == (1, 10, 30) 

633 subset = chain.subset(exclude = 'a') 

634 assert subset.keys() == ('b', 'c') 

635 subset = chain.subset(include = 'b') 

636 assert subset.keys() == ('b',) 

637 assert len(subset.contents) == 2 

638 assert chain.keys() == ('a', 'b', 'a', 'c') 

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

640 chain.subset() 

641 

642 def test_subset_returns(self, chain: ChainDict) -> None: 

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

644 

645 Args: 

646 chain: `ChainDict` with two mappings that share a key. 

647 

648 """ 

649 chain.default_factory = 'nothing' 

650 copied = chain.subset(include = 'a', returns = 'copy') 

651 assert copied.default_factory == 'nothing' 

652 simple = chain.subset(include = 'a', returns = 'simple') 

653 assert isinstance(simple, list) 

654 assert simple[0] == Dictionary({'a': 1}) 

655 

656 def test_mutable_mapping_methods(self, chain: ChainDict) -> None: 

657 """Tests methods inherited from `MutableMapping`. 

658 

659 Args: 

660 chain: `ChainDict` with two mappings that share a key. 

661 

662 """ 

663 chain.update({'q': 5}) 

664 assert chain.contents[0]['q'] == 5 

665 assert chain.pop('q') == 5 

666 assert 'q' not in chain 

667 

668 def test_new_child_defaults(self, chain: ChainDict) -> None: 

669 """Tests `new_child` without a mapping and with a plain `dict`. 

670 

671 Args: 

672 chain: `ChainDict` with two mappings that share a key. 

673 

674 """ 

675 chain.new_child() 

676 assert chain.contents[0] == Dictionary() 

677 assert chain['a'] == 1 

678 chain.new_child({'a': 0}) 

679 assert isinstance(chain.contents[0], Dictionary) 

680 assert chain['a'] == 0 

681 assert chain.subset(include = 'a').values() == (0, 1, 10) 

682 

683 def test_get(self, chain: ChainDict) -> None: 

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

685 

686 Args: 

687 chain: `ChainDict` with two mappings that share a key. 

688 

689 """ 

690 assert chain.get('c') == 30 

691 assert chain.get('missing', 'x') == 'x' 

692 with pytest.raises(KeyError): 

693 chain.get('missing') 

694 chain.default_factory = 'nothing' 

695 assert chain.get('missing') == 'nothing' 

696 

697 def test_fromkeys_default_value(self) -> None: 

698 """Tests that `fromkeys` matches `dict.fromkeys` without a value.""" 

699 chain = ChainDict.fromkeys(['a']) 

700 assert chain['a'] is None 

701 

702 def test_add_operators(self, chain: ChainDict) -> None: 

703 """Tests `+` and `+=`. 

704 

705 Args: 

706 chain: `ChainDict` with two mappings that share a key. 

707 

708 """ 

709 combined = chain + {'z': 1} 

710 assert len(combined.contents) == 3 

711 assert len(chain.contents) == 2 

712 assert combined['z'] == 1 

713 chain += {'z': 2} 

714 assert chain['z'] == 2 

715 

716 def test_contains(self, chain: ChainDict) -> None: 

717 """Tests that `in` checks every stored mapping. 

718 

719 Args: 

720 chain: `ChainDict` with two mappings that share a key. 

721 

722 """ 

723 assert 'a' in chain 

724 assert 'c' in chain 

725 assert 'missing' not in chain 

726 assert ['a'] not in chain 

727 assert 'a' not in ChainDict() 

728 

729 def test_parents_is_property(self, chain: ChainDict) -> None: 

730 """Tests that `parents` is a property, as in `collections.ChainMap`. 

731 

732 Args: 

733 chain: `ChainDict` with two mappings that share a key. 

734 

735 """ 

736 assert isinstance(ChainDict.parents, property) 

737 assert chain.parents.keys() == ('a', 'c') 

738 assert chain.parents.parents.contents == [] 

739 

740 

741class TestRepository: 

742 """Tests for `Repository`.""" 

743 

744 def test_init(self) -> None: 

745 """Tests defaults.""" 

746 repository = Repository() 

747 assert repository.contents == {} 

748 assert repository.overwrite is False 

749 

750 def test_add_infers_names(self) -> None: 

751 """Tests that `add` infers keys.""" 

752 repository = Repository() 

753 repository.add(Another()) 

754 repository.add(Unnamed()) 

755 repository.add('a_string') 

756 repository.add(Something) 

757 assert repository.keys() == ( 

758 'another', 'unnamed', 'a_string', 'something') 

759 

760 def test_add_explicit_key(self) -> None: 

761 """Tests `add` with a key.""" 

762 repository = Repository() 

763 repository.add(Something, 'random_name') 

764 repository.add(Another(), key = 'other') 

765 assert repository['random_name'] is Something 

766 assert 'other' in repository 

767 repository.add(Another(), key = 'other') 

768 assert repository.keys() == ('random_name', 'other', 'other2') 

769 

770 def test_add_unique_keys(self) -> None: 

771 """Tests that keys are made unique when `overwrite` is `False`.""" 

772 repository = Repository() 

773 for _ in range(3): 

774 repository.add(Another()) 

775 assert repository.keys() == ('another', 'another2', 'another3') 

776 

777 def test_add_overwrite(self) -> None: 

778 """Tests that items are overwritten when `overwrite` is `True`.""" 

779 repository = Repository(overwrite = True) 

780 first = Another('same') 

781 second = Another('same') 

782 repository.add(first) 

783 repository.add(second) 

784 assert repository.keys() == ('same',) 

785 assert repository['same'] is second 

786 

787 def test_add_kwargs(self) -> None: 

788 """Tests that extra keyword arguments are stored.""" 

789 repository = Repository() 

790 repository.add(Another(), extra = 1) 

791 assert repository['extra'] == 1 

792 

793 def test_get_name_override(self) -> None: 

794 """Tests that a subclass can change how names are inferred.""" 

795 

796 class Numbered(Repository): 

797 """`Repository` that names items by their position.""" 

798 

799 def _get_name(self, item: object) -> str: 

800 """Returns a name based on the number of stored items. 

801 

802 Args: 

803 item: ignored. 

804 

805 Returns: 

806 'item_' followed by the number of stored items. 

807 

808 """ 

809 return f'item_{len(self)}' 

810 

811 repository = Numbered() 

812 repository.add('a') 

813 repository.add('b') 

814 assert repository.keys() == ('item_0', 'item_1') 

815 

816 def test_get_name(self) -> None: 

817 """Tests `_get_name`, which uses the global key namer.""" 

818 repository = Repository() 

819 assert repository._get_name(Something()) == 'something' 

820 settings.set_key_namer(lambda item: 'same') 

821 assert repository._get_name(Something()) == 'same' 

822 

823 def test_delete(self) -> None: 

824 """Tests `delete`.""" 

825 repository = Repository() 

826 repository.add(Another()) 

827 repository.add(Something, 'random_name') 

828 repository.delete('random_name') 

829 assert 'random_name' not in repository 

830 assert 'another' in repository 

831 with pytest.raises(KeyError): 

832 repository.delete('random_name') 

833 

834 def test_subset(self) -> None: 

835 """Tests `subset` keeps `Repository` behavior.""" 

836 repository = Repository(overwrite = True) 

837 repository.add(Another()) 

838 repository.add(Something()) 

839 subset = repository.subset(include = 'another', returns = 'copy') 

840 assert isinstance(subset, Repository) 

841 assert subset.overwrite is True 

842 assert subset.keys() == ('another',) 

843 

844 def test_add_empty_string_key(self) -> None: 

845 """Tests that an empty `str` key is used instead of inferred.""" 

846 repository = Repository() 

847 repository.add(Another(), key = '') 

848 assert repository.keys() == ('',) 

849 

850 def test_add_without_name_raises(self) -> None: 

851 """Tests that `add` raises when no name can be inferred.""" 

852 settings.set_key_namer(lambda item: None) 

853 repository = Repository() 

854 with pytest.raises(ValueError, match = 'name could not be inferred'): 

855 repository.add(Another()) 

856 repository.add(Another(), key = 'given') 

857 assert repository.keys() == ('given',) 

858 

859 def test_add_unique_keys_ending_in_digits(self) -> None: 

860 """Tests unique keys for names that already end in a digit.""" 

861 repository = Repository() 

862 for _ in range(3): 

863 repository.add(Another('v2')) 

864 assert repository.keys() == ('v2', 'v22', 'v23')