From f2cf0d9c1a6403f3ec5c2a701f968a84056dc0dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mose=20M=C3=BCller?= Date: Tue, 5 Mar 2024 13:23:26 +0100 Subject: [PATCH 1/7] fixes update of cache when the type has changed When an attribute changes from, say, a quantity to an enumeration, the enum key in the serialization was not added to the cache, and thus the frontend was not able to render the enum. --- src/pydase/utils/serializer.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/pydase/utils/serializer.py b/src/pydase/utils/serializer.py index 669021b..32b30ce 100644 --- a/src/pydase/utils/serializer.py +++ b/src/pydase/utils/serializer.py @@ -269,12 +269,11 @@ def set_nested_value_by_path( # setting the new value serialized_value = dump(value) - if "readonly" in current_dict: - if current_dict["type"] != "method": - current_dict["type"] = serialized_value["type"] - current_dict["value"] = serialized_value["value"] - else: - current_dict.update(serialized_value) + serialized_value.pop("readonly", None) + value_type = serialized_value.pop("type") + if "readonly" in current_dict and current_dict["type"] != "method": + current_dict["type"] = value_type + current_dict.update(serialized_value) def get_nested_dict_by_path( From 8971cebfcdbc5bae3455c5beb32e64eb57bd84e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mose=20M=C3=BCller?= Date: Tue, 5 Mar 2024 13:24:15 +0100 Subject: [PATCH 2/7] adds todos --- src/pydase/data_service/data_service_observer.py | 2 ++ src/pydase/utils/serializer.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/src/pydase/data_service/data_service_observer.py b/src/pydase/data_service/data_service_observer.py index aeabfef..ef4e85d 100644 --- a/src/pydase/data_service/data_service_observer.py +++ b/src/pydase/data_service/data_service_observer.py @@ -44,6 +44,8 @@ class DataServiceObserver(PropertyObserver): self._update_cache_value(full_access_path, value, cached_value_dict) + # TODO: get the cached value again -> _update_cache_value already put the + # right thing into the cache for callback in self._notification_callbacks: callback(full_access_path, value, cached_value_dict) diff --git a/src/pydase/utils/serializer.py b/src/pydase/utils/serializer.py index 32b30ce..4f136f8 100644 --- a/src/pydase/utils/serializer.py +++ b/src/pydase/utils/serializer.py @@ -273,6 +273,8 @@ def set_nested_value_by_path( value_type = serialized_value.pop("type") if "readonly" in current_dict and current_dict["type"] != "method": current_dict["type"] = value_type + # TODO: this does not yet remove keys that are not present in the serialized new + # value current_dict.update(serialized_value) From 8787cb0509c7b35f11f0f93f5f99522a9823464a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mose=20M=C3=BCller?= Date: Tue, 5 Mar 2024 13:53:41 +0100 Subject: [PATCH 3/7] get cached value before executing custom notification callbacks --- src/pydase/data_service/data_service_observer.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/pydase/data_service/data_service_observer.py b/src/pydase/data_service/data_service_observer.py index ef4e85d..29450f4 100644 --- a/src/pydase/data_service/data_service_observer.py +++ b/src/pydase/data_service/data_service_observer.py @@ -44,8 +44,12 @@ class DataServiceObserver(PropertyObserver): self._update_cache_value(full_access_path, value, cached_value_dict) - # TODO: get the cached value again -> _update_cache_value already put the - # right thing into the cache + cached_value_dict = deepcopy( + self.state_manager._data_service_cache.get_value_dict_from_cache( + full_access_path + ) + ) + for callback in self._notification_callbacks: callback(full_access_path, value, cached_value_dict) From 7aacc21010814d00cb5327f003f56e886f08e7b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mose=20M=C3=BCller?= Date: Tue, 5 Mar 2024 13:54:24 +0100 Subject: [PATCH 4/7] removes processing of value from sio_callback (cached value is up-to-date already) --- src/pydase/server/web_server/sio_setup.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/pydase/server/web_server/sio_setup.py b/src/pydase/server/web_server/sio_setup.py index 96a8f93..4dc40f5 100644 --- a/src/pydase/server/web_server/sio_setup.py +++ b/src/pydase/server/web_server/sio_setup.py @@ -9,7 +9,6 @@ from pydase.data_service.data_service_observer import DataServiceObserver from pydase.data_service.state_manager import StateManager from pydase.utils.helpers import get_object_attr_from_path_list from pydase.utils.logging import SocketIOHandler -from pydase.utils.serializer import dump logger = logging.getLogger(__name__) @@ -62,7 +61,7 @@ class RunMethodDict(TypedDict): kwargs: dict[str, Any] -def setup_sio_server( # noqa: C901 +def setup_sio_server( observer: DataServiceObserver, enable_cors: bool, loop: asyncio.AbstractEventLoop, @@ -97,15 +96,6 @@ def setup_sio_server( # noqa: C901 full_access_path: str, value: Any, cached_value_dict: dict[str, Any] ) -> None: if cached_value_dict != {}: - serialized_value = dump(value) - if cached_value_dict["type"] != "method": - cached_value_dict["type"] = serialized_value["type"] - - cached_value_dict["value"] = serialized_value["value"] - - # Check if the serialized value contains an "enum" key, and if so, copy it - if "enum" in serialized_value: - cached_value_dict["enum"] = serialized_value["enum"] async def notify() -> None: try: From b8a52c2e6a55b0785a12eff1cdb2e5acdec1119e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mose=20M=C3=BCller?= Date: Tue, 5 Mar 2024 13:56:02 +0100 Subject: [PATCH 5/7] only update cache and execute notification callbacks if attribute is public and has changed --- src/pydase/data_service/data_service_observer.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/pydase/data_service/data_service_observer.py b/src/pydase/data_service/data_service_observer.py index 29450f4..ec2262e 100644 --- a/src/pydase/data_service/data_service_observer.py +++ b/src/pydase/data_service/data_service_observer.py @@ -42,16 +42,16 @@ class DataServiceObserver(PropertyObserver): ): logger.debug("'%s' changed to '%s'", full_access_path, value) - self._update_cache_value(full_access_path, value, cached_value_dict) + self._update_cache_value(full_access_path, value, cached_value_dict) - cached_value_dict = deepcopy( - self.state_manager._data_service_cache.get_value_dict_from_cache( - full_access_path + cached_value_dict = deepcopy( + self.state_manager._data_service_cache.get_value_dict_from_cache( + full_access_path + ) ) - ) - for callback in self._notification_callbacks: - callback(full_access_path, value, cached_value_dict) + for callback in self._notification_callbacks: + callback(full_access_path, value, cached_value_dict) if isinstance(value, ObservableObject): self._update_property_deps_dict() From 24a01c09826df1f3a4355be548d266051c43d3e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mose=20M=C3=BCller?= Date: Tue, 5 Mar 2024 14:17:05 +0100 Subject: [PATCH 6/7] removes keys from cache entry if they are not part of the new value serialization --- src/pydase/utils/serializer.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/pydase/utils/serializer.py b/src/pydase/utils/serializer.py index 4f136f8..e91ea77 100644 --- a/src/pydase/utils/serializer.py +++ b/src/pydase/utils/serializer.py @@ -273,10 +273,16 @@ def set_nested_value_by_path( value_type = serialized_value.pop("type") if "readonly" in current_dict and current_dict["type"] != "method": current_dict["type"] = value_type - # TODO: this does not yet remove keys that are not present in the serialized new - # value + current_dict.update(serialized_value) + # removes keys that are not present in the serialized new value + keys_to_keep = set(serialized_value.keys()) | {"type", "readonly"} + + for key in list(current_dict.keys()): + if key not in keys_to_keep: + current_dict.pop(key, None) + def get_nested_dict_by_path( serialization_dict: dict[str, Any], From 99c7ad0ec8eb4b399201909b4801fcfcbc554ba9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mose=20M=C3=BCller?= Date: Tue, 5 Mar 2024 14:24:49 +0100 Subject: [PATCH 7/7] updates serializer tests --- tests/utils/test_serializer.py | 38 +++++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/tests/utils/test_serializer.py b/tests/utils/test_serializer.py index 4d4868d..096dec3 100644 --- a/tests/utils/test_serializer.py +++ b/tests/utils/test_serializer.py @@ -1,4 +1,5 @@ import asyncio +import enum from enum import Enum from typing import Any @@ -18,6 +19,11 @@ from pydase.utils.serializer import ( ) +class MyEnum(enum.Enum): + RUNNING = "running" + FINISHED = "finished" + + @pytest.mark.parametrize( "test_input, expected", [ @@ -396,33 +402,55 @@ def setup_dict() -> dict[str, Any]: class ServiceClass(pydase.DataService): attr1 = 1.0 attr2 = MySubclass() + enum_attr = MyEnum.RUNNING attr_list = [0, 1, MySubclass()] return ServiceClass().serialize()["value"] -def test_update_attribute(setup_dict) -> None: +def test_update_attribute(setup_dict: dict[str, Any]) -> None: set_nested_value_by_path(setup_dict, "attr1", 15) assert setup_dict["attr1"]["value"] == 15 -def test_update_nested_attribute(setup_dict) -> None: +def test_update_nested_attribute(setup_dict: dict[str, Any]) -> None: set_nested_value_by_path(setup_dict, "attr2.attr3", 25.0) assert setup_dict["attr2"]["value"]["attr3"]["value"] == 25.0 -def test_update_list_entry(setup_dict) -> None: +def test_update_float_attribute_to_enum(setup_dict: dict[str, Any]) -> None: + set_nested_value_by_path(setup_dict, "attr2.attr3", MyEnum.RUNNING) + assert setup_dict["attr2"]["value"]["attr3"] == { + "doc": None, + "enum": {"FINISHED": "finished", "RUNNING": "running"}, + "readonly": False, + "type": "Enum", + "value": "RUNNING", + } + + +def test_update_enum_attribute_to_float(setup_dict: dict[str, Any]) -> None: + set_nested_value_by_path(setup_dict, "enum_attr", 1.01) + assert setup_dict["enum_attr"] == { + "doc": None, + "readonly": False, + "type": "float", + "value": 1.01, + } + + +def test_update_list_entry(setup_dict: dict[str, Any]) -> None: set_nested_value_by_path(setup_dict, "attr_list[1]", 20) assert setup_dict["attr_list"]["value"][1]["value"] == 20 -def test_update_list_append(setup_dict) -> None: +def test_update_list_append(setup_dict: dict[str, Any]) -> None: set_nested_value_by_path(setup_dict, "attr_list[3]", 20) assert setup_dict["attr_list"]["value"][3]["value"] == 20 def test_update_invalid_list_index( - setup_dict, caplog: pytest.LogCaptureFixture + setup_dict: dict[str, Any], caplog: pytest.LogCaptureFixture ) -> None: set_nested_value_by_path(setup_dict, "attr_list[10]", 30) assert (