Skip to content

Commit 43eab3e

Browse files
authored
67 add observable collections (#72)
* Add observable collections, which can be reduced to Values * Add context manager to Variable, allowing setting it, but delaying notifying listeners * Add ValuesObservable and ValuesEvent, which allows observing iterables, either as an Iterable or as each individual element * Add VoidObservables, which can not be triggered and are instantiated as a singleton * Allow Values to be derived with transforming function * Allow deriving Observables from other Observables with a mapping function * Allow Events to emit lazily, so values don't have to be instantiated if they are not observed
1 parent 77976c4 commit 43eab3e

58 files changed

Lines changed: 5769 additions & 286 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/spellbind/actions.py

Lines changed: 579 additions & 0 deletions
Large diffs are not rendered by default.

src/spellbind/bool_values.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -146,10 +146,6 @@ def of(cls, value: bool) -> BoolConstant:
146146
def logical_not(self) -> BoolConstant:
147147
return BoolConstant.of(not self.value)
148148

149-
@property
150-
def constant_value_or_raise(self) -> bool:
151-
return self.value
152-
153149

154150
class BoolVariable(SimpleVariable[bool], BoolValue):
155151
pass

src/spellbind/deriveds.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
from __future__ import annotations
2+
3+
from abc import ABC, abstractmethod
4+
from typing import Iterable
5+
6+
7+
class Derived(ABC):
8+
@property
9+
@abstractmethod
10+
def derived_from(self) -> frozenset[Derived]: ...
11+
12+
@property
13+
def deep_derived_from(self) -> Iterable[Derived]:
14+
found_derived = set()
15+
derive_queue = [self]
16+
17+
while derive_queue:
18+
current = derive_queue.pop(0)
19+
for dependency in current.derived_from:
20+
if dependency not in found_derived:
21+
found_derived.add(dependency)
22+
yield dependency
23+
derive_queue.append(dependency)
24+
25+
def is_derived_from(self, derived: Derived) -> bool:
26+
for dependency in self.deep_derived_from:
27+
if derived is dependency:
28+
return True
29+
return False

src/spellbind/emitters.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
from abc import ABC, abstractmethod
2-
from typing import TypeVar, Generic
3-
2+
from typing import TypeVar, Generic, Iterable
43

54
T = TypeVar("T")
65
U = TypeVar("U")
@@ -14,7 +13,7 @@ def __call__(self) -> None: ...
1413

1514
class ValueEmitter(Generic[T], ABC):
1615
@abstractmethod
17-
def __call__(self, value0: T) -> None: ...
16+
def __call__(self, value: T) -> None: ...
1817

1918

2019
class BiEmitter(Generic[T, U], ABC):
@@ -25,3 +24,8 @@ def __call__(self, value0: T, value1: U) -> None: ...
2524
class TriEmitter(Generic[T, U, S], ABC):
2625
@abstractmethod
2726
def __call__(self, value0: T, value1: U, value2: S) -> None: ...
27+
28+
29+
class ValuesEmitter(Generic[T], ABC):
30+
@abstractmethod
31+
def __call__(self, values: Iterable[T]) -> None: ...

src/spellbind/event.py

Lines changed: 29 additions & 51 deletions
Original file line numberDiff line numberDiff line change
@@ -1,82 +1,60 @@
1-
from abc import ABC, abstractmethod
2-
from typing import Callable, TypeVar, Generic
1+
from typing import Callable, TypeVar, Generic, Iterable, Sequence
32

4-
from spellbind.emitters import Emitter, TriEmitter, BiEmitter, ValueEmitter
5-
from spellbind.functions import assert_parameter_max_count
3+
from spellbind.emitters import Emitter, TriEmitter, BiEmitter, ValueEmitter, ValuesEmitter
64
from spellbind.observables import Observable, ValueObservable, BiObservable, TriObservable, Observer, \
7-
ValueObserver, BiObserver, TriObserver, Subscription, WeakSubscription, StrongSubscription, \
8-
RemoveSubscriptionError
5+
ValueObserver, BiObserver, TriObserver, ValuesObserver, ValuesObservable, _BaseObservable, _BaseValuesObservable, \
6+
_SingleBaseObservable
97

108
_S = TypeVar("_S")
119
_T = TypeVar("_T")
1210
_U = TypeVar("_U")
1311
_O = TypeVar('_O', bound=Callable)
1412

1513

16-
class _BaseEvent(Generic[_O], ABC):
17-
_subscriptions: list[Subscription[_O]]
18-
19-
def __init__(self):
20-
self._subscriptions = []
21-
22-
@abstractmethod
23-
def _get_parameter_count(self) -> int: ...
24-
25-
def observe(self, observer: _O, times: int | None = None) -> None:
26-
assert_parameter_max_count(observer, self._get_parameter_count())
27-
self._subscriptions.append(StrongSubscription(observer, times))
28-
29-
def weak_observe(self, observer: _O, times: int | None = None) -> None:
30-
assert_parameter_max_count(observer, self._get_parameter_count())
31-
self._subscriptions.append(WeakSubscription(observer, times))
32-
33-
def unobserve(self, observer: _O) -> None:
34-
for i, sub in enumerate(self._subscriptions):
35-
if sub.matches_observer(observer):
36-
del self._subscriptions[i]
37-
return
38-
raise ValueError(f"Observer {observer} is not subscribed to this event.")
39-
40-
def is_observed(self, observer: _O) -> bool:
41-
return any(sub.matches_observer(observer) for sub in self._subscriptions)
42-
43-
def _emit(self, *args) -> None:
44-
i = 0
45-
while i < len(self._subscriptions):
46-
try:
47-
self._subscriptions[i](*args)
48-
i += 1
49-
except RemoveSubscriptionError:
50-
del self._subscriptions[i]
51-
52-
53-
class Event(_BaseEvent[Observer], Observable, Emitter):
14+
class Event(_BaseObservable[Observer], Observable, Emitter):
5415
def _get_parameter_count(self) -> int:
5516
return 0
5617

5718
def __call__(self) -> None:
58-
self._emit()
19+
self._emit_nothing()
5920

6021

61-
class ValueEvent(Generic[_S], _BaseEvent[Observer | ValueObserver[_S]], ValueObservable[_S], ValueEmitter[_S]):
22+
class ValueEvent(Generic[_S], _SingleBaseObservable[Observer | ValueObserver[_S]], ValueObservable[_S], ValueEmitter[_S]):
6223
def _get_parameter_count(self) -> int:
6324
return 1
6425

6526
def __call__(self, value: _S) -> None:
66-
self._emit(value)
27+
self._emit_single(value)
6728

29+
def emit_lazy(self, func: Callable[[], _S]) -> None:
30+
self._emit_single_lazy(func)
6831

69-
class BiEvent(Generic[_S, _T], _BaseEvent[Observer | ValueObserver[_S] | BiObserver[_S, _T]], BiObservable[_S, _T], BiEmitter[_S, _T]):
32+
33+
class BiEvent(Generic[_S, _T], _BaseObservable[Observer | ValueObserver[_S] | BiObserver[_S, _T]], BiObservable[_S, _T], BiEmitter[_S, _T]):
7034
def _get_parameter_count(self) -> int:
7135
return 2
7236

7337
def __call__(self, value_0: _S, value_1: _T) -> None:
74-
self._emit(value_0, value_1)
38+
self._emit_n((value_0, value_1))
7539

7640

77-
class TriEvent(Generic[_S, _T, _U], _BaseEvent[Observer | ValueObserver[_S] | BiObserver[_S, _T] | TriObserver[_S, _T, _U]], TriObservable[_S, _T, _U], TriEmitter[_S, _T, _U]):
41+
class TriEvent(Generic[_S, _T, _U],
42+
_BaseObservable[Observer | ValueObserver[_S] | BiObserver[_S, _T] | TriObserver[_S, _T, _U]],
43+
TriObservable[_S, _T, _U],
44+
TriEmitter[_S, _T, _U]):
7845
def _get_parameter_count(self) -> int:
7946
return 3
8047

8148
def __call__(self, value_0: _S, value_1: _T, value_2: _U) -> None:
82-
self._emit(value_0, value_1, value_2)
49+
self._emit_n((value_0, value_1, value_2))
50+
51+
52+
class ValuesEvent(Generic[_S], _BaseValuesObservable[Observer | ValuesObserver[_S]], ValuesObservable[_S], ValuesEmitter[_S]):
53+
def __call__(self, value: Iterable[_S]) -> None:
54+
self._emit_single(value)
55+
56+
def emit_single(self, value: _S) -> None:
57+
self._emit_single((value,))
58+
59+
def emit_lazy(self, func: Callable[[], Sequence[_S]]) -> None:
60+
self._emit_single_lazy(func)

src/spellbind/float_values.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,6 @@
2323
_U = TypeVar("_U")
2424

2525

26-
_COMMUTATIVE_OPERATORS = {
27-
operator.add, sum, _multiply_all_floats, max, min
28-
}
29-
30-
3126
def _average_float(values: Sequence[float]) -> float:
3227
return sum(values) / len(values)
3328

@@ -367,3 +362,6 @@ def __neg__(self) -> FloatValue:
367362
class CompareNumbersValues(TwoFloatsToOneValue[bool], BoolValue):
368363
def __init__(self, left: FloatLike, right: FloatLike, op: Callable[[float, float], bool]):
369364
super().__init__(op, left, right)
365+
366+
367+
ZERO = FloatConstant.of(0.)

src/spellbind/functions.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ def _is_positional_parameter(param: Parameter) -> bool:
77
return param.kind in (Parameter.POSITIONAL_ONLY, Parameter.POSITIONAL_OR_KEYWORD)
88

99

10+
def has_var_args(function: Callable) -> bool:
11+
parameters = inspect.signature(function).parameters
12+
return any(param.kind == Parameter.VAR_POSITIONAL for param in parameters.values())
13+
14+
1015
def count_positional_parameters(function: Callable) -> int:
1116
parameters = inspect.signature(function).parameters
1217
return sum(1 for parameter in parameters.values() if _is_positional_parameter(parameter))

src/spellbind/int_collections.py

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
from __future__ import annotations
2+
3+
import operator
4+
from abc import ABC, abstractmethod
5+
from functools import cached_property
6+
from typing import Iterable, Callable, Any
7+
8+
from typing_extensions import TypeIs
9+
10+
from spellbind.int_values import IntValue, IntConstant
11+
from spellbind.observable_collections import ObservableCollection, ReducedValue, CombinedValue, ValueCollection
12+
from spellbind.observable_sequences import ObservableList, _S, TypedValueList, ValueSequence, UnboxedValueSequence, \
13+
ObservableSequence
14+
from spellbind.values import Value
15+
16+
17+
class ObservableIntCollection(ObservableCollection[int], ABC):
18+
@property
19+
def summed(self) -> IntValue:
20+
return self.reduce_to_int(add_reducer=operator.add, remove_reducer=operator.sub, initial=0)
21+
22+
@property
23+
def multiplied(self) -> IntValue:
24+
return self.reduce_to_int(add_reducer=operator.mul, remove_reducer=operator.floordiv, initial=1)
25+
26+
27+
class ObservableIntSequence(ObservableSequence[int], ObservableIntCollection, ABC):
28+
pass
29+
30+
31+
class ObservableIntList(ObservableList[int], ObservableIntSequence):
32+
pass
33+
34+
35+
class IntValueCollection(ValueCollection[int], ABC):
36+
@property
37+
def summed(self) -> IntValue:
38+
return self.unboxed.reduce_to_int(add_reducer=operator.add, remove_reducer=operator.sub, initial=0)
39+
40+
@property
41+
@abstractmethod
42+
def unboxed(self) -> ObservableIntCollection: ...
43+
44+
45+
class CombinedIntValue(CombinedValue[int], IntValue):
46+
def __init__(self, collection: ObservableCollection[_S], combiner: Callable[[Iterable[_S]], int]):
47+
super().__init__(collection=collection, combiner=combiner)
48+
49+
50+
class ReducedIntValue(ReducedValue[int], IntValue):
51+
def __init__(self,
52+
collection: ObservableCollection[_S],
53+
add_reducer: Callable[[int, _S], int],
54+
remove_reducer: Callable[[int, _S], int],
55+
initial: int):
56+
super().__init__(collection=collection,
57+
add_reducer=add_reducer,
58+
remove_reducer=remove_reducer,
59+
initial=initial)
60+
61+
62+
class UnboxedIntValueSequence(UnboxedValueSequence[int], ObservableIntSequence):
63+
def __init__(self, sequence: IntValueSequence):
64+
super().__init__(sequence)
65+
66+
67+
class IntValueSequence(ValueSequence[int], IntValueCollection, ABC):
68+
@cached_property
69+
def unboxed(self) -> ObservableIntSequence:
70+
return UnboxedIntValueSequence(self)
71+
72+
73+
class IntValueList(TypedValueList[int], IntValueSequence):
74+
def __init__(self, values: Iterable[int | Value[int]] | None = None):
75+
def is_int(value: Any) -> TypeIs[int]:
76+
return isinstance(value, int)
77+
super().__init__(values, checker=is_int, constant_factory=IntConstant.of)

src/spellbind/int_values.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,3 +252,6 @@ def __neg__(self) -> IntValue:
252252
if isinstance(of, IntValue):
253253
return of
254254
return super().__neg__()
255+
256+
257+
ZERO = IntConstant.of(0)

0 commit comments

Comments
 (0)