From bb3d6fcce19ce50c6bcaf0500774ac4a34692cac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mose=20M=C3=BCller?= Date: Tue, 30 Apr 2024 07:25:42 +0200 Subject: [PATCH] updates _ObservableDict - allows for strings and numbers now - key will have double quotes (") instead of single quote (') when key is a string - fixed some few things - added/updated tests --- .../observable/observable_object.py | 39 +++++++++----- .../observable/test_observable_object.py | 53 ++++++++++++------- 2 files changed, 61 insertions(+), 31 deletions(-) diff --git a/src/pydase/observer_pattern/observable/observable_object.py b/src/pydase/observer_pattern/observable/observable_object.py index 40ef632..333f633 100644 --- a/src/pydase/observer_pattern/observable/observable_object.py +++ b/src/pydase/observer_pattern/observable/observable_object.py @@ -3,6 +3,8 @@ from abc import ABC, abstractmethod from collections.abc import Iterable from typing import TYPE_CHECKING, Any, ClassVar, SupportsIndex +from pydase.utils.helpers import parse_serialized_key + if TYPE_CHECKING: from pydase.observer_pattern.observer.observer import Observer @@ -81,7 +83,7 @@ class ObservableObject(ABC): ) observer._notify_change_start(extended_attr_path) - def _initialise_new_objects(self, attr_name_or_key: Any, value: Any) -> Any: + def _initialise_new_objects(self, attr_name_or_key: str, value: Any) -> Any: new_value = value if isinstance(value, list): if id(value) in self._list_mapping: @@ -100,7 +102,7 @@ class ObservableObject(ABC): new_value = _ObservableDict(original_dict=value) self._dict_mapping[id(value)] = new_value if isinstance(new_value, ObservableObject): - new_value.add_observer(self, str(attr_name_or_key)) + new_value.add_observer(self, attr_name_or_key) return new_value @abstractmethod @@ -224,33 +226,44 @@ class _ObservableList(ObservableObject, list[Any]): return instance_attr_name -class _ObservableDict(dict[str, Any], ObservableObject): +class _ObservableDict(dict[str | float, Any], ObservableObject): def __init__( self, - original_dict: dict[str, Any], + original_dict: dict[str | float, Any], ) -> None: self._original_dict = original_dict ObservableObject.__init__(self) dict.__init__(self) for key, value in self._original_dict.items(): - super().__setitem__(key, self._initialise_new_objects(f"['{key}']", value)) + observer_key = key if not isinstance(key, str) else f'"{key}"' + super().__setitem__( + key, self._initialise_new_objects(f"[{observer_key}]", value) + ) - def __setitem__(self, key: str, value: Any) -> None: - if not isinstance(key, str): - logger.warning("Converting non-string dictionary key %s to string.", key) + def __setitem__(self, key: str | float, value: Any) -> None: + if not isinstance(key, str | int | float): + logger.warning( + "Dictionary key %s is neither string nor number. Converting to string" + "...", + key, + ) key = str(key) + observer_key = key + if isinstance(key, str): + observer_key = f'"{key}"' + if hasattr(self, "_observers"): - self._remove_observer_if_observable(f"['{key}']") - value = self._initialise_new_objects(key, value) - self._notify_change_start(f"['{key}']") + self._remove_observer_if_observable(f"[{observer_key}]") + value = self._initialise_new_objects(f"[{observer_key}]", value) + self._notify_change_start(f"[{observer_key}]") super().__setitem__(key, value) - self._notify_changed(f"['{key}']", value) + self._notify_changed(f"[{observer_key}]", value) def _remove_observer_if_observable(self, name: str) -> None: - key = name[2:-2] + key = parse_serialized_key(name) current_value = self.get(key, None) if isinstance(current_value, ObservableObject): diff --git a/tests/observer_pattern/observable/test_observable_object.py b/tests/observer_pattern/observable/test_observable_object.py index 0b56380..70d7324 100644 --- a/tests/observer_pattern/observable/test_observable_object.py +++ b/tests/observer_pattern/observable/test_observable_object.py @@ -80,8 +80,8 @@ def test_simple_instance_dict_attribute(caplog: pytest.LogCaptureFixture) -> Non instance.dict_attr["first"] = "Ciao" instance.dict_attr["second"] = "World" - assert "'dict_attr['first']' changed to 'Ciao'" in caplog.text - assert "'dict_attr['second']' changed to 'World'" in caplog.text + assert "'dict_attr[\"first\"]' changed to 'Ciao'" in caplog.text + assert "'dict_attr[\"second\"]' changed to 'World'" in caplog.text def test_simple_class_dict_attribute(caplog: pytest.LogCaptureFixture) -> None: @@ -93,8 +93,8 @@ def test_simple_class_dict_attribute(caplog: pytest.LogCaptureFixture) -> None: instance.dict_attr["first"] = "Ciao" instance.dict_attr["second"] = "World" - assert "'dict_attr['first']' changed to 'Ciao'" in caplog.text - assert "'dict_attr['second']' changed to 'World'" in caplog.text + assert "'dict_attr[\"first\"]' changed to 'Ciao'" in caplog.text + assert "'dict_attr[\"second\"]' changed to 'World'" in caplog.text def test_instance_dict_attribute(caplog: pytest.LogCaptureFixture) -> None: @@ -112,7 +112,7 @@ def test_instance_dict_attribute(caplog: pytest.LogCaptureFixture) -> None: MyObserver(instance) instance.dict_attr["first"].name = "Ciao" - assert "'dict_attr['first'].name' changed to 'Ciao'" in caplog.text + assert "'dict_attr[\"first\"].name' changed to 'Ciao'" in caplog.text def test_class_dict_attribute(caplog: pytest.LogCaptureFixture) -> None: @@ -126,7 +126,7 @@ def test_class_dict_attribute(caplog: pytest.LogCaptureFixture) -> None: MyObserver(instance) instance.dict_attr["first"].name = "Ciao" - assert "'dict_attr['first'].name' changed to 'Ciao'" in caplog.text + assert "'dict_attr[\"first\"].name' changed to 'Ciao'" in caplog.text def test_removed_observer_on_class_list_attr(caplog: pytest.LogCaptureFixture) -> None: @@ -166,19 +166,28 @@ def test_removed_observer_on_instance_dict_attr( def __init__(self) -> None: super().__init__() self.nested_attr = nested_instance - self.changed_dict_attr = {"nested": nested_instance} + self.changed_dict_attr = {"nested": nested_instance, 2.1: nested_instance} instance = MyObservable() MyObserver(instance) instance.changed_dict_attr["nested"] = "Ciao" + instance.changed_dict_attr[2.1] = "foo" - assert "'changed_dict_attr['nested']' changed to 'Ciao'" in caplog.text + assert "'changed_dict_attr[\"nested\"]' changed to 'Ciao'" in caplog.text + assert "'changed_dict_attr[2.1]' changed to 'foo'" in caplog.text caplog.clear() + assert nested_instance._observers == { + '["nested"]': [], + "[2.1]": [], + "nested_attr": [instance], + } + instance.nested_attr.name = "Hi" assert "'nested_attr.name' changed to 'Hi'" in caplog.text - assert "'changed_dict_attr['nested'].name' changed to 'Hi'" not in caplog.text + assert "'changed_dict_attr[\"nested\"].name' changed to 'Hi'" not in caplog.text + assert "'changed_dict_attr[2.1].name' changed to 'Hi'" not in caplog.text def test_removed_observer_on_instance_list_attr( @@ -217,24 +226,32 @@ def test_removed_observer_on_class_dict_attr(caplog: pytest.LogCaptureFixture) - self.name = "Hello" nested_instance = NestedObservable() + assert nested_instance._observers == {} class MyObservable(Observable): - def __init__(self) -> None: - super().__init__() - self.nested_attr = nested_instance - self.changed_dict_attr = {"nested": nested_instance} + nested_attr = nested_instance + changed_dict_attr = {"nested": nested_instance, 2.1: nested_instance} instance = MyObservable() MyObserver(instance) instance.changed_dict_attr["nested"] = "Ciao" + instance.changed_dict_attr[2.1] = "foo" - assert "'changed_dict_attr['nested']' changed to 'Ciao'" in caplog.text + assert "'changed_dict_attr[\"nested\"]' changed to 'Ciao'" in caplog.text + assert "'changed_dict_attr[2.1]' changed to 'foo'" in caplog.text caplog.clear() + assert nested_instance._observers == { + '["nested"]': [], + "[2.1]": [], + "nested_attr": [instance], + } + instance.nested_attr.name = "Hi" assert "'nested_attr.name' changed to 'Hi'" in caplog.text - assert "'changed_dict_attr['nested'].name' changed to 'Hi'" not in caplog.text + assert "'changed_dict_attr[\"nested\"].name' changed to 'Hi'" not in caplog.text + assert "'changed_dict_attr[2.1].name' changed to 'Hi'" not in caplog.text def test_nested_dict_instances(caplog: pytest.LogCaptureFixture) -> None: @@ -249,7 +266,7 @@ def test_nested_dict_instances(caplog: pytest.LogCaptureFixture) -> None: MyObserver(instance) instance.nested_dict_attr["nested"]["first"] = "Ciao" - assert "'nested_dict_attr['nested']['first']' changed to 'Ciao'" in caplog.text + assert "'nested_dict_attr[\"nested\"][\"first\"]' changed to 'Ciao'" in caplog.text def test_dict_in_list_instance(caplog: pytest.LogCaptureFixture) -> None: @@ -264,7 +281,7 @@ def test_dict_in_list_instance(caplog: pytest.LogCaptureFixture) -> None: MyObserver(instance) instance.dict_in_list[0]["first"] = "Ciao" - assert "'dict_in_list[0]['first']' changed to 'Ciao'" in caplog.text + assert "'dict_in_list[0][\"first\"]' changed to 'Ciao'" in caplog.text def test_list_in_dict_instance(caplog: pytest.LogCaptureFixture) -> None: @@ -279,7 +296,7 @@ def test_list_in_dict_instance(caplog: pytest.LogCaptureFixture) -> None: MyObserver(instance) instance.list_in_dict["some_list"][0] = "Ciao" - assert "'list_in_dict['some_list'][0]' changed to 'Ciao'" in caplog.text + assert "'list_in_dict[\"some_list\"][0]' changed to 'Ciao'" in caplog.text def test_list_append(caplog: pytest.LogCaptureFixture) -> None: