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)