173 lines
3.8 KiB
Python
173 lines
3.8 KiB
Python
import pytest
|
|
|
|
from myutils.observable import NotObservableError, make_observable, bind
|
|
|
|
|
|
# Test fixtures
|
|
class Demo:
|
|
def __init__(self):
|
|
self.number = 1
|
|
|
|
def get_double(self):
|
|
return self.number * 2
|
|
|
|
|
|
# Tests
|
|
def test_i_can_make_an_object_observable():
|
|
demo = Demo()
|
|
result = make_observable(demo)
|
|
|
|
assert hasattr(demo, '_listeners')
|
|
assert isinstance(demo._listeners, dict)
|
|
assert result is demo
|
|
|
|
|
|
def test_i_can_bind_a_callback_to_an_attribute():
|
|
demo = Demo()
|
|
make_observable(demo)
|
|
|
|
callback = lambda old, new: None
|
|
bind(demo, 'number', callback)
|
|
|
|
assert 'number' in demo._listeners
|
|
assert callback in demo._listeners['number']
|
|
|
|
|
|
def test_i_can_receive_notification_when_attribute_changes():
|
|
demo = Demo()
|
|
make_observable(demo)
|
|
|
|
called = []
|
|
bind(demo, 'number', lambda old, new: called.append(True))
|
|
|
|
demo.number = 5
|
|
|
|
assert len(called) == 1
|
|
|
|
|
|
def test_i_can_receive_old_and_new_values_in_callback():
|
|
demo = Demo()
|
|
make_observable(demo)
|
|
|
|
values = []
|
|
bind(demo, 'number', lambda old, new: values.append((old, new)))
|
|
|
|
demo.number = 5
|
|
demo.number = 10
|
|
|
|
assert values == [(1, 5), (5, 10)]
|
|
|
|
|
|
def test_i_can_bind_multiple_callbacks_to_same_attribute():
|
|
demo = Demo()
|
|
make_observable(demo)
|
|
|
|
calls1 = []
|
|
calls2 = []
|
|
bind(demo, 'number', lambda old, new: calls1.append(new))
|
|
bind(demo, 'number', lambda old, new: calls2.append(new))
|
|
|
|
demo.number = 5
|
|
|
|
assert calls1 == [5]
|
|
assert calls2 == [5]
|
|
|
|
|
|
def test_i_can_bind_callbacks_to_different_attributes():
|
|
demo = Demo()
|
|
make_observable(demo)
|
|
|
|
number_calls = []
|
|
name_calls = []
|
|
bind(demo, 'number', lambda old, new: number_calls.append(new))
|
|
bind(demo, 'name', lambda old, new: name_calls.append(new))
|
|
|
|
demo.number = 5
|
|
demo.name = "test"
|
|
|
|
assert number_calls == [5]
|
|
assert name_calls == ["test"]
|
|
|
|
|
|
def test_i_can_modify_non_observed_attributes_without_notification():
|
|
demo = Demo()
|
|
make_observable(demo)
|
|
|
|
called = []
|
|
bind(demo, 'number', lambda old, new: called.append(True))
|
|
|
|
demo.other_attr = "value"
|
|
|
|
assert len(called) == 0
|
|
assert demo.other_attr == "value"
|
|
|
|
|
|
def test_i_can_have_multiple_instances_with_independent_observers():
|
|
demo1 = Demo()
|
|
demo2 = Demo()
|
|
|
|
make_observable(demo1)
|
|
|
|
calls1 = []
|
|
bind(demo1, 'number', lambda old, new: calls1.append(new))
|
|
|
|
demo1.number = 5
|
|
demo2.number = 10
|
|
|
|
# Only demo1 should trigger callback
|
|
assert calls1 == [5]
|
|
assert demo1.number == 5
|
|
assert demo2.number == 10
|
|
|
|
|
|
def test_i_can_call_make_observable_multiple_times_safely():
|
|
demo = Demo()
|
|
|
|
result1 = make_observable(demo)
|
|
result2 = make_observable(demo)
|
|
|
|
assert result1 is result2
|
|
assert hasattr(demo, '_listeners')
|
|
|
|
# Should still work normally
|
|
called = []
|
|
bind(demo, 'number', lambda old, new: called.append(new))
|
|
demo.number = 5
|
|
|
|
assert len(called) == 1
|
|
|
|
|
|
def test_i_cannot_bind_before_making_observable():
|
|
demo = Demo()
|
|
|
|
with pytest.raises(NotObservableError) as exc_info:
|
|
bind(demo, 'number', lambda old, new: None)
|
|
|
|
assert "must be made observable" in str(exc_info.value).lower()
|
|
|
|
|
|
def test_i_can_set_attribute_that_does_not_exist_yet():
|
|
demo = Demo()
|
|
make_observable(demo)
|
|
|
|
values = []
|
|
bind(demo, 'new_attr', lambda old, new: values.append((old, new)))
|
|
|
|
demo.new_attr = "first_value"
|
|
|
|
assert values == [(None, "first_value")]
|
|
assert demo.new_attr == "first_value"
|
|
|
|
|
|
def test_i_can_preserve_original_class_behavior():
|
|
demo = Demo()
|
|
make_observable(demo)
|
|
|
|
# Test that original methods still work
|
|
assert demo.get_double() == 2
|
|
|
|
demo.number = 5
|
|
assert demo.get_double() == 10
|
|
|
|
# Test that isinstance still works
|
|
assert isinstance(demo, Demo) |