diff --git a/src/pydase/data_service/data_service_observer.py b/src/pydase/data_service/data_service_observer.py index aeabfef..ec2262e 100644 --- a/src/pydase/data_service/data_service_observer.py +++ b/src/pydase/data_service/data_service_observer.py @@ -42,10 +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) - for callback in self._notification_callbacks: - callback(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 + ) + ) + + for callback in self._notification_callbacks: + callback(full_access_path, value, cached_value_dict) if isinstance(value, ObservableObject): self._update_property_deps_dict() 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: diff --git a/src/pydase/utils/serializer.py b/src/pydase/utils/serializer.py index 669021b..e91ea77 100644 --- a/src/pydase/utils/serializer.py +++ b/src/pydase/utils/serializer.py @@ -269,12 +269,19 @@ 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) + + # 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( 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 (