288 lines
9.0 KiB
Python
288 lines
9.0 KiB
Python
from __future__ import annotations
|
|
|
|
from collections.abc import Mapping
|
|
from collections.abc import MutableMapping
|
|
from collections.abc import MutableSequence
|
|
from collections.abc import MutableSet
|
|
from typing import Any
|
|
|
|
import pytest
|
|
|
|
from hooked_containers import events as e
|
|
from hooked_containers.mapping import HookedMapping
|
|
from hooked_containers.mixins import ListMixin
|
|
from hooked_containers.mixins import MutableMappingMixin
|
|
from hooked_containers.mixins import MutableSetMixin
|
|
from hooked_containers.sequence import HookedList
|
|
from hooked_containers.set import HookedSet
|
|
|
|
|
|
class TestHookedList:
|
|
class TestConstruction:
|
|
def test_empty(self):
|
|
lst = HookedList([])
|
|
assert list(lst) == []
|
|
|
|
def test_with_items(self):
|
|
lst = HookedList([1, 2, 3])
|
|
assert list(lst) == [1, 2, 3]
|
|
|
|
def test_recreation(self):
|
|
initial = [1, 2, 3]
|
|
|
|
lst = HookedList(initial)
|
|
assert lst._storage is not initial
|
|
|
|
lst2 = HookedList(lst)
|
|
assert lst2._storage is not lst._storage
|
|
assert lst2 == initial
|
|
|
|
def test_recreation_does_not_duplicate_hooks(self):
|
|
original_changes = []
|
|
changes = []
|
|
original = HookedList([1], original_changes.append)
|
|
lst = HookedList(original, changes.append)
|
|
|
|
lst.append(2)
|
|
assert lst._storage is not original._storage
|
|
assert original._storage == [1]
|
|
assert original_changes == []
|
|
assert len(changes) == 1
|
|
assert changes == [e.Change("add", 1, 2)]
|
|
|
|
class TestSeqOps:
|
|
def test_len(self):
|
|
lst = HookedList([1, 2, 3])
|
|
assert len(lst) == 3
|
|
|
|
def test_getitem(self):
|
|
lst = HookedList([1, 2, 3])
|
|
assert lst[0] == 1
|
|
assert lst[1] == 2
|
|
assert lst[-1] == 3
|
|
|
|
def test_contains(self):
|
|
lst = HookedList([1, 2, 3])
|
|
assert 2 in lst
|
|
assert 4 not in lst
|
|
|
|
def test_iter(self):
|
|
lst = HookedList([1, 2, 3])
|
|
for i, item in enumerate(lst, start=1):
|
|
assert item == i
|
|
|
|
class TestMutableOps:
|
|
def test_setitem(self):
|
|
added = []
|
|
lst = HookedList[Any](
|
|
[1, 2, [4, 5, [6, 7]]],
|
|
lambda event: added.append(event.value),
|
|
)
|
|
lst[0] = 10
|
|
lst.append(20)
|
|
assert added == [10, 20]
|
|
|
|
lst[2][-1].append(8)
|
|
assert added == [10, 20]
|
|
|
|
def test_delitem(self):
|
|
lst = HookedList([1, 2, 3])
|
|
del lst[1]
|
|
assert list(lst) == [1, 3]
|
|
|
|
def test_insert(self):
|
|
lst = HookedList([1, 3])
|
|
lst.insert(1, 2)
|
|
assert list(lst) == [1, 2, 3]
|
|
|
|
def test_inherited_mutation_helpers(self):
|
|
changes = []
|
|
lst = HookedList([1], changes.append)
|
|
|
|
lst.extend([2, 3])
|
|
assert lst.pop() == 3
|
|
lst.remove(1)
|
|
assert list(lst) == [2]
|
|
assert changes == [
|
|
e.Change("add", 1, 2),
|
|
e.Change("add", 2, 3),
|
|
e.Change("remove", 2, 3),
|
|
e.Change("remove", 0, 1),
|
|
]
|
|
|
|
def test_slice_mutations(self):
|
|
changes = []
|
|
lst = HookedList([1, 2, 3], changes.append)
|
|
|
|
lst[1:3] = iter([4, 5])
|
|
assert list(lst) == [1, 4, 5]
|
|
del lst[::2]
|
|
assert list(lst) == [4]
|
|
assert changes == [
|
|
e.Change("set", slice(1, 3), [4, 5]),
|
|
e.Change("remove", slice(None, None, 2), [1, 5]),
|
|
]
|
|
|
|
def test_failed_mutations_do_not_notify(self):
|
|
changes = []
|
|
lst = HookedList([1, 2, 3], changes.append)
|
|
|
|
with pytest.raises(ValueError):
|
|
lst[::2] = [4]
|
|
with pytest.raises(IndexError):
|
|
del lst[10]
|
|
assert list(lst) == [1, 2, 3]
|
|
assert changes == []
|
|
|
|
def test_nested_values_are_not_observed(self):
|
|
changes = []
|
|
lst = HookedList([[1]], changes.append)
|
|
nested = lst[0]
|
|
|
|
nested[0] = 2
|
|
nested.insert(1, 3)
|
|
del nested[0]
|
|
assert lst._storage == [[3]]
|
|
assert changes == []
|
|
|
|
def test_native_comparisons_and_non_mutating_results(self):
|
|
changes = []
|
|
items = HookedList([1, 2], changes.append)
|
|
assert items == [1, 2]
|
|
native = [1, 2]
|
|
assert native == items
|
|
assert items != (1, 2)
|
|
assert items < [1, 3]
|
|
assert items <= HookedList([1, 2])
|
|
assert items > [0]
|
|
assert items >= [1, 2]
|
|
with pytest.raises(TypeError):
|
|
assert items < (1, 3)
|
|
with pytest.raises(TypeError):
|
|
_ = items + (3,)
|
|
for result in (items[:], items.copy(), items + [3], [0] + items, items * 2, 2 * items):
|
|
assert type(result) is list
|
|
result.append(99)
|
|
assert items == [1, 2]
|
|
assert changes == []
|
|
|
|
def test_index_protocol_and_normalized_indices(self):
|
|
class Index:
|
|
def __index__(self):
|
|
return -1
|
|
|
|
changes = []
|
|
items = HookedList([1, 2], changes.append)
|
|
assert items[Index()] == 2
|
|
items[Index()] = 3
|
|
items.insert(-100, 0)
|
|
items.insert(100, 4)
|
|
del items[Index()]
|
|
assert items == [0, 1, 3]
|
|
assert changes == [
|
|
e.Change("set", 1, 3),
|
|
e.Change("add", 0, 0),
|
|
e.Change("add", 3, 4),
|
|
e.Change("remove", 3, 4),
|
|
]
|
|
|
|
def test_self_extension_and_in_place_repetition(self):
|
|
changes = []
|
|
items = HookedList([1, 2], changes.append)
|
|
original = items
|
|
items += items
|
|
assert items is original
|
|
assert items == [1, 2, 1, 2]
|
|
assert changes == [e.Change("add", 2, 1), e.Change("add", 3, 2)]
|
|
changes.clear()
|
|
items *= 2
|
|
assert items is original
|
|
assert items == [1, 2] * 4
|
|
assert changes == [e.Change("set", slice(None), [1, 2] * 4)]
|
|
changes.clear()
|
|
items *= 1
|
|
assert changes == []
|
|
items *= 0
|
|
assert items == []
|
|
assert changes == [e.Change("set", slice(None), [])]
|
|
|
|
def test_reverse_sort_and_empty_writes(self):
|
|
changes = []
|
|
items = HookedList([3, 1, 2], changes.append)
|
|
items.reverse()
|
|
assert items == [2, 1, 3]
|
|
assert changes == [e.Change("set", 0, 2), e.Change("set", 2, 3)]
|
|
changes.clear()
|
|
items.sort(reverse=True)
|
|
assert items == [3, 2, 1]
|
|
assert changes == [e.Change("set", slice(None), [3, 2, 1])]
|
|
changes.clear()
|
|
del items[0:0]
|
|
items[0:0] = []
|
|
items.extend([])
|
|
assert changes == []
|
|
items.clear()
|
|
assert [change.value for change in changes] == [1, 2, 3]
|
|
changes.clear()
|
|
items.clear()
|
|
items.reverse()
|
|
items.sort()
|
|
assert changes == []
|
|
|
|
def test_partial_extend_and_sort_failure(self):
|
|
def values():
|
|
yield 2
|
|
raise RuntimeError("iteration failed")
|
|
|
|
changes = []
|
|
items = HookedList([1], changes.append)
|
|
with pytest.raises(RuntimeError, match="iteration failed"):
|
|
items.extend(values())
|
|
assert items == [1, 2]
|
|
assert changes == [e.Change("add", 1, 2)]
|
|
changes.clear()
|
|
|
|
def fail(_value):
|
|
raise ValueError("key failed")
|
|
|
|
with pytest.raises(ValueError, match="key failed"):
|
|
items.sort(key=fail)
|
|
assert items == [1, 2]
|
|
assert changes == []
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("mixin", "factory", "mutate", "expected"),
|
|
[
|
|
(MutableMappingMixin, dict, lambda container: container.update(a=1), e.Change("add", "a", 1)),
|
|
(ListMixin, list, lambda container: container.append(1), e.Change("add", 0, 1)),
|
|
(MutableSetMixin, set, lambda container: container.add(1), e.Change("add", None, 1)),
|
|
],
|
|
)
|
|
def test_mixins_work_without_concrete_wrappers(mixin, factory, mutate, expected):
|
|
class Custom(mixin):
|
|
def __init__(self):
|
|
self._storage = factory()
|
|
|
|
changes = []
|
|
container = Custom()
|
|
container.hook = changes.append
|
|
mutate(container)
|
|
assert changes == [expected]
|
|
|
|
|
|
def test_helpers_are_inherited_from_standard_abcs():
|
|
assert HookedList.append is MutableSequence.append
|
|
assert HookedList.extend is MutableSequence.extend
|
|
assert HookedList.reverse is MutableSequence.reverse
|
|
assert HookedList.__iadd__ is MutableSequence.__iadd__
|
|
assert HookedMapping.get is Mapping.get
|
|
assert HookedMapping.keys is Mapping.keys
|
|
assert HookedMapping.update is MutableMapping.update
|
|
assert HookedSet.pop is MutableSet.pop
|
|
assert HookedSet.remove is MutableSet.remove
|
|
assert HookedSet.__ior__ is MutableSet.__ior__
|
|
assert HookedSet.__iand__ is MutableSet.__iand__
|
|
assert HookedSet.__ixor__ is MutableSet.__ixor__
|
|
assert HookedSet.__isub__ is MutableSet.__isub__
|