From 569e343e89ca843c5730efac73ee4d255d99fcbf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mose=20M=C3=BCller?= Date: Tue, 5 Dec 2023 14:10:49 +0100 Subject: [PATCH 1/6] overrides append in _ObservableList --- src/pydase/observer_pattern/observable/observable_object.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/pydase/observer_pattern/observable/observable_object.py b/src/pydase/observer_pattern/observable/observable_object.py index 73603ae..a09e0cf 100644 --- a/src/pydase/observer_pattern/observable/observable_object.py +++ b/src/pydase/observer_pattern/observable/observable_object.py @@ -146,6 +146,11 @@ class _ObservableList(ObservableObject, list[Any]): self._notify_changed(f"[{key}]", value) + def append(self, __object: Any) -> None: + self._initialise_new_objects(f"[{len(self)}]", __object) + super().append(__object) + self._notify_changed("", self) + def _remove_observer_if_observable(self, name: str) -> None: key = int(name[1:-1]) current_value = self.__getitem__(key) From 4bd0092fbf26e854097aa2e238c4786aa9a6c34e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mose=20M=C3=BCller?= Date: Tue, 5 Dec 2023 14:25:40 +0100 Subject: [PATCH 2/6] adds warnings for non-overridden observable-list methods --- .../observable/observable_object.py | 38 ++++++++++++++++- .../observable/test_observable_object.py | 41 +++++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/src/pydase/observer_pattern/observable/observable_object.py b/src/pydase/observer_pattern/observable/observable_object.py index a09e0cf..922018d 100644 --- a/src/pydase/observer_pattern/observable/observable_object.py +++ b/src/pydase/observer_pattern/observable/observable_object.py @@ -1,6 +1,7 @@ import logging from abc import ABC, abstractmethod -from typing import TYPE_CHECKING, Any, ClassVar +from collections.abc import Iterable +from typing import TYPE_CHECKING, Any, ClassVar, SupportsIndex if TYPE_CHECKING: from pydase.observer_pattern.observer.observer import Observer @@ -151,6 +152,41 @@ class _ObservableList(ObservableObject, list[Any]): super().append(__object) self._notify_changed("", self) + def clear(self) -> None: + logger.warning( + "'clear' has not been overridden yet. This might lead to unexpected " + "behaviour." + ) + super().clear() + + def extend(self, __iterable: Iterable[Any]) -> None: + logger.warning( + "'extend' has not been overridden yet. This might lead to unexpected " + "behaviour." + ) + return super().extend(__iterable) + + def insert(self, __index: SupportsIndex, __object: Any) -> None: + logger.warning( + "'insert' has not been overridden yet. This might lead to unexpected " + "behaviour." + ) + super().insert(__index, __object) + + def pop(self, __index: SupportsIndex = -1) -> Any: + logger.warning( + "'pop' has not been overridden yet. This might lead to unexpected " + "behaviour." + ) + return super().pop(__index) + + def remove(self, __value: Any) -> None: + logger.warning( + "'remove' has not been overridden yet. This might lead to unexpected " + "behaviour." + ) + super().remove(__value) + def _remove_observer_if_observable(self, name: str) -> None: key = int(name[1:-1]) current_value = self.__getitem__(key) diff --git a/tests/observer_pattern/observable/test_observable_object.py b/tests/observer_pattern/observable/test_observable_object.py index d12f927..91838e7 100644 --- a/tests/observer_pattern/observable/test_observable_object.py +++ b/tests/observer_pattern/observable/test_observable_object.py @@ -280,3 +280,44 @@ def test_list_in_dict_instance(caplog: pytest.LogCaptureFixture) -> None: instance.list_in_dict["some_list"][0] = "Ciao" assert "'list_in_dict['some_list'][0]' changed to 'Ciao'" in caplog.text + + +def test_list_warnings(caplog: pytest.LogCaptureFixture) -> None: + class MyObservable(Observable): + def __init__(self) -> None: + super().__init__() + self.my_list = [1, 2, 3] + + observable_instance = MyObservable() + + observable_instance.my_list.insert(1, -1) + assert ( + "'insert' has not been overridden yet. This might lead to unexpected " + "behaviour." + ) in caplog.text + caplog.clear() + + observable_instance.my_list.extend([1]) + assert ( + "'extend' has not been overridden yet. This might lead to unexpected " + "behaviour." + ) in caplog.text + caplog.clear() + + observable_instance.my_list.remove(1) + assert ( + "'remove' has not been overridden yet. This might lead to unexpected " + "behaviour." + ) in caplog.text + caplog.clear() + + observable_instance.my_list.pop() + assert ( + "'pop' has not been overridden yet. This might lead to unexpected behaviour." + ) in caplog.text + caplog.clear() + + observable_instance.my_list.clear() + assert ( + "'clear' has not been overridden yet. This might lead to unexpected behaviour." + ) in caplog.text From 3169531a2418c187a6831d4f43123c8f82946a47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mose=20M=C3=BCller?= Date: Tue, 5 Dec 2023 14:45:36 +0100 Subject: [PATCH 3/6] updates property dependencies when changing to an observable object --- src/pydase/data_service/data_service_observer.py | 4 ++++ src/pydase/observer_pattern/observer/property_observer.py | 3 +++ 2 files changed, 7 insertions(+) diff --git a/src/pydase/data_service/data_service_observer.py b/src/pydase/data_service/data_service_observer.py index 4989f5c..1cddb77 100644 --- a/src/pydase/data_service/data_service_observer.py +++ b/src/pydase/data_service/data_service_observer.py @@ -4,6 +4,7 @@ from copy import deepcopy from typing import Any from pydase.data_service.state_manager import StateManager +from pydase.observer_pattern.observable.observable_object import ObservableObject from pydase.observer_pattern.observer.property_observer import ( PropertyObserver, ) @@ -37,6 +38,9 @@ class DataServiceObserver(PropertyObserver): for callback in self._notification_callbacks: callback(full_access_path, value, cached_value_dict) + if isinstance(value, ObservableObject): + self._update_property_deps_dict() + self._notify_dependent_property_changes(full_access_path) def _update_cache_value( diff --git a/src/pydase/observer_pattern/observer/property_observer.py b/src/pydase/observer_pattern/observer/property_observer.py index 9298d3e..f543e0c 100644 --- a/src/pydase/observer_pattern/observer/property_observer.py +++ b/src/pydase/observer_pattern/observer/property_observer.py @@ -29,6 +29,9 @@ def get_property_dependencies(prop: property, prefix: str = "") -> list[str]: class PropertyObserver(Observer): def __init__(self, observable: Observable) -> None: super().__init__(observable) + self._update_property_deps_dict() + + def _update_property_deps_dict(self) -> None: self.property_deps_dict = reverse_dict( self._get_properties_and_their_dependencies(self.observable) ) From 5a76d76d2b927f44e61f139ed31ec179a71144e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mose=20M=C3=BCller?= Date: Tue, 5 Dec 2023 14:58:12 +0100 Subject: [PATCH 4/6] adds test for (dynamic / static) property dependencies --- .../test_data_service_observer.py | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 tests/data_service/test_data_service_observer.py diff --git a/tests/data_service/test_data_service_observer.py b/tests/data_service/test_data_service_observer.py new file mode 100644 index 0000000..a15e97d --- /dev/null +++ b/tests/data_service/test_data_service_observer.py @@ -0,0 +1,76 @@ +import logging + +import pydase +from pydase.data_service.data_service_observer import DataServiceObserver +from pydase.data_service.state_manager import StateManager + +logger = logging.getLogger() + + +def test_static_property_dependencies() -> None: + class SubClass(pydase.DataService): + _name = "SubClass" + + @property + def name(self) -> str: + return self._name + + @name.setter + def name(self, value: str) -> None: + self._name = value + + class ServiceClass(pydase.DataService): + def __init__(self) -> None: + super().__init__() + self.list_attr = [SubClass()] + self._name = "ServiceClass" + + @property + def name(self) -> str: + return self._name + + @name.setter + def name(self, value: str) -> None: + self._name = value + + service_instance = ServiceClass() + state_manager = StateManager(service_instance) + observer = DataServiceObserver(state_manager) + logger.debug(observer.property_deps_dict) + assert observer.property_deps_dict == { + "list_attr[0]._name": ["list_attr[0].name"], + "_name": ["name"], + } + + +def test_dynamic_list_property_dependencies() -> None: + class SubClass(pydase.DataService): + _name = "SubClass" + + @property + def name(self) -> str: + return self._name + + @name.setter + def name(self, value: str) -> None: + self._name = value + + class ServiceClass(pydase.DataService): + def __init__(self) -> None: + super().__init__() + self.list_attr = [SubClass()] + + service_instance = ServiceClass() + state_manager = StateManager(service_instance) + observer = DataServiceObserver(state_manager) + + assert observer.property_deps_dict == { + "list_attr[0]._name": ["list_attr[0].name"], + } + + service_instance.list_attr.append(SubClass()) + + assert observer.property_deps_dict == { + "list_attr[0]._name": ["list_attr[0].name"], + "list_attr[1]._name": ["list_attr[1].name"], + } From f6bf229c8cbb59a733508b5e5186cfeb8a5438a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mose=20M=C3=BCller?= Date: Wed, 6 Dec 2023 17:25:09 +0100 Subject: [PATCH 5/6] updates ruff config (and workflow) --- .github/workflows/python-package.yml | 2 ++ pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/python-package.yml b/.github/workflows/python-package.yml index 3dbc323..999efea 100644 --- a/.github/workflows/python-package.yml +++ b/.github/workflows/python-package.yml @@ -21,6 +21,8 @@ jobs: steps: - uses: actions/checkout@v3 - uses: chartboost/ruff-action@v1 + with: + src: "./src" - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v3 with: diff --git a/pyproject.toml b/pyproject.toml index f9d5718..5e03ccf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -82,7 +82,7 @@ ignore = [ "PERF203", # try-except-in-loop ] extend-exclude = [ - "docs", "frontend", "tests" + "docs", "frontend" ] [tool.ruff.lint.mccabe] From 8e641c1b8457a58cdb0c1966457713431ddd2bc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mose=20M=C3=BCller?= Date: Wed, 6 Dec 2023 18:02:26 +0100 Subject: [PATCH 6/6] implements clear, insert, remove, extend and pop for observable lists --- .../observable/observable_object.py | 69 ++++-- .../observable/test_observable_object.py | 233 +++++++++++++++--- 2 files changed, 237 insertions(+), 65 deletions(-) diff --git a/src/pydase/observer_pattern/observable/observable_object.py b/src/pydase/observer_pattern/observable/observable_object.py index 922018d..1f56644 100644 --- a/src/pydase/observer_pattern/observable/observable_object.py +++ b/src/pydase/observer_pattern/observable/observable_object.py @@ -153,39 +153,60 @@ class _ObservableList(ObservableObject, list[Any]): self._notify_changed("", self) def clear(self) -> None: - logger.warning( - "'clear' has not been overridden yet. This might lead to unexpected " - "behaviour." - ) + self._remove_self_from_observables() + super().clear() + self._notify_changed("", self) + def extend(self, __iterable: Iterable[Any]) -> None: - logger.warning( - "'extend' has not been overridden yet. This might lead to unexpected " - "behaviour." - ) - return super().extend(__iterable) + self._remove_self_from_observables() + + try: + super().extend(__iterable) + finally: + for i, item in enumerate(self): + super().__setitem__(i, self._initialise_new_objects(f"[{i}]", item)) + + self._notify_changed("", self) def insert(self, __index: SupportsIndex, __object: Any) -> None: - logger.warning( - "'insert' has not been overridden yet. This might lead to unexpected " - "behaviour." - ) - super().insert(__index, __object) + self._remove_self_from_observables() + + try: + super().insert(__index, __object) + finally: + for i, item in enumerate(self): + super().__setitem__(i, self._initialise_new_objects(f"[{i}]", item)) + + self._notify_changed("", self) def pop(self, __index: SupportsIndex = -1) -> Any: - logger.warning( - "'pop' has not been overridden yet. This might lead to unexpected " - "behaviour." - ) - return super().pop(__index) + self._remove_self_from_observables() + + try: + popped_item = super().pop(__index) + finally: + for i, item in enumerate(self): + super().__setitem__(i, self._initialise_new_objects(f"[{i}]", item)) + + self._notify_changed("", self) + return popped_item def remove(self, __value: Any) -> None: - logger.warning( - "'remove' has not been overridden yet. This might lead to unexpected " - "behaviour." - ) - super().remove(__value) + self._remove_self_from_observables() + + try: + super().remove(__value) + finally: + for i, item in enumerate(self): + super().__setitem__(i, self._initialise_new_objects(f"[{i}]", item)) + + self._notify_changed("", self) + + def _remove_self_from_observables(self) -> None: + for i in range(len(self)): + self._remove_observer_if_observable(f"[{i}]") def _remove_observer_if_observable(self, name: str) -> None: key = int(name[1:-1]) diff --git a/tests/observer_pattern/observable/test_observable_object.py b/tests/observer_pattern/observable/test_observable_object.py index 91838e7..0b56380 100644 --- a/tests/observer_pattern/observable/test_observable_object.py +++ b/tests/observer_pattern/observable/test_observable_object.py @@ -20,7 +20,7 @@ def test_simple_instance_list_attribute(caplog: pytest.LogCaptureFixture) -> Non self.list_attr = [1, 2] instance = MyObservable() - observer = MyObserver(instance) + MyObserver(instance) instance.list_attr[0] = 12 assert "'list_attr[0]' changed to '12'" in caplog.text @@ -38,7 +38,7 @@ def test_instance_object_list_attribute(caplog: pytest.LogCaptureFixture) -> Non self.list_attr = [NestedObservable()] instance = MyObservable() - observer = MyObserver(instance) + MyObserver(instance) instance.list_attr[0].name = "Ciao" assert "'list_attr[0].name' changed to 'Ciao'" in caplog.text @@ -49,7 +49,7 @@ def test_simple_class_list_attribute(caplog: pytest.LogCaptureFixture) -> None: list_attr = [1, 2] instance = MyObservable() - observer = MyObserver(instance) + MyObserver(instance) instance.list_attr[0] = 12 assert "'list_attr[0]' changed to '12'" in caplog.text @@ -63,7 +63,7 @@ def test_class_object_list_attribute(caplog: pytest.LogCaptureFixture) -> None: list_attr = [NestedObservable()] instance = MyObservable() - observer = MyObserver(instance) + MyObserver(instance) instance.list_attr[0].name = "Ciao" assert "'list_attr[0].name' changed to 'Ciao'" in caplog.text @@ -76,7 +76,7 @@ def test_simple_instance_dict_attribute(caplog: pytest.LogCaptureFixture) -> Non self.dict_attr = {"first": "Hello"} instance = MyObservable() - observer = MyObserver(instance) + MyObserver(instance) instance.dict_attr["first"] = "Ciao" instance.dict_attr["second"] = "World" @@ -89,7 +89,7 @@ def test_simple_class_dict_attribute(caplog: pytest.LogCaptureFixture) -> None: dict_attr = {"first": "Hello"} instance = MyObservable() - observer = MyObserver(instance) + MyObserver(instance) instance.dict_attr["first"] = "Ciao" instance.dict_attr["second"] = "World" @@ -109,7 +109,7 @@ def test_instance_dict_attribute(caplog: pytest.LogCaptureFixture) -> None: self.dict_attr = {"first": NestedObservable()} instance = MyObservable() - observer = MyObserver(instance) + MyObserver(instance) instance.dict_attr["first"].name = "Ciao" assert "'dict_attr['first'].name' changed to 'Ciao'" in caplog.text @@ -123,7 +123,7 @@ def test_class_dict_attribute(caplog: pytest.LogCaptureFixture) -> None: dict_attr = {"first": NestedObservable()} instance = MyObservable() - observer = MyObserver(instance) + MyObserver(instance) instance.dict_attr["first"].name = "Ciao" assert "'dict_attr['first'].name' changed to 'Ciao'" in caplog.text @@ -140,7 +140,7 @@ def test_removed_observer_on_class_list_attr(caplog: pytest.LogCaptureFixture) - changed_list_attr = [nested_instance] instance = MyObservable() - observer = MyObserver(instance) + MyObserver(instance) instance.changed_list_attr[0] = "Ciao" assert "'changed_list_attr[0]' changed to 'Ciao'" in caplog.text @@ -169,7 +169,7 @@ def test_removed_observer_on_instance_dict_attr( self.changed_dict_attr = {"nested": nested_instance} instance = MyObservable() - observer = MyObserver(instance) + MyObserver(instance) instance.changed_dict_attr["nested"] = "Ciao" assert "'changed_dict_attr['nested']' changed to 'Ciao'" in caplog.text @@ -198,7 +198,7 @@ def test_removed_observer_on_instance_list_attr( self.changed_list_attr = [nested_instance] instance = MyObservable() - observer = MyObserver(instance) + MyObserver(instance) instance.changed_list_attr[0] = "Ciao" assert "'changed_list_attr[0]' changed to 'Ciao'" in caplog.text @@ -225,7 +225,7 @@ def test_removed_observer_on_class_dict_attr(caplog: pytest.LogCaptureFixture) - self.changed_dict_attr = {"nested": nested_instance} instance = MyObservable() - observer = MyObserver(instance) + MyObserver(instance) instance.changed_dict_attr["nested"] = "Ciao" assert "'changed_dict_attr['nested']' changed to 'Ciao'" in caplog.text @@ -246,7 +246,7 @@ def test_nested_dict_instances(caplog: pytest.LogCaptureFixture) -> None: self.nested_dict_attr = {"nested": dict_instance} instance = MyObservable() - observer = MyObserver(instance) + MyObserver(instance) instance.nested_dict_attr["nested"]["first"] = "Ciao" assert "'nested_dict_attr['nested']['first']' changed to 'Ciao'" in caplog.text @@ -261,14 +261,14 @@ def test_dict_in_list_instance(caplog: pytest.LogCaptureFixture) -> None: self.dict_in_list = [dict_instance] instance = MyObservable() - observer = MyObserver(instance) + MyObserver(instance) instance.dict_in_list[0]["first"] = "Ciao" assert "'dict_in_list[0]['first']' changed to 'Ciao'" in caplog.text def test_list_in_dict_instance(caplog: pytest.LogCaptureFixture) -> None: - list_instance = [1, 2, 3] + list_instance: list[Any] = [1, 2, 3] class MyObservable(Observable): def __init__(self) -> None: @@ -276,48 +276,199 @@ def test_list_in_dict_instance(caplog: pytest.LogCaptureFixture) -> None: self.list_in_dict = {"some_list": list_instance} instance = MyObservable() - observer = MyObserver(instance) + MyObserver(instance) instance.list_in_dict["some_list"][0] = "Ciao" assert "'list_in_dict['some_list'][0]' changed to 'Ciao'" in caplog.text -def test_list_warnings(caplog: pytest.LogCaptureFixture) -> None: +def test_list_append(caplog: pytest.LogCaptureFixture) -> None: + class OtherObservable(Observable): + def __init__(self) -> None: + super().__init__() + self.greeting = "Other Observable" + class MyObservable(Observable): def __init__(self) -> None: super().__init__() - self.my_list = [1, 2, 3] + self.my_list = [] observable_instance = MyObservable() + MyObserver(observable_instance) - observable_instance.my_list.insert(1, -1) - assert ( - "'insert' has not been overridden yet. This might lead to unexpected " - "behaviour." - ) in caplog.text + observable_instance.my_list.append(OtherObservable()) + assert f"'my_list' changed to '{observable_instance.my_list}'" in caplog.text caplog.clear() - observable_instance.my_list.extend([1]) - assert ( - "'extend' has not been overridden yet. This might lead to unexpected " - "behaviour." - ) in caplog.text + observable_instance.my_list.append(OtherObservable()) + assert f"'my_list' changed to '{observable_instance.my_list}'" in caplog.text caplog.clear() - observable_instance.my_list.remove(1) - assert ( - "'remove' has not been overridden yet. This might lead to unexpected " - "behaviour." - ) in caplog.text + observable_instance.my_list[0].greeting = "Hi" + observable_instance.my_list[1].greeting = "Hello" + + assert observable_instance.my_list[0].greeting == "Hi" + assert observable_instance.my_list[1].greeting == "Hello" + assert "'my_list[0].greeting' changed to 'Hi'" in caplog.text + assert "'my_list[1].greeting' changed to 'Hello'" in caplog.text + + +def test_list_pop(caplog: pytest.LogCaptureFixture) -> None: + class OtherObservable(Observable): + def __init__(self) -> None: + super().__init__() + self.greeting = "Hello there!" + + class MyObservable(Observable): + def __init__(self) -> None: + super().__init__() + self.my_list = [OtherObservable() for _ in range(2)] + + observable_instance = MyObservable() + MyObserver(observable_instance) + + popped_instance = observable_instance.my_list.pop(0) + + assert len(observable_instance.my_list) == 1 + assert f"'my_list' changed to '{observable_instance.my_list}'" in caplog.text + + # checks if observer is removed + popped_instance.greeting = "Ciao" + assert "'my_list[0].greeting' changed to 'Ciao'" not in caplog.text caplog.clear() - observable_instance.my_list.pop() - assert ( - "'pop' has not been overridden yet. This might lead to unexpected behaviour." - ) in caplog.text - caplog.clear() + # checks if observer keys have been updated (index 1 moved to 0) + observable_instance.my_list[0].greeting = "Hi" + assert "'my_list[0].greeting' changed to 'Hi'" in caplog.text + + +def test_list_clear(caplog: pytest.LogCaptureFixture) -> None: + class OtherObservable(Observable): + def __init__(self) -> None: + super().__init__() + self.greeting = "Hello there!" + + other_observable_instance = OtherObservable() + + class MyObservable(Observable): + def __init__(self) -> None: + super().__init__() + self.my_list = [other_observable_instance] + + observable_instance = MyObservable() + MyObserver(observable_instance) + + other_observable_instance.greeting = "Hello" + assert "'my_list[0].greeting' changed to 'Hello'" in caplog.text observable_instance.my_list.clear() - assert ( - "'clear' has not been overridden yet. This might lead to unexpected behaviour." - ) in caplog.text + + assert len(observable_instance.my_list) == 0 + assert "'my_list' changed to '[]'" in caplog.text + + # checks if observer has been removed + other_observable_instance.greeting = "Hi" + assert "'my_list[0].greeting' changed to 'Hi'" not in caplog.text + + +def test_list_extend(caplog: pytest.LogCaptureFixture) -> None: + class OtherObservable(Observable): + def __init__(self) -> None: + super().__init__() + self.greeting = "Hello there!" + + other_observable_instance = OtherObservable() + + class MyObservable(Observable): + def __init__(self) -> None: + super().__init__() + self.my_list = [] + + observable_instance = MyObservable() + MyObserver(observable_instance) + + other_observable_instance.greeting = "Hello" + assert "'my_list[0].greeting' changed to 'Hello'" not in caplog.text + + observable_instance.my_list.extend([other_observable_instance, OtherObservable()]) + + assert len(observable_instance.my_list) == 2 + assert f"'my_list' changed to '{observable_instance.my_list}'" in caplog.text + + # checks if observer has been removed + other_observable_instance.greeting = "Hi" + assert "'my_list[0].greeting' changed to 'Hi'" in caplog.text + observable_instance.my_list[1].greeting = "Ciao" + assert "'my_list[1].greeting' changed to 'Ciao'" in caplog.text + + +def test_list_insert(caplog: pytest.LogCaptureFixture) -> None: + class OtherObservable(Observable): + def __init__(self) -> None: + super().__init__() + self.greeting = "Hello there!" + + other_observable_instance_1 = OtherObservable() + other_observable_instance_2 = OtherObservable() + + class MyObservable(Observable): + def __init__(self) -> None: + super().__init__() + self.my_list = [other_observable_instance_1, OtherObservable()] + + observable_instance = MyObservable() + MyObserver(observable_instance) + + other_observable_instance_1.greeting = "Hello" + assert "'my_list[0].greeting' changed to 'Hello'" in caplog.text + + observable_instance.my_list.insert(0, other_observable_instance_2) + + assert len(observable_instance.my_list) == 3 + assert f"'my_list' changed to '{observable_instance.my_list}'" in caplog.text + + # checks if observer keys have been updated + other_observable_instance_2.greeting = "Hey" + other_observable_instance_1.greeting = "Hi" + observable_instance.my_list[2].greeting = "Ciao" + + assert "'my_list[0].greeting' changed to 'Hey'" in caplog.text + assert "'my_list[1].greeting' changed to 'Hi'" in caplog.text + assert "'my_list[2].greeting' changed to 'Ciao'" in caplog.text + + +def test_list_remove(caplog: pytest.LogCaptureFixture) -> None: + class OtherObservable(Observable): + def __init__(self) -> None: + super().__init__() + self.greeting = "Hello there!" + + other_observable_instance_1 = OtherObservable() + other_observable_instance_2 = OtherObservable() + + class MyObservable(Observable): + def __init__(self) -> None: + super().__init__() + self.my_list = [other_observable_instance_1, other_observable_instance_2] + + observable_instance = MyObservable() + MyObserver(observable_instance) + + other_observable_instance_1.greeting = "Hello" + other_observable_instance_2.greeting = "Hi" + caplog.clear() + + observable_instance.my_list.remove(other_observable_instance_1) + + assert len(observable_instance.my_list) == 1 + assert f"'my_list' changed to '{observable_instance.my_list}'" in caplog.text + caplog.clear() + + # checks if observer has been removed + other_observable_instance_1.greeting = "Hi" + assert "'my_list[0].greeting' changed to 'Hi'" not in caplog.text + caplog.clear() + + # checks if observer key was updated correctly (was index 1) + other_observable_instance_2.greeting = "Ciao" + assert "'my_list[0].greeting' changed to 'Ciao'" in caplog.text