Added add_event_listener and remove_event_listener
This commit is contained in:
79
README.md
79
README.md
@@ -150,6 +150,82 @@ data.value = "final"
|
|||||||
# Callback2: test -> final
|
# Callback2: test -> final
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### Event listener
|
||||||
|
I can register a listener for the `ObservableEvent.AFTER_PROPERTY_CHANGE` event.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from myutils.observable import make_observable, bind, add_event_listener, ObservableEvent
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Data:
|
||||||
|
number: int = 1
|
||||||
|
|
||||||
|
data = Data()
|
||||||
|
make_observable(data)
|
||||||
|
|
||||||
|
on_change_results = []
|
||||||
|
on_after_change_results = []
|
||||||
|
|
||||||
|
def on_change(old, new):
|
||||||
|
on_change_results.append((old, new))
|
||||||
|
return new + 1
|
||||||
|
|
||||||
|
def on_after_change(attr, old, new, results):
|
||||||
|
on_after_change_results.append((attr, old, new, results))
|
||||||
|
|
||||||
|
bind(data, 'number', on_change)
|
||||||
|
add_event_listener(ObservableEvent.AFTER_PROPERTY_CHANGE, data, "number", on_after_change)
|
||||||
|
|
||||||
|
data.number = 5
|
||||||
|
data.number = 10
|
||||||
|
|
||||||
|
assert on_change_results == [(1, 5), (5, 10)]
|
||||||
|
assert on_after_change_results == [("number", 1, 5, [6]), ("number", 5, 10, [11])]
|
||||||
|
```
|
||||||
|
|
||||||
|
I can register for all attributes change events.
|
||||||
|
|
||||||
|
```python
|
||||||
|
|
||||||
|
from myutils.observable import make_observable, bind, add_event_listener, ObservableEvent
|
||||||
|
|
||||||
|
|
||||||
|
class Data:
|
||||||
|
number: int = 1
|
||||||
|
value: str = "initial"
|
||||||
|
|
||||||
|
|
||||||
|
data = Data()
|
||||||
|
make_observable(data)
|
||||||
|
|
||||||
|
|
||||||
|
def on_change_1(old, new):
|
||||||
|
return new
|
||||||
|
|
||||||
|
|
||||||
|
def on_change_2(old, new):
|
||||||
|
return str(new) + "_1"
|
||||||
|
|
||||||
|
|
||||||
|
on_after_change_results = []
|
||||||
|
|
||||||
|
|
||||||
|
def on_after_change(attr, old, new, results):
|
||||||
|
on_after_change_results.append((attr, old, new, results))
|
||||||
|
|
||||||
|
|
||||||
|
add_event_listener(ObservableEvent.AFTER_PROPERTY_CHANGE, data, "", on_after_change)
|
||||||
|
bind(data, 'number', on_change_1)
|
||||||
|
bind(data, 'number', on_change_2)
|
||||||
|
bind(data, 'value', on_change_1)
|
||||||
|
|
||||||
|
data.number = 5
|
||||||
|
data.value = "new value"
|
||||||
|
|
||||||
|
assert on_after_change_results == [("number", 1, 5, [5, "5_1"]), ("value", "initial", "new value", ["new value"])]
|
||||||
|
```
|
||||||
|
|
||||||
#### Using Helper Functions
|
#### Using Helper Functions
|
||||||
|
|
||||||
```python
|
```python
|
||||||
@@ -319,4 +395,5 @@ Special thanks to the Python and open-source community for their tools, inspirat
|
|||||||
|
|
||||||
* 0.1.0 : Initial release
|
* 0.1.0 : Initial release
|
||||||
* 0.2.0 : Observable results can be collected using `collect_return_values`
|
* 0.2.0 : Observable results can be collected using `collect_return_values`
|
||||||
* 0.3.0 : Added `unbind`, `unbind_all`, `has_listeners` `get_listener_count` to Observable
|
* 0.3.0 : Added `unbind`, `unbind_all`, `has_listeners` `get_listener_count` to Observable
|
||||||
|
* 0.4.0 : Added `add_event_listener` and `remove_event_listener`
|
||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "myutils"
|
name = "myutils"
|
||||||
version = "0.3.0"
|
version = "0.4.0"
|
||||||
description = "Base useful classes."
|
description = "Base useful classes."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
authors = [
|
authors = [
|
||||||
|
|||||||
@@ -1,8 +1,15 @@
|
|||||||
|
from enum import Enum
|
||||||
|
|
||||||
|
|
||||||
class NotObservableError(Exception):
|
class NotObservableError(Exception):
|
||||||
def __init__(self, obj):
|
def __init__(self, obj):
|
||||||
super().__init__(f'{obj} is not observable')
|
super().__init__(f'{obj} is not observable')
|
||||||
|
|
||||||
|
|
||||||
|
class ObservableEvent(Enum):
|
||||||
|
AFTER_PROPERTY_CHANGE = "__*after_property_change*__"
|
||||||
|
|
||||||
|
|
||||||
def make_observable(obj):
|
def make_observable(obj):
|
||||||
if hasattr(obj, '_listeners'):
|
if hasattr(obj, '_listeners'):
|
||||||
return obj # Already observable
|
return obj # Already observable
|
||||||
@@ -24,6 +31,16 @@ def make_observable(obj):
|
|||||||
for callback in self._listeners[name]:
|
for callback in self._listeners[name]:
|
||||||
res.append(callback(old_value, value))
|
res.append(callback(old_value, value))
|
||||||
setattr(self, '_return_values', res)
|
setattr(self, '_return_values', res)
|
||||||
|
|
||||||
|
# Trigger AFTER_PROPERTY_CHANGE event
|
||||||
|
after_prop_changed_event = ObservableEvent.AFTER_PROPERTY_CHANGE.value
|
||||||
|
if after_prop_changed_event in self._listeners:
|
||||||
|
if "" in self._listeners[after_prop_changed_event]: # Trigger for all attributes
|
||||||
|
for callback in self._listeners[after_prop_changed_event][""]:
|
||||||
|
callback(name, old_value, value, res)
|
||||||
|
else:
|
||||||
|
for callback in self._listeners[after_prop_changed_event].get(name, []):
|
||||||
|
callback(name, old_value, value, res)
|
||||||
|
|
||||||
obj.__class__ = ObservableVersion
|
obj.__class__ = ObservableVersion
|
||||||
obj._listeners = {}
|
obj._listeners = {}
|
||||||
@@ -48,12 +65,48 @@ def bind(obj, attr_name, callback):
|
|||||||
obj._listeners[attr_name].append(callback)
|
obj._listeners[attr_name].append(callback)
|
||||||
|
|
||||||
|
|
||||||
"""
|
def add_event_listener(event: ObservableEvent, obj, attr_name, callback):
|
||||||
Implementation of unbind() function for myutils.observable module.
|
if not hasattr(obj, '_listeners'):
|
||||||
|
raise NotObservableError(
|
||||||
|
f"Object must be made observable with make_observable() before binding"
|
||||||
|
)
|
||||||
|
event_name = event.value
|
||||||
|
if event_name not in obj._listeners:
|
||||||
|
obj._listeners[event_name] = {}
|
||||||
|
if attr_name not in obj._listeners[event_name]:
|
||||||
|
obj._listeners[event_name][attr_name] = []
|
||||||
|
obj._listeners[event_name][attr_name].append(callback)
|
||||||
|
|
||||||
This implementation is adapted to work with the existing make_observable()
|
|
||||||
and bind() functions that use the _listeners dictionary pattern.
|
def remove_event_listener(event: ObservableEvent, obj, attr_name, callback):
|
||||||
"""
|
if not hasattr(obj, '_listeners'):
|
||||||
|
raise NotObservableError(
|
||||||
|
f"Object must be made observable with make_observable() before binding"
|
||||||
|
)
|
||||||
|
event_name = event.value
|
||||||
|
if event_name not in obj._listeners:
|
||||||
|
return
|
||||||
|
if attr_name and attr_name not in obj._listeners[event_name]:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not attr_name:
|
||||||
|
for attr in list(obj._listeners[event_name].keys()):
|
||||||
|
if callback:
|
||||||
|
obj._listeners[event_name][attr].remove(callback)
|
||||||
|
else:
|
||||||
|
del obj._listeners[event_name][attr]
|
||||||
|
else:
|
||||||
|
if callback:
|
||||||
|
obj._listeners[event_name][attr_name].remove(callback)
|
||||||
|
else:
|
||||||
|
del obj._listeners[event_name][attr_name]
|
||||||
|
|
||||||
|
# cleanup: remove empty attributes
|
||||||
|
for attr in list(obj._listeners[event_name].keys()):
|
||||||
|
if len(obj._listeners[event_name][attr]) == 0:
|
||||||
|
del obj._listeners[event_name][attr]
|
||||||
|
if len(obj._listeners[event_name]) == 0:
|
||||||
|
del obj._listeners[event_name]
|
||||||
|
|
||||||
|
|
||||||
def unbind(obj, attr_name, callback):
|
def unbind(obj, attr_name, callback):
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from myutils.observable import NotObservableError, make_observable, bind, collect_return_values, unbind, unbind_all, \
|
from myutils.observable import NotObservableError, make_observable, bind, collect_return_values, unbind, unbind_all, \
|
||||||
has_listeners, get_listener_count
|
has_listeners, get_listener_count, add_event_listener, ObservableEvent, remove_event_listener
|
||||||
|
|
||||||
|
|
||||||
# Test fixtures
|
# Test fixtures
|
||||||
@@ -552,6 +552,337 @@ def test_i_cannot_get_listener_count_on_non_observable_object(data):
|
|||||||
get_listener_count(data, "value")
|
get_listener_count(data, "value")
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# add_event_listener() tests
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def test_i_can_add_an_event_listener(observable_data):
|
||||||
|
"""
|
||||||
|
add_event_listener() should correctly add a listener for a specific event and attribute.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def callback(attr, old, new, results):
|
||||||
|
pass
|
||||||
|
|
||||||
|
add_event_listener(
|
||||||
|
ObservableEvent.AFTER_PROPERTY_CHANGE, observable_data, "value", callback
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check that the listener has been added
|
||||||
|
event_name = ObservableEvent.AFTER_PROPERTY_CHANGE.value
|
||||||
|
assert event_name in observable_data._listeners
|
||||||
|
assert "value" in observable_data._listeners[event_name]
|
||||||
|
assert callback in observable_data._listeners[event_name]["value"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_can_add_multiple_event_listeners_for_same_attribute(observable_data):
|
||||||
|
"""
|
||||||
|
Multiple event listeners can be added to the same attribute and event.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def callback1(attr, old, new, results):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def callback2(attr, old, new, results):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def callback3(attr, old, new, results):
|
||||||
|
pass
|
||||||
|
|
||||||
|
event = ObservableEvent.AFTER_PROPERTY_CHANGE
|
||||||
|
|
||||||
|
add_event_listener(event, observable_data, "value", callback1)
|
||||||
|
add_event_listener(event, observable_data, "value", callback2)
|
||||||
|
add_event_listener(event, observable_data, "value", callback3)
|
||||||
|
|
||||||
|
# Check that all listeners have been added
|
||||||
|
event_name = event.value
|
||||||
|
assert len(observable_data._listeners[event_name]["value"]) == 3
|
||||||
|
assert callback1 in observable_data._listeners[event_name]["value"]
|
||||||
|
assert callback2 in observable_data._listeners[event_name]["value"]
|
||||||
|
assert callback3 in observable_data._listeners[event_name]["value"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_can_handle_duplicate_event_listeners(observable_data):
|
||||||
|
"""
|
||||||
|
The same callback can be added multiple times and should appear multiple times.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def callback(attr, old, new, results):
|
||||||
|
pass
|
||||||
|
|
||||||
|
event = ObservableEvent.AFTER_PROPERTY_CHANGE
|
||||||
|
|
||||||
|
add_event_listener(event, observable_data, "value", callback)
|
||||||
|
add_event_listener(event, observable_data, "value", callback)
|
||||||
|
add_event_listener(event, observable_data, "value", callback)
|
||||||
|
|
||||||
|
# Check that the same callback appears 3 times
|
||||||
|
event_name = event.value
|
||||||
|
assert len(observable_data._listeners[event_name]["value"]) == 3
|
||||||
|
assert observable_data._listeners[event_name]["value"].count(callback) == 3
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_cannot_add_listener_to_non_observable_object(data):
|
||||||
|
"""
|
||||||
|
Trying to add an event listener to a non-observable object should raise NotObservableError.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def callback(attr, old, new, results):
|
||||||
|
pass
|
||||||
|
|
||||||
|
with pytest.raises(NotObservableError, match="must be made observable"):
|
||||||
|
add_event_listener(ObservableEvent.AFTER_PROPERTY_CHANGE, data, "value", callback)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# remove_event_listener() tests
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def test_i_can_remove_an_event_listener(observable_data):
|
||||||
|
"""
|
||||||
|
remove_event_listener() should correctly remove a previously added listener.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def callback(attr, old, new, results):
|
||||||
|
pass
|
||||||
|
|
||||||
|
event = ObservableEvent.AFTER_PROPERTY_CHANGE
|
||||||
|
|
||||||
|
# Add the listener
|
||||||
|
add_event_listener(event, observable_data, "value", callback)
|
||||||
|
|
||||||
|
event_name = event.value
|
||||||
|
assert callback in observable_data._listeners[event_name]["value"]
|
||||||
|
|
||||||
|
# Remove the listener
|
||||||
|
remove_event_listener(event, observable_data, "value", callback)
|
||||||
|
|
||||||
|
# Check that the listener has been removed and structure cleaned up
|
||||||
|
assert event_name not in observable_data._listeners
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_can_remove_last_event_listener_and_clean_up_structure(observable_data):
|
||||||
|
"""
|
||||||
|
Removing the last listener should clean up the internal data structures.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def callback(attr, old, new, results):
|
||||||
|
pass
|
||||||
|
|
||||||
|
event = ObservableEvent.AFTER_PROPERTY_CHANGE
|
||||||
|
event_name = event.value
|
||||||
|
|
||||||
|
add_event_listener(event, observable_data, "value", callback)
|
||||||
|
|
||||||
|
# Verify it was added
|
||||||
|
assert event_name in observable_data._listeners
|
||||||
|
assert "value" in observable_data._listeners[event_name]
|
||||||
|
|
||||||
|
# Remove it
|
||||||
|
remove_event_listener(event, observable_data, "value", callback)
|
||||||
|
|
||||||
|
# Verify cleanup: event should be removed from _listeners
|
||||||
|
assert event_name not in observable_data._listeners
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_cannot_remove_listener_from_non_observable_object(data):
|
||||||
|
"""
|
||||||
|
Trying to remove an event listener from a non-observable object should raise NotObservableError.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def callback(attr, old, new, results):
|
||||||
|
pass
|
||||||
|
|
||||||
|
with pytest.raises(NotObservableError, match="must be made observable"):
|
||||||
|
remove_event_listener(ObservableEvent.AFTER_PROPERTY_CHANGE, data, "value", callback)
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_cannot_remove_non_existent_event_listener(observable_data):
|
||||||
|
"""
|
||||||
|
Trying to remove a non-existent listener should fail silently (no exception).
|
||||||
|
"""
|
||||||
|
|
||||||
|
def callback(attr, old, new, results):
|
||||||
|
pass
|
||||||
|
|
||||||
|
event = ObservableEvent.AFTER_PROPERTY_CHANGE
|
||||||
|
|
||||||
|
# Should not raise an exception
|
||||||
|
remove_event_listener(event, observable_data, "value", callback)
|
||||||
|
|
||||||
|
|
||||||
|
def test_remove_event_listener_does_not_affect_others(observable_data):
|
||||||
|
"""
|
||||||
|
Removing one listener should not affect other listeners on the same attribute.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def callback1(attr, old, new, results):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def callback2(attr, old, new, results):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def callback3(attr, old, new, results):
|
||||||
|
pass
|
||||||
|
|
||||||
|
event = ObservableEvent.AFTER_PROPERTY_CHANGE
|
||||||
|
event_name = event.value
|
||||||
|
|
||||||
|
# Add three listeners
|
||||||
|
add_event_listener(event, observable_data, "value", callback1)
|
||||||
|
add_event_listener(event, observable_data, "value", callback2)
|
||||||
|
add_event_listener(event, observable_data, "value", callback3)
|
||||||
|
|
||||||
|
assert len(observable_data._listeners[event_name]["value"]) == 3
|
||||||
|
|
||||||
|
# Remove only the second one
|
||||||
|
remove_event_listener(event, observable_data, "value", callback2)
|
||||||
|
|
||||||
|
# Check that only callback2 was removed
|
||||||
|
assert len(observable_data._listeners[event_name]["value"]) == 2
|
||||||
|
assert callback1 in observable_data._listeners[event_name]["value"]
|
||||||
|
assert callback2 not in observable_data._listeners[event_name]["value"]
|
||||||
|
assert callback3 in observable_data._listeners[event_name]["value"]
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Integration tests for add/remove event listeners
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
def test_i_can_add_and_remove_event_listeners_sequentially(observable_data):
|
||||||
|
"""
|
||||||
|
Multiple add/remove cycles should work correctly.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def callback(attr, old, new, results):
|
||||||
|
pass
|
||||||
|
|
||||||
|
event = ObservableEvent.AFTER_PROPERTY_CHANGE
|
||||||
|
event_name = event.value
|
||||||
|
|
||||||
|
# Cycle 1: Add and remove
|
||||||
|
add_event_listener(event, observable_data, "value", callback)
|
||||||
|
assert event_name in observable_data._listeners
|
||||||
|
|
||||||
|
remove_event_listener(event, observable_data, "value", callback)
|
||||||
|
assert event_name not in observable_data._listeners
|
||||||
|
|
||||||
|
# Cycle 2: Add and remove again
|
||||||
|
add_event_listener(event, observable_data, "value", callback)
|
||||||
|
assert event_name in observable_data._listeners
|
||||||
|
|
||||||
|
remove_event_listener(event, observable_data, "value", callback)
|
||||||
|
assert event_name not in observable_data._listeners
|
||||||
|
|
||||||
|
|
||||||
|
def test_listener_removal_on_multiple_event_types(observable_data):
|
||||||
|
"""
|
||||||
|
Listeners for different attributes should be independently manageable.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def callback1(attr, old, new, results):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def callback2(attr, old, new, results):
|
||||||
|
pass
|
||||||
|
|
||||||
|
event = ObservableEvent.AFTER_PROPERTY_CHANGE
|
||||||
|
event_name = event.value
|
||||||
|
|
||||||
|
# Add listeners to different attributes
|
||||||
|
add_event_listener(event, observable_data, "value", callback1)
|
||||||
|
add_event_listener(event, observable_data, "number", callback2)
|
||||||
|
|
||||||
|
assert "value" in observable_data._listeners[event_name]
|
||||||
|
assert "number" in observable_data._listeners[event_name]
|
||||||
|
|
||||||
|
# Remove listener from "value"
|
||||||
|
remove_event_listener(event, observable_data, "value", callback1)
|
||||||
|
|
||||||
|
# Check that "value" is removed but "number" remains
|
||||||
|
assert "value" not in observable_data._listeners[event_name]
|
||||||
|
assert "number" in observable_data._listeners[event_name]
|
||||||
|
assert callback2 in observable_data._listeners[event_name]["number"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_can_remove_all_event_listener_when_attr_name_is_none(observable_data):
|
||||||
|
"""
|
||||||
|
When unbind_all() is called with attr_name=None, all listeners for all attributes
|
||||||
|
should be removed from the observable object.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def callback1(attr, old, new, results):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def callback2(attr, old, new, results):
|
||||||
|
pass
|
||||||
|
|
||||||
|
event = ObservableEvent.AFTER_PROPERTY_CHANGE
|
||||||
|
event_name = event.value
|
||||||
|
|
||||||
|
# Add listeners to different attributes
|
||||||
|
add_event_listener(event, observable_data, "value", callback1)
|
||||||
|
add_event_listener(event, observable_data, "value", callback2)
|
||||||
|
add_event_listener(event, observable_data, "number", callback1)
|
||||||
|
add_event_listener(event, observable_data, "number", callback2)
|
||||||
|
|
||||||
|
remove_event_listener(event, observable_data, None, callback1)
|
||||||
|
callbacks = observable_data._listeners[event_name]["value"] + observable_data._listeners[event_name]["number"]
|
||||||
|
assert callback1 not in callbacks
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_can_remove_all_event_listener_when_callback_is_none(observable_data):
|
||||||
|
"""
|
||||||
|
When unbind_all() is called with attr_name=None, all listeners for all attributes
|
||||||
|
should be removed from the observable object.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def callback1(attr, old, new, results):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def callback2(attr, old, new, results):
|
||||||
|
pass
|
||||||
|
|
||||||
|
event = ObservableEvent.AFTER_PROPERTY_CHANGE
|
||||||
|
event_name = event.value
|
||||||
|
|
||||||
|
# Add listeners to different attributes
|
||||||
|
add_event_listener(event, observable_data, "value", callback1)
|
||||||
|
add_event_listener(event, observable_data, "value", callback2)
|
||||||
|
add_event_listener(event, observable_data, "number", callback1)
|
||||||
|
add_event_listener(event, observable_data, "number", callback2)
|
||||||
|
|
||||||
|
remove_event_listener(event, observable_data, "value", None)
|
||||||
|
assert "value" not in observable_data._listeners[event_name]
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_can_remove_all_event_listener_when_attr_name_callback_are_none(observable_data):
|
||||||
|
"""
|
||||||
|
When unbind_all() is called with attr_name=None, all listeners for all attributes
|
||||||
|
should be removed from the observable object.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def callback1(attr, old, new, results):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def callback2(attr, old, new, results):
|
||||||
|
pass
|
||||||
|
|
||||||
|
event = ObservableEvent.AFTER_PROPERTY_CHANGE
|
||||||
|
event_name = event.value
|
||||||
|
|
||||||
|
# Add listeners to different attributes
|
||||||
|
add_event_listener(event, observable_data, "value", callback1)
|
||||||
|
add_event_listener(event, observable_data, "value", callback2)
|
||||||
|
add_event_listener(event, observable_data, "number", callback1)
|
||||||
|
add_event_listener(event, observable_data, "number", callback2)
|
||||||
|
|
||||||
|
remove_event_listener(event, observable_data, None, None)
|
||||||
|
assert "value" not in observable_data._listeners[event_name]
|
||||||
|
assert "number" not in observable_data._listeners[event_name]
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Integration tests
|
# Integration tests
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@@ -584,24 +915,133 @@ def test_multiple_bind_unbind_cycles(observable_data):
|
|||||||
assert len(results) == 2
|
assert len(results) == 2
|
||||||
|
|
||||||
|
|
||||||
def test_same_callback_can_be_bound_to_multiple_attributes(observable_data):
|
# ============================================================================
|
||||||
"""
|
# After property changed event
|
||||||
The same callback can be bound to multiple attributes and unbound independently.
|
# ============================================================================
|
||||||
"""
|
|
||||||
results = []
|
|
||||||
|
def test_i_can_receive_changes_in_after_property_change_event(observable_data):
|
||||||
|
"""Default case : after_property_change event should be fired when property changes."""
|
||||||
|
data = Data()
|
||||||
|
make_observable(data)
|
||||||
|
|
||||||
def callback(old, new):
|
on_change_results = []
|
||||||
results.append((old, new))
|
on_after_change_results = []
|
||||||
|
|
||||||
bind(observable_data, "value", callback)
|
def on_change(old, new):
|
||||||
bind(observable_data, "count", callback)
|
on_change_results.append((old, new))
|
||||||
|
return new + 1
|
||||||
|
|
||||||
observable_data.value = "first"
|
def on_after_change(attr, old, new, results):
|
||||||
observable_data.count = 1
|
on_after_change_results.append((attr, old, new, results))
|
||||||
assert len(results) == 2
|
|
||||||
|
|
||||||
unbind(observable_data, "value", callback)
|
bind(data, 'number', on_change)
|
||||||
|
add_event_listener(ObservableEvent.AFTER_PROPERTY_CHANGE, data, "number", on_after_change)
|
||||||
|
|
||||||
observable_data.value = "second"
|
data.number = 5
|
||||||
observable_data.count = 2
|
data.number = 10
|
||||||
assert len(results) == 3 # Only count callback triggered
|
|
||||||
|
assert on_change_results == [(1, 5), (5, 10)]
|
||||||
|
assert on_after_change_results == [("number", 1, 5, [6]), ("number", 5, 10, [11])]
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_can_receive_changes_when_multiple_properties_are_changed(observable_data):
|
||||||
|
"""When multiple properties are changed, after_property_change event fires when all properties change."""
|
||||||
|
data = Data()
|
||||||
|
make_observable(data)
|
||||||
|
|
||||||
|
def on_change_1(old, new):
|
||||||
|
return new
|
||||||
|
|
||||||
|
def on_change_2(old, new):
|
||||||
|
return new + 1
|
||||||
|
|
||||||
|
on_after_change_results = []
|
||||||
|
|
||||||
|
def on_after_change(attr, old, new, results):
|
||||||
|
on_after_change_results.append((attr, old, new, results))
|
||||||
|
|
||||||
|
bind(data, 'number', on_change_1)
|
||||||
|
bind(data, 'number', on_change_2)
|
||||||
|
add_event_listener(ObservableEvent.AFTER_PROPERTY_CHANGE, data, "number", on_after_change)
|
||||||
|
|
||||||
|
data.number = 5
|
||||||
|
data.number = 10
|
||||||
|
|
||||||
|
assert on_after_change_results == [("number", 1, 5, [5, 6]), ("number", 5, 10, [10, 11])]
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_can_receive_changes_in_after_property_change_event_when_declared_first(observable_data):
|
||||||
|
"""after_property_change event should be fired even if defined before the bindings."""
|
||||||
|
data = Data()
|
||||||
|
make_observable(data)
|
||||||
|
|
||||||
|
def on_change(old, new):
|
||||||
|
return new + 1
|
||||||
|
|
||||||
|
on_after_change_results = []
|
||||||
|
|
||||||
|
def on_after_change(attr, old, new, results):
|
||||||
|
on_after_change_results.append((attr, old, new, results))
|
||||||
|
|
||||||
|
add_event_listener(ObservableEvent.AFTER_PROPERTY_CHANGE, data, "number", on_after_change)
|
||||||
|
bind(data, 'number', on_change)
|
||||||
|
|
||||||
|
data.number = 5
|
||||||
|
data.number = 10
|
||||||
|
|
||||||
|
assert on_after_change_results == [("number", 1, 5, [6]), ("number", 5, 10, [11])]
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_can_receive_changes_in_after_property_change_event_for_requested_attribute(observable_data):
|
||||||
|
"""after_property_change event should be fired even if defined before the bindings."""
|
||||||
|
data = Data()
|
||||||
|
make_observable(data)
|
||||||
|
|
||||||
|
def on_change_1(old, new):
|
||||||
|
return new
|
||||||
|
|
||||||
|
def on_change_2(old, new):
|
||||||
|
return str(new) + "_1"
|
||||||
|
|
||||||
|
on_after_change_results = []
|
||||||
|
|
||||||
|
def on_after_change(attr, old, new, results):
|
||||||
|
on_after_change_results.append((attr, old, new, results))
|
||||||
|
|
||||||
|
add_event_listener(ObservableEvent.AFTER_PROPERTY_CHANGE, data, "number", on_after_change)
|
||||||
|
bind(data, 'number', on_change_1)
|
||||||
|
bind(data, 'number', on_change_2)
|
||||||
|
bind(data, 'value', on_change_1)
|
||||||
|
|
||||||
|
data.number = 5
|
||||||
|
data.value = "new value"
|
||||||
|
|
||||||
|
assert on_after_change_results == [("number", 1, 5, [5, "5_1"])]
|
||||||
|
|
||||||
|
|
||||||
|
def test_i_can_receive_changes_in_after_property_change_event_for_all_attributes(observable_data):
|
||||||
|
"""after_property_change event should be fired even if defined before the bindings."""
|
||||||
|
data = Data()
|
||||||
|
make_observable(data)
|
||||||
|
|
||||||
|
def on_change_1(old, new):
|
||||||
|
return new
|
||||||
|
|
||||||
|
def on_change_2(old, new):
|
||||||
|
return str(new) + "_1"
|
||||||
|
|
||||||
|
on_after_change_results = []
|
||||||
|
|
||||||
|
def on_after_change(attr, old, new, results):
|
||||||
|
on_after_change_results.append((attr, old, new, results))
|
||||||
|
|
||||||
|
add_event_listener(ObservableEvent.AFTER_PROPERTY_CHANGE, data, "", on_after_change)
|
||||||
|
bind(data, 'number', on_change_1)
|
||||||
|
bind(data, 'number', on_change_2)
|
||||||
|
bind(data, 'value', on_change_1)
|
||||||
|
|
||||||
|
data.number = 5
|
||||||
|
data.value = "new value"
|
||||||
|
|
||||||
|
assert on_after_change_results == [("number", 1, 5, [5, "5_1"]), ("value", "initial", "new value", ["new value"])]
|
||||||
|
|||||||
Reference in New Issue
Block a user