Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions src/spellbind/observables.py
Original file line number Diff line number Diff line change
Expand Up @@ -113,11 +113,11 @@ def unobserve(self, observer: Observer) -> None:

class ValueObservable(Generic[_S], ABC):
@abstractmethod
def observe(self, observer: Observer | ValueObserver[_S]) -> None:
def observe(self, observer: Observer | ValueObserver[_S], times: int | None = None) -> None:
raise NotImplementedError

@abstractmethod
def weak_observe(self, observer: Observer | ValueObserver[_S]) -> None:
def weak_observe(self, observer: Observer | ValueObserver[_S], times: int | None = None) -> None:
raise NotImplementedError

@abstractmethod
Expand All @@ -127,11 +127,13 @@ def unobserve(self, observer: Observer | ValueObserver[_S]) -> None:

class BiObservable(Generic[_S, _T], ABC):
@abstractmethod
def observe(self, observer: Observer | ValueObserver[_S] | BiObserver[_S, _T]) -> None:
def observe(self, observer: Observer | ValueObserver[_S] | BiObserver[_S, _T],
times: int | None = None) -> None:
raise NotImplementedError

@abstractmethod
def weak_observe(self, observer: Observer | ValueObserver[_S] | BiObserver[_S, _T]) -> None:
def weak_observe(self, observer: Observer | ValueObserver[_S] | BiObserver[_S, _T],
times: int | None = None) -> None:
raise NotImplementedError

@abstractmethod
Expand All @@ -141,11 +143,13 @@ def unobserve(self, observer: Observer | ValueObserver[_S] | BiObserver[_S, _T])

class TriObservable(Generic[_S, _T, _U], ABC):
@abstractmethod
def observe(self, observer: Observer | ValueObserver[_S] | BiObserver[_S, _T] | TriObserver[_S, _T, _U]) -> None:
def observe(self, observer: Observer | ValueObserver[_S] | BiObserver[_S, _T] | TriObserver[_S, _T, _U],
times: int | None = None) -> None:
raise NotImplementedError

@abstractmethod
def weak_observe(self, observer: Observer | ValueObserver[_S] | BiObserver[_S, _T] | TriObserver[_S, _T, _U]) -> None:
def weak_observe(self, observer: Observer | ValueObserver[_S] | BiObserver[_S, _T] | TriObserver[_S, _T, _U],
times: int | None = None) -> None:
raise NotImplementedError

@abstractmethod
Expand Down
42 changes: 10 additions & 32 deletions src/spellbind/values.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,6 @@ class Value(ValueObservable[_S], Generic[_S], ABC):
def value(self) -> _S:
raise NotImplementedError

@abstractmethod
def observe(self, observer: Observer | ValueObserver[_S]) -> None:
raise NotImplementedError

@abstractmethod
def unobserve(self, observer: Observer | ValueObserver[_S]) -> None:
raise NotImplementedError

@abstractmethod
def derived_from(self) -> frozenset[Value]:
raise NotImplementedError
Expand Down Expand Up @@ -135,11 +127,11 @@ def _set_value_bypass_bound_check(self, new_value: _S) -> None:
def _receive_bound_value(self, value: _S) -> None:
self._set_value_bypass_bound_check(value)

def observe(self, observer: Observer | ValueObserver[_S]) -> None:
self._on_change.observe(observer)
def observe(self, observer: Observer | ValueObserver[_S], times: int | None = None) -> None:
self._on_change.observe(observer, times)

def weak_observe(self, observer: Observer | ValueObserver[_S]) -> None:
self._on_change.weak_observe(observer)
def weak_observe(self, observer: Observer | ValueObserver[_S], times: int | None = None) -> None:
self._on_change.weak_observe(observer, times)

def unobserve(self, observer: Observer | ValueObserver[_S]) -> None:
self._on_change.unobserve(observer)
Expand Down Expand Up @@ -188,10 +180,10 @@ def __init__(self, value: _S):
def value(self) -> _S:
return self._value

def observe(self, observer: Observer | ValueObserver[_S]) -> None:
def observe(self, observer: Observer | ValueObserver[_S], times: int | None = None) -> None:
pass

def weak_observe(self, observer: Observer | ValueObserver[_S]) -> None:
def weak_observe(self, observer: Observer | ValueObserver[_S], times: int | None = None) -> None:
pass

def unobserve(self, observer: Observer | ValueObserver[_S]) -> None:
Expand All @@ -209,11 +201,11 @@ def __init__(self, *values: Value):
def derived_from(self) -> frozenset[Value]:
return self._values

