Skip to content

Commit efd57c5

Browse files
committed
Add classes StaticObservableSequence and FrozenObservableSequence
1 parent 4a7325a commit efd57c5

File tree

2 files changed

+182
-2
lines changed

2 files changed

+182
-2
lines changed

src/spellbind/observable_sequences.py

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from abc import ABC, abstractmethod
44
from functools import cached_property
55
from typing import Sequence, Generic, MutableSequence, Iterable, overload, SupportsIndex, Callable, Iterator, \
6-
TypeVar, Any
6+
TypeVar, Any, Hashable
77

88
from typing_extensions import TypeIs, Self, override
99

@@ -16,7 +16,7 @@
1616
clear_action, DeltaAction, SimpleRemoveOneAction, SimpleAddOneAction, ElementsChangedAction, \
1717
SimpleOneElementChangedAction
1818
from spellbind.event import ValueEvent
19-
from spellbind.int_values import IntVariable, IntValue
19+
from spellbind.int_values import IntVariable, IntValue, IntConstant
2020
from spellbind.observable_collections import ObservableCollection, ValueCollection
2121
from spellbind.observables import ValueObservable, ValuesObservable, void_value_observable, void_values_observable, \
2222
combine_values_observables, combine_value_observables
@@ -25,6 +25,7 @@
2525
_S = TypeVar("_S")
2626
_S_co = TypeVar("_S_co", covariant=True)
2727
_T = TypeVar("_T")
28+
_H = TypeVar("_H", bound=Hashable)
2829

2930

3031
class ObservableSequence(Sequence[_S_co], ObservableCollection[_S_co], Generic[_S_co], ABC):
@@ -973,6 +974,76 @@ def __str__(self) -> str:
973974
return "[]"
974975

975976

977+
class StaticObservableSequence(IndexObservableSequence[_S], Generic[_S]):
978+
_on_change: ValueObservable[AtIndicesDeltasAction[_S] | ClearAction[_S] | ReverseAction[_S]]
979+
_delta_observable: ValuesObservable[AtIndexDeltaAction[_S]]
980+
981+
def __init__(self, iterable: Iterable[_S] = ()) -> None:
982+
self._sequence = tuple(iterable)
983+
self._on_change = void_value_observable()
984+
self._delta_observable = void_values_observable()
985+
self._length_value = IntConstant.of(len(self._sequence))
986+
987+
@property
988+
@override
989+
def on_change(self) -> ValueObservable[AtIndicesDeltasAction[_S] | ClearAction[_S] | ReverseAction[_S]]:
990+
return self._on_change
991+
992+
@property
993+
@override
994+
def delta_observable(self) -> ValuesObservable[AtIndexDeltaAction[_S]]:
995+
return self._delta_observable
996+
997+
@property
998+
@override
999+
def length_value(self) -> IntValue:
1000+
return self._length_value
1001+
1002+
@overload
1003+
@override
1004+
def __getitem__(self, index: int) -> _S: ...
1005+
1006+
@overload
1007+
@override
1008+
def __getitem__(self, index: slice) -> Sequence[_S]: ...
1009+
1010+
@override
1011+
def __getitem__(self, index: int | slice) -> _S | Sequence[_S]:
1012+
return self._sequence[index]
1013+
1014+
@override
1015+
def __iter__(self) -> Iterator[_S]:
1016+
return iter(self._sequence)
1017+
1018+
@override
1019+
def __len__(self) -> int:
1020+
return len(self._sequence)
1021+
1022+
@override
1023+
def __str__(self) -> str:
1024+
return "[" + ", ".join(repr(item) for item in self._sequence) + "]"
1025+
1026+
@override
1027+
def __repr__(self) -> str:
1028+
return f"{self.__class__.__name__}({self._sequence!r})"
1029+
1030+
@override
1031+
def __eq__(self, other: object) -> bool:
1032+
if isinstance(other, FrozenObservableSequence):
1033+
return self._sequence == other._sequence
1034+
return super().__eq__(other)
1035+
1036+
1037+
class FrozenObservableSequence(StaticObservableSequence[_H], Generic[_H]):
1038+
def __init__(self, iterable: Iterable[_H] = ()) -> None:
1039+
super().__init__(iterable)
1040+
self._hash = hash(self._sequence) # ensure fast-fail for non hashable elements, like frozenset does
1041+
1042+
@override
1043+
def __hash__(self) -> int:
1044+
return self._hash
1045+
1046+
9761047
EMPTY_SEQUENCE: IndexObservableSequence[Any] = _EmptyObservableSequence()
9771048

