11from __future__ import annotations
22
33import functools
4+ import logging
45from abc import ABC , abstractmethod
5- from typing import TypeVar , Generic , Collection , Callable , Iterable , Iterator , Any
6+ from typing import TypeVar , Generic , Collection , Callable , Iterable , Iterator , Any , TYPE_CHECKING
67
78from typing_extensions import override
89
1112from spellbind .bool_values import BoolValue
1213from spellbind .deriveds import Derived
1314from spellbind .event import BiEvent , ValueEvent
15+ from spellbind .float_values import FloatValue
1416from spellbind .int_values import IntValue , IntVariable
1517from spellbind .observables import ValuesObservable , ValueObservable , Observer , ValueObserver , BiObserver , \
1618 Subscription
1719from spellbind .str_values import StrValue
1820from spellbind .values import Value , EMPTY_FROZEN_SET
1921
22+ if TYPE_CHECKING :
23+ from spellbind .float_collections import ObservableFloatCollection
24+ from spellbind .int_collections import ObservableIntCollection
25+
26+
2027_S = TypeVar ("_S" )
2128_S_co = TypeVar ("_S_co" , covariant = True )
2229_T = TypeVar ("_T" )
2330
31+ _logger = logging .getLogger (__name__ )
32+
2433
2534class ObservableCollection (Collection [_S_co ], Generic [_S_co ], ABC ):
2635 @property
@@ -56,6 +65,11 @@ def combine_to_int(self, combiner: Callable[[Iterable[_S_co]], int]) -> IntValue
5665
5766 return CombinedIntValue (self , combiner = combiner )
5867
68+ def combine_to_float (self , combiner : Callable [[Iterable [_S_co ]], float ]) -> 'FloatValue' :
69+ from spellbind .float_collections import CombinedFloatValue
70+
71+ return CombinedFloatValue (self , combiner = combiner )
72+
5973 def reduce (self ,
6074 add_reducer : Callable [[_T , _S_co ], _T ],
6175 remove_reducer : Callable [[_T , _S_co ], _T ],
@@ -79,17 +93,41 @@ def reduce_to_str(self,
7993 def reduce_to_int (self ,
8094 add_reducer : Callable [[int , _S_co ], int ],
8195 remove_reducer : Callable [[int , _S_co ], int ],
82- initial : int ) -> IntValue :
96+ initial : int = 0 ) -> IntValue :
8397 from spellbind .int_collections import ReducedIntValue
8498
8599 return ReducedIntValue (self ,
86100 add_reducer = add_reducer ,
87101 remove_reducer = remove_reducer ,
88102 initial = initial )
89103
90- def filter_to_bag (self , predicate : Callable [[_S_co ], bool ]) -> FilteredObservableBag [_S_co ]:
104+ def reduce_to_float (self ,
105+ add_reducer : Callable [[float , _S_co ], float ],
106+ remove_reducer : Callable [[float , _S_co ], float ],
107+ initial : float = 0. ) -> FloatValue :
108+ from spellbind .float_collections import ReducedFloatValue
109+
110+ return ReducedFloatValue (self ,
111+ add_reducer = add_reducer ,
112+ remove_reducer = remove_reducer ,
113+ initial = initial )
114+
115+ def filter_to_bag (self , predicate : Callable [[_S_co ], bool ]) -> ObservableCollection [_S_co ]:
91116 return FilteredObservableBag (self , predicate )
92117
118+ def map (self , transform : Callable [[_S_co ], _T ]) -> ObservableCollection [_T ]:
119+ return MappedObservableBag (self , transform )
120+
121+ def map_to_float (self , transform : Callable [[_S_co ], float ]) -> ObservableFloatCollection :
122+ from spellbind .float_collections import MappedToFloatBag
123+
124+ return MappedToFloatBag (self , transform )
125+
126+ def map_to_int (self , transform : Callable [[_S_co ], int ]) -> ObservableIntCollection :
127+ from spellbind .int_collections import MappedToIntBag
128+
129+ return MappedToIntBag (self , transform )
130+
93131
94132class ReducedValue (Value [_S ], Generic [_S ]):
95133 def __init__ (self ,
@@ -156,18 +194,6 @@ def is_observed(self, by: Callable[..., Any] | None = None) -> bool:
156194
157195
158196class ValueCollection (ObservableCollection [Value [_S ]], Generic [_S ], ABC ):
159- @override
160- def reduce_to_int (self ,
161- add_reducer : Callable [[int , Value [_S ]], int ],
162- remove_reducer : Callable [[int , Value [_S ]], int ],
163- initial : int ) -> IntValue :
164- from spellbind .int_collections import ReducedIntValue
165-
166- return ReducedIntValue (self ,
167- add_reducer = add_reducer ,
168- remove_reducer = remove_reducer ,
169- initial = initial )
170-
171197 def value_iterable (self ) -> Iterable [_S ]:
172198 return (value .value for value in self )
173199
@@ -281,6 +307,52 @@ def _clear(self) -> None:
281307 self ._action_event (clear_action ())
282308
283309
310+ class MappedObservableBag (_ObservableBagBase [_S ], Generic [_S ]):
311+ def __init__ (self , source : ObservableCollection [_T ], transform : Callable [[_T ], _S ]) -> None :
312+ super ().__init__ (tuple (transform (item ) for item in source ))
313+ self ._source = source
314+ self ._transform = transform
315+
316+ self ._source .on_change .observe (self ._on_source_action )
317+
318+ def _on_source_action (self , action : CollectionAction [Any ]) -> None :
319+ if isinstance (action , ClearAction ):
320+ self ._clear ()
321+ elif isinstance (action , ReverseAction ):
322+ pass
323+ elif isinstance (action , DeltasAction ):
324+ mapped_action = action .map (self ._transform )
325+ total_count = self ._len_value .value
326+ for delta in mapped_action .delta_actions :
327+ if delta .is_add :
328+ self ._item_counts [delta .value ] = self ._item_counts .get (delta .value , 0 ) + 1
329+ total_count += 1
330+ else :
331+ count = self ._item_counts .get (delta .value , 0 )
332+ if count > 0 :
333+ if count == 1 :
334+ del self ._item_counts [delta .value ]
335+ else :
336+ self ._item_counts [delta .value ] = count - 1
337+ total_count -= 1
338+ else :
339+ _logger .warning (
340+ f"Attempted to remove { delta .value !r} from { self .__class__ .__name__ } , "
341+ f"but item not present. Source collection may be inconsistent with the mapped collection."
342+ )
343+
344+ if self ._is_observed ():
345+ with self ._len_value .set_delay_notify (total_count ):
346+ self ._action_event (mapped_action )
347+ self ._deltas_event (mapped_action )
348+ else :
349+ self ._len_value .value = total_count
350+
351+ @override
352+ def __repr__ (self ) -> str :
353+ return f"{ self .__class__ .__name__ } ({ list (self )!r} )"
354+
355+
284356class FilteredObservableBag (_ObservableBagBase [_S ], Generic [_S ]):
285357 def __init__ (self , source : ObservableCollection [_S ], predicate : Callable [[_S ], bool ]) -> None :
286358 super ().__init__ (tuple (item for item in source if predicate (item )))
@@ -310,6 +382,11 @@ def _on_source_action(self, action: CollectionAction[_S]) -> None:
310382 else :
311383 self ._item_counts [delta .value ] = count - 1
312384 total_count -= 1
385+ else :
386+ _logger .warning (
387+ f"Attempted to remove { delta .value !r} from { self .__class__ .__name__ } , "
388+ f"but item not present. Source collection may be inconsistent with the filtered collection."
389+ )
313390
314391 if self ._is_observed ():
315392 with self ._len_value .set_delay_notify (total_count ):
0 commit comments