def observe(self, observer: Observer | ValueObserver[_T]) -> None:
self._on_change.observe(observer)
def observe(self, observer: Observer | ValueObserver[_T], times: int | None = None) -> None:
self._on_change.observe(observer, times)

def weak_observe(self, observer: Observer | ValueObserver[_T]) -> None:
self._on_change.weak_observe(observer)
def weak_observe(self, observer: Observer | ValueObserver[_T], times: int | None = None) -> None:
self._on_change.weak_observe(observer, times)

def unobserve(self, observer: Observer | ValueObserver[_T]) -> None:
self._on_change.unobserve(observer)
Expand Down Expand Up @@ -269,7 +261,6 @@ def __init__(self, left: Value[_S] | _S, right: Value[_T] | _T):
if isinstance(right, Value):
right.observe(self._on_right_change)
self._value = self.transform(self._left_getter(), self._right_getter())
self._on_change = ValueEvent()

def _on_left_change(self, new_left_value: _S) -> None:
new_value = self.transform(new_left_value, self._right_getter())
Expand All @@ -292,12 +283,6 @@ def transform(self, left: _S, right: _T) -> _U:
def value(self) -> _U:
return self._value

def observe(self, observer: Observer | ValueObserver[_U]) -> None:
self._on_change.observe(observer)

def unobserve(self, observer: Observer | ValueObserver[_U]) -> None:
self._on_change.unobserve(observer)


def _get_value(value: Value[_S] | _S) -> _S:
if isinstance(value, Value):
Expand All @@ -314,7 +299,6 @@ def __init__(self, *sources: Value[_S] | _S):
if isinstance(v, Value):
v.observe(self._create_on_n_changed(i))
self._value = self._calculate_value()
self._on_change: ValueEvent[_T] = ValueEvent()

def _create_on_n_changed(self, index: int) -> Callable[[_S], None]:
def on_change(new_value: _S) -> None:
Expand All @@ -337,9 +321,3 @@ def transform(self, *args: _S) -> _T:
@property
def value(self) -> _T:
return self._value

def observe(self, observer: Observer | ValueObserver[_T]) -> None:
self._on_change.observe(observer)

def unobserve(self, observer: Observer | ValueObserver[_T]) -> None:
self._on_change.unobserve(observer)
36 changes: 36 additions & 0 deletions tests/test_events/test_bi_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,3 +211,39 @@ def test_bi_event_call_mock_observer_with_none_values():
event(None, None)

observer.assert_called_once_with(None, None)


def test_bi_event_observe_mock_observer_times_parameter_limits_calls():
event = BiEvent[str, int]()
mock_observer = TwoParametersObserver()

event.observe(mock_observer, times=2)

event("test", 42)
event("test", 42)
event("test", 42)

assert mock_observer.call_count == 2


def test_bi_event_observe_mock_observer_times_parameter_removes_subscription_after_limit():
event = BiEvent[str, int]()
mock_observer = TwoParametersObserver()

event.observe(mock_observer, times=1)
event("test", 42)

assert not event.is_observed(mock_observer)


def test_bi_event_observe_mock_observer_times_none_unlimited_calls():
event = BiEvent[str, int]()
mock_observer = TwoParametersObserver()

event.observe(mock_observer, times=None)

for _ in range(10):
event("test", 42)

assert mock_observer.call_count == 10
assert event.is_observed(mock_observer)
36 changes: 36 additions & 0 deletions tests/test_events/test_bi_events_weak_observe.py
Original file line number Diff line number Diff line change
Expand Up @@ -337,3 +337,39 @@ def test_bi_event_weak_observe_multiple_mock_observers_different_parameters():
observer1.assert_called_once_with("hello")
observer2.assert_called_once_with("hello", 123)
observer3.assert_called_once_with("hello", 123)


def test_bi_event_weak_observe_mock_observer_times_parameter_limits_calls():
event = BiEvent[str, int]()
mock_observer = TwoParametersObserver()

event.weak_observe(mock_observer, times=2)

event("test", 42)
event("test", 42)
event("test", 42)

assert mock_observer.call_count == 2


def test_bi_event_weak_observe_mock_observer_times_parameter_removes_subscription_after_limit():
event = BiEvent[str, int]()
mock_observer = TwoParametersObserver()

event.weak_observe(mock_observer, times=1)
event("test", 42)

assert not event.is_observed(mock_observer)


def test_bi_event_weak_observe_mock_observer_times_none_unlimited_calls():
event = BiEvent[str, int]()
mock_observer = TwoParametersObserver()

event.weak_observe(mock_observer, times=None)

for _ in range(10):
event("test", 42)

