Added add_event_listener and remove_event_listener

This commit is contained in:
2025-11-09 18:05:41 +01:00
parent d62b135cee
commit 0ef9523e6b
4 changed files with 594 additions and 24 deletions

View File

@@ -1,7 +1,7 @@
import pytest
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
@@ -552,6 +552,337 @@ def test_i_cannot_get_listener_count_on_non_observable_object(data):
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
# ============================================================================
@@ -584,24 +915,133 @@ def test_multiple_bind_unbind_cycles(observable_data):
assert len(results) == 2
def test_same_callback_can_be_bound_to_multiple_attributes(observable_data):
"""
The same callback can be bound to multiple attributes and unbound independently.
"""
results = []
# ============================================================================
# After property changed event
# ============================================================================
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):
results.append((old, new))
on_change_results = []
on_after_change_results = []
bind(observable_data, "value", callback)
bind(observable_data, "count", callback)
def on_change(old, new):
on_change_results.append((old, new))
return new + 1
observable_data.value = "first"
observable_data.count = 1
assert len(results) == 2
def on_after_change(attr, old, new, results):
on_after_change_results.append((attr, old, new, results))
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"
observable_data.count = 2
assert len(results) == 3 # Only count callback triggered
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])]
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"])]