9781049

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import pytest
2+
3+
from spellbind.observable_sequences import FrozenObservableSequence, ObservableList, StaticObservableSequence
4+
5+
6+
def test_initialize_frozen_sequence_empty_str():
7+
sequence = StaticObservableSequence()
8+
assert str(sequence) == "[]"
9+
10+
11+
def test_initialize_frozen_sequence_empty_is_empty():
12+
sequence = StaticObservableSequence()
13+
assert len(sequence) == 0
14+
assert sequence.length_value.value == 0
15+
assert sequence.length_value.constant_value_or_raise == 0
16+
assert list(sequence) == []
17+
18+
19+
def test_initialize_frozen_sequence_empty_observers_not_observed():
20+
sequence = StaticObservableSequence()
21+
assert not sequence.on_change.is_observed()
22+
assert not sequence.delta_observable.is_observed()
23+
24+
sequence.on_change.observe(lambda _: None)
25+
assert not sequence.on_change.is_observed()
26+
27+
sequence.delta_observable.observe(lambda _: None)
28+
assert not sequence.delta_observable.is_observed()
29+
30+
31+
def test_static_sequence_str():
32+
sequence = StaticObservableSequence([1, 2, 3])
33+
assert str(sequence) == "[1, 2, 3]"
34+
35+
36+
def test_static_sequence_length():
37+
sequence = StaticObservableSequence([1, 2, 3])
38+
assert len(sequence) == 3
39+
assert sequence.length_value.value == 3
40+
assert sequence.length_value.constant_value_or_raise == 3
41+
42+
43+
def test_static_sequence_get_item():
44+
sequence = StaticObservableSequence([1, 2, 3])
45+
assert sequence[0] == 1
46+
assert sequence[1] == 2
47+
assert sequence[2] == 3
48+
49+
50+
def test_static_sequence_iter():
51+
sequence = StaticObservableSequence([1, 2, 3])
52+
assert list(sequence) == [1, 2, 3]
53+
54+
55+
def test_static_sequence_contains():
56+
sequence = StaticObservableSequence([1, 2, 3])
57+
assert 1 in sequence
58+
assert 2 in sequence
59+
assert 3 in sequence
60+
assert 4 not in sequence
61+
assert "test" not in sequence
62+
63+
64+
def test_static_sequence_has_no_append():
65+
sequence = StaticObservableSequence([1, 2, 3])
66+
with pytest.raises(AttributeError):
67+
sequence.append(4)
68+
69+
70+
def test_static_sequence_has_no_remove():
71+
sequence = StaticObservableSequence([1, 2, 3])
72+
with pytest.raises(AttributeError):
73+
sequence.remove(2)
74+
75+
76+
def test_static_sequence_equals_true():
77+
seq1 = StaticObservableSequence([1, 2, 3])
78+
seq2 = StaticObservableSequence([1, 2, 3])
79+
assert seq1 == seq2
80+
81+
82+
def test_static_sequence_equals_false():
83+
seq1 = StaticObservableSequence([1, 2, 3])
84+
seq2 = StaticObservableSequence([1, 2, 4])
85+
assert seq1 != seq2
86+
87+
88+
def test_static_sequence_equals_observable_list_true():
89+
seq1 = StaticObservableSequence([1, 2, 3])
90+
seq2 = ObservableList([1, 2, 3])
91+
assert seq1 == seq2
92+
93+
94+
def test_static_sequence_equals_observable_list_false():
95+
seq1 = StaticObservableSequence([1, 2, 3])
96+
seq2 = ObservableList([1, 2, 4])
97+
assert seq1 != seq2
98+
99+
100+
def test_frozen_sequence_hash_equal():
101+
seq1 = FrozenObservableSequence([1, 2, 3])
102+
seq2 = FrozenObservableSequence([1, 2, 3])
103+
assert hash(seq1) == hash(seq2)
104+
105+
106+
def test_frozen_sequence_hash_not_equal():
107+
seq1 = FrozenObservableSequence([1, 2, 3])
108+
seq2 = FrozenObservableSequence([1, 2, 4])
109+
assert hash(seq1) != hash(seq2)

0 commit comments

Comments
 (0)