From cdcd01408865a2420bd49ef11e784e2711e9e2f5 Mon Sep 17 00:00:00 2001 From: Georg Plaz Date: Tue, 10 Jun 2025 18:25:21 +0200 Subject: [PATCH] Add times parameter, allowing observer to limit observe count --- src/spellbind/observables.py | 16 ++++--- src/spellbind/values.py | 42 +++++-------------- tests/test_events/test_bi_events.py | 36 ++++++++++++++++ .../test_bi_events_weak_observe.py | 36 ++++++++++++++++ tests/test_events/test_events.py | 36 ++++++++++++++++ tests/test_events/test_events_weak_observe.py | 36 ++++++++++++++++ tests/test_events/test_tri_events.py | 36 ++++++++++++++++ .../test_tri_events_weak_observe.py | 36 ++++++++++++++++ tests/test_events/test_value_events.py | 36 ++++++++++++++++ .../test_value_events_weak_observe.py | 36 ++++++++++++++++ 10 files changed, 308 insertions(+), 38 deletions(-) diff --git a/src/spellbind/observables.py b/src/spellbind/observables.py index bf76358..217cb7b 100644 --- a/src/spellbind/observables.py +++ b/src/spellbind/observables.py @@ -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 @@ -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 @@ -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 diff --git a/src/spellbind/values.py b/src/spellbind/values.py index 584e792..76d81d8 100644 --- a/src/spellbind/values.py +++ b/src/spellbind/values.py @@ -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 @@ -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) @@ -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: @@ -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) @@ -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()) @@ -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): @@ -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: @@ -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) diff --git a/tests/test_events/test_bi_events.py b/tests/test_events/test_bi_events.py index 91d9b1e..a76eee0 100644 --- a/tests/test_events/test_bi_events.py +++ b/tests/test_events/test_bi_events.py @@ -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) diff --git a/tests/test_events/test_bi_events_weak_observe.py b/tests/test_events/test_bi_events_weak_observe.py index 2227afd..47cb645 100644 --- a/tests/test_events/test_bi_events_weak_observe.py +++ b/tests/test_events/test_bi_events_weak_observe.py @@ -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) diff --git a/tests/test_events/test_events.py b/tests/test_events/test_events.py index 3f0f766..4083fc5 100644 --- a/tests/test_events/test_events.py +++ b/tests/test_events/test_events.py @@ -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) diff --git a/tests/test_events/test_events_weak_observe.py b/tests/test_events/test_events_weak_observe.py index d3e5fa7..02d8317 100644 --- a/tests/test_events/test_events_weak_observe.py +++ b/tests/test_events/test_events_weak_observe.py @@ -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) diff --git a/tests/test_events/test_tri_events.py b/tests/test_events/test_tri_events.py index 084bce8..d43b9b6 100644 --- a/tests/test_events/test_tri_events.py +++ b/tests/test_events/test_tri_events.py @@ -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) diff --git a/tests/test_events/test_tri_events_weak_observe.py b/tests/test_events/test_tri_events_weak_observe.py index 1c121ad..6ce4d70 100644 --- a/tests/test_events/test_tri_events_weak_observe.py +++ b/tests/test_events/test_tri_events_weak_observe.py @@ -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) diff --git a/tests/test_events/test_value_events.py b/tests/test_events/test_value_events.py index aef4e1d..c2c0d59 100644 --- a/tests/test_events/test_value_events.py +++ b/tests/test_events/test_value_events.py @@ -179,3 +179,39 @@ def test_value_event_call_with_two_parameters_fails(): with pytest.raises(TypeError): event("param0", "param1") + + +def test_value_event_observe_mock_observer_times_parameter_limits_calls(): + event = ValueEvent[str]() + mock_observer = OneParameterObserver() + + event.observe(mock_observer, times=2) + + event("test") + event("test") + event("test") + + assert mock_observer.call_count == 2 + + +def test_value_event_observe_mock_observer_times_parameter_removes_subscription_after_limit(): + event = ValueEvent[str]() + mock_observer = OneParameterObserver() + + event.observe(mock_observer, times=1) + event("test") + + assert not event.is_observed(mock_observer) + + +def test_value_event_observe_mock_observer_times_none_unlimited_calls(): + event = ValueEvent[str]() + mock_observer = OneParameterObserver() + + event.observe(mock_observer, times=None) + + for _ in range(10): + event("test") + + assert mock_observer.call_count == 10 + assert event.is_observed(mock_observer) diff --git a/tests/test_events/test_value_events_weak_observe.py b/tests/test_events/test_value_events_weak_observe.py index 5ff9505..12f3f4c 100644 --- a/tests/test_events/test_value_events_weak_observe.py +++ b/tests/test_events/test_value_events_weak_observe.py @@ -279,3 +279,39 @@ def test_value_event_weak_observe_multiple_mock_observers_different_parameters() observer0.assert_called_once_with() observer1.assert_called_once_with("hello") observer2.assert_called_once_with("hello") + + +def test_value_event_weak_observe_mock_observer_times_parameter_limits_calls(): + event = ValueEvent[str]() + mock_observer = OneParameterObserver() + + event.weak_observe(mock_observer, times=2) + + event("test") + event("test") + event("test") + + assert mock_observer.call_count == 2 + + +def test_value_event_weak_observe_mock_observer_times_parameter_removes_subscription_after_limit(): + event = ValueEvent[str]() + mock_observer = OneParameterObserver() + + event.weak_observe(mock_observer, times=1) + event("test") + + assert not event.is_observed(mock_observer) + + +def test_value_event_weak_observe_mock_observer_times_none_unlimited_calls(): + event = ValueEvent[str]() + mock_observer = OneParameterObserver() + + event.weak_observe(mock_observer, times=None) + + for _ in range(10): + event("test") + + assert mock_observer.call_count == 10 + assert event.is_observed(mock_observer)