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
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-30 23:02 +0000
1"""Tests for `bunches.mappings`."""
3from __future__ import annotations
5import pytest
7from bunches import Catalog, ChainDict, Dictionary, Repository, settings
9from .conftest import Another, Something, Unnamed
12class TestDictionary:
13 """Tests for `Dictionary`."""
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
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
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}
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']
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
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'
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}
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']
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 == {}
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')
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}
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]
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}
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}))
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')
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
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]
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
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()
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
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
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]
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
229class TestCatalog:
230 """Tests for `Catalog`."""
232 @pytest.fixture
233 def catalog(self) -> Catalog:
234 """Returns a `Catalog` with three items.
236 Returns:
237 `Catalog` with three items.
239 """
240 return Catalog(contents = {
241 'tester': Something,
242 'another': Another,
243 'a_third': Unnamed()})
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
252 def test_direct_access(self, catalog: Catalog) -> None:
253 """Tests direct key access and membership.
255 Args:
256 catalog: `Catalog` with three items.
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']
267 def test_all(self, catalog: Catalog) -> None:
268 """Tests the `all` wildcard.
270 Args:
271 catalog: `Catalog` with three items.
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
280 def test_default(self, catalog: Catalog) -> None:
281 """Tests the `default` wildcard.
283 Args:
284 catalog: `Catalog` with three items.
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]
294 def test_none(self, catalog: Catalog) -> None:
295 """Tests the `none` wildcard.
297 Args:
298 catalog: `Catalog` with three items.
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'] == []
311 def test_list_of_keys(self, catalog: Catalog) -> None:
312 """Tests accessing a list of keys.
314 Args:
315 catalog: `Catalog` with three items.
317 """
318 assert catalog[['tester', 'another']] == [Something, Another]
319 assert catalog[('tester',)] == [Something]
320 assert catalog[['tester', 'missing']] == [Something]
322 def test_always_return_list(self, catalog: Catalog) -> None:
323 """Tests `always_return_list`.
325 Args:
326 catalog: `Catalog` with three items.
328 """
329 catalog.always_return_list = True
330 assert catalog['tester'] == [Something]
331 with pytest.raises(KeyError):
332 catalog['missing']
334 def test_setitem(self, catalog: Catalog) -> None:
335 """Tests `__setitem__` with a key and with a list of keys.
337 Args:
338 catalog: `Catalog` with three items.
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]
349 def test_add_and_get(self, catalog: Catalog) -> None:
350 """Tests `add` and `get`.
352 Args:
353 catalog: `Catalog` with three items.
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'
363 def test_delete(self, catalog: Catalog) -> None:
364 """Tests `delete` with one and several keys.
366 Args:
367 catalog: `Catalog` with three items.
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
376 def test_delete_missing(self, catalog: Catalog) -> None:
377 """Tests that `delete` raises and does not change anything.
379 Args:
380 catalog: `Catalog` with three items.
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')
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}
397 def test_subset(self, catalog: Catalog) -> None:
398 """Tests `subset`.
400 Args:
401 catalog: `Catalog` with three items.
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']
411 def test_tuple_key(self, catalog: Catalog) -> None:
412 """Tests that a stored `tuple` key is used as a single key.
414 Args:
415 catalog: `Catalog` with three items.
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
426 def test_default_with_missing_key(self, catalog: Catalog) -> None:
427 """Tests that missing keys in `default` are skipped.
429 Args:
430 catalog: `Catalog` with three items.
432 """
433 catalog.default = ['tester', 'missing']
434 assert catalog['default'] == [Something]
436 def test_iteration(self, catalog: Catalog) -> None:
437 """Tests that iterating a `Catalog` returns its keys.
439 Args:
440 catalog: `Catalog` with three items.
442 """
443 assert list(catalog) == ['tester', 'another', 'a_third']
444 assert catalog.keys() == ('tester', 'another', 'a_third')
446 def test_contains_wildcards(self, catalog: Catalog) -> None:
447 """Tests that wildcards and key lists are not stored keys.
449 Args:
450 catalog: `Catalog` with three items.
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
463class TestChainDict:
464 """Tests for `ChainDict`."""
466 @pytest.fixture
467 def chain(self) -> ChainDict:
468 """Returns a `ChainDict` with two mappings that share a key.
470 Returns:
471 `ChainDict` with two mappings that share a key.
473 """
474 return ChainDict(contents = [
475 Dictionary({'a': 1, 'b': 2}),
476 Dictionary({'a': 10, 'c': 30})])
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) == []
486 def test_maps(self, chain: ChainDict) -> None:
487 """Tests the `maps` property.
489 Args:
490 chain: `ChainDict` with two mappings that share a key.
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 == []
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
507 def test_getitem_return_first(self, chain: ChainDict) -> None:
508 """Tests `__getitem__` when returning only the first match.
510 Args:
511 chain: `ChainDict` with two mappings that share a key.
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']
520 def test_getitem_all_matches(self, chain: ChainDict) -> None:
521 """Tests `__getitem__` when returning all matches.
523 Args:
524 chain: `ChainDict` with two mappings that share a key.
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']
534 def test_setitem(self, chain: ChainDict) -> None:
535 """Tests `__setitem__`, including on an empty `ChainDict`.
537 Args:
538 chain: `ChainDict` with two mappings that share a key.
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
549 def test_keys_values_items(self, chain: ChainDict) -> None:
550 """Tests `keys`, `values` and `items`.
552 Args:
553 chain: `ChainDict` with two mappings that share a key.
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))
560 def test_mapping_interface(self, chain: ChainDict) -> None:
561 """Tests iteration and length use keys, as in any mapping.
563 Args:
564 chain: `ChainDict` with two mappings that share a key.
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}
573 def test_add(self, chain: ChainDict) -> None:
574 """Tests `add` with `Dictionary` instances and plain mappings.
576 Args:
577 chain: `ChainDict` with two mappings that share a key.
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}
590 def test_delete(self, chain: ChainDict) -> None:
591 """Tests `delete` removes a key from every stored mapping.
593 Args:
594 chain: `ChainDict` with two mappings that share a key.
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')
604 def test_new_child_and_parents(self, chain: ChainDict) -> None:
605 """Tests `new_child` and `parents`.
607 Args:
608 chain: `ChainDict` with two mappings that share a key.
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
622 def test_subset(self, chain: ChainDict) -> None:
623 """Tests `subset`, including keys missing from some mappings.
625 Args:
626 chain: `ChainDict` with two mappings that share a key.
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()
642 def test_subset_returns(self, chain: ChainDict) -> None:
643 """Tests the `returns` argument of `subset`.
645 Args:
646 chain: `ChainDict` with two mappings that share a key.
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})
656 def test_mutable_mapping_methods(self, chain: ChainDict) -> None:
657 """Tests methods inherited from `MutableMapping`.
659 Args:
660 chain: `ChainDict` with two mappings that share a key.
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
668 def test_new_child_defaults(self, chain: ChainDict) -> None:
669 """Tests `new_child` without a mapping and with a plain `dict`.
671 Args:
672 chain: `ChainDict` with two mappings that share a key.
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)
683 def test_get(self, chain: ChainDict) -> None:
684 """Tests `get` with and without defaults.
686 Args:
687 chain: `ChainDict` with two mappings that share a key.
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'
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
702 def test_add_operators(self, chain: ChainDict) -> None:
703 """Tests `+` and `+=`.
705 Args:
706 chain: `ChainDict` with two mappings that share a key.
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
716 def test_contains(self, chain: ChainDict) -> None:
717 """Tests that `in` checks every stored mapping.
719 Args:
720 chain: `ChainDict` with two mappings that share a key.
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()
729 def test_parents_is_property(self, chain: ChainDict) -> None:
730 """Tests that `parents` is a property, as in `collections.ChainMap`.
732 Args:
733 chain: `ChainDict` with two mappings that share a key.
735 """
736 assert isinstance(ChainDict.parents, property)
737 assert chain.parents.keys() == ('a', 'c')
738 assert chain.parents.parents.contents == []
741class TestRepository:
742 """Tests for `Repository`."""
744 def test_init(self) -> None:
745 """Tests defaults."""
746 repository = Repository()
747 assert repository.contents == {}
748 assert repository.overwrite is False
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')
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')
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')
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
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
793 def test_get_name_override(self) -> None:
794 """Tests that a subclass can change how names are inferred."""
796 class Numbered(Repository):
797 """`Repository` that names items by their position."""
799 def _get_name(self, item: object) -> str:
800 """Returns a name based on the number of stored items.
802 Args:
803 item: ignored.
805 Returns:
806 'item_' followed by the number of stored items.
808 """
809 return f'item_{len(self)}'
811 repository = Numbered()
812 repository.add('a')
813 repository.add('b')
814 assert repository.keys() == ('item_0', 'item_1')
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'
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')
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',)
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() == ('',)
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',)
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')