assert mock_observer.call_count == 10
assert event.is_observed(mock_observer)
36 changes: 36 additions & 0 deletions tests/test_events/test_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,39 @@ def test_event_observe_lambda_observer():
event()

assert calls == [True]


def test_event_observe_mock_observer_times_parameter_limits_calls():
event = Event()
mock_observer = NoParametersObserver()

event.observe(mock_observer, times=2)

event()
event()
event()

assert mock_observer.call_count == 2


def test_event_observe_mock_observer_times_parameter_removes_subscription_after_limit():
event = Event()
mock_observer = NoParametersObserver()

event.observe(mock_observer, times=1)
event()

assert not event.is_observed(mock_observer)


def test_event_observe_mock_observer_times_none_unlimited_calls():
event = Event()
mock_observer = NoParametersObserver()

event.observe(mock_observer, times=None)

for _ in range(10):
event()

assert mock_observer.call_count == 10
assert event.is_observed(mock_observer)
36 changes: 36 additions & 0 deletions tests/test_events/test_events_weak_observe.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,3 +188,39 @@ def test_event_call_mixed_weak_strong_lambda_observers_in_order():
event()

assert calls == ["test 0", "test 1", "test 2", "test 3"]


def test_event_weak_observe_mock_observer_times_parameter_limits_calls():
event = Event()
mock_observer = NoParametersObserver()

event.weak_observe(mock_observer, times=2)

event()
event()
event()

assert mock_observer.call_count == 2


def test_event_weak_observe_mock_observer_times_parameter_removes_subscription_after_limit():
event = Event()
mock_observer = NoParametersObserver()

event.weak_observe(mock_observer, times=1)
event()

assert not event.is_observed(mock_observer)


def test_event_weak_observe_mock_observer_times_none_unlimited_calls():
event = Event()
mock_observer = NoParametersObserver()

event.weak_observe(mock_observer, times=None)

for _ in range(10):
event()

assert mock_observer.call_count == 10
assert event.is_observed(mock_observer)
36 changes: 36 additions & 0 deletions tests/test_events/test_tri_events.py
Original file line number Diff line number Diff line change
Expand Up @@ -244,3 +244,39 @@ def test_tri_event_call_mock_observer_with_none_values():
event(None, None, None)

observer.assert_called_once_with(None, None, None)


def test_tri_event_observe_mock_observer_times_parameter_limits_calls():
event = TriEvent[str, int, bool]()
mock_observer = ThreeParametersObserver()

event.observe(mock_observer, times=2)

event("test", 42, True)
event("test", 42, True)
event("test", 42, True)

assert mock_observer.call_count == 2


def test_tri_event_observe_mock_observer_times_parameter_removes_subscription_after_limit():
event = TriEvent[str, int, bool]()
mock_observer = ThreeParametersObserver()

event.observe(mock_observer, times=1)
event("test", 42, True)

assert not event.is_observed(mock_observer)


def test_tri_event_observe_mock_observer_times_none_unlimited_calls():
event = TriEvent[str, int, bool]()
mock_observer = ThreeParametersObserver()

event.observe(mock_observer, times=None)

for _ in range(10):
event("test", 42, True)

assert mock_observer.call_count == 10
assert event.is_observed(mock_observer)
36 changes: 36 additions & 0 deletions tests/test_events/test_tri_events_weak_observe.py
Original file line number Diff line number Diff line change
Expand Up @@ -386,3 +386,39 @@ def test_tri_event_weak_observe_multiple_mock_observers_different_parameters():
observer2.assert_called_once_with("hello", 123)
observer3.assert_called_once_with("hello", 123, False)
observer4.assert_called_once_with("hello", 123, False)


def test_tri_event_weak_observe_mock_observer_times_parameter_limits_calls():
event = TriEvent[str, int, bool]()
mock_observer = ThreeParametersObserver()

event.weak_observe(mock_observer, times=2)

event("test", 42, True)
event("test", 42, True)
event("test", 42, True)

assert mock_observer.call_count == 2


def test_tri_event_weak_observe_mock_observer_times_parameter_removes_subscription_after_limit():
event = TriEvent[str, int, bool]()
mock_observer = ThreeParametersObserver()

event.weak_observe(mock_observer, times=1)
event("test", 42, True)

assert not event.is_observed(mock_observer)


def test_tri_event_weak_observe_mock_observer_times_none_unlimited_calls():
event = TriEvent[str, int, bool]()
mock_observer = ThreeParametersObserver()

event.weak_observe(mock_observer, times=None)

for _ in range(10):
event("test", 42, True)

assert mock_observer.call_count == 10
assert event.is_observed(mock_observer)
Loading