diff --git a/pyproject.toml b/pyproject.toml index c93714e..90d51f0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "pydase" -version = "0.10.2" +version = "0.10.3" description = "A flexible and robust Python library for creating, managing, and interacting with data services, with built-in support for web and RPC servers, and customizable features for diverse use cases." authors = ["Mose Mueller "] readme = "README.md" diff --git a/src/pydase/data_service/data_service_observer.py b/src/pydase/data_service/data_service_observer.py index 28569bc..75a78b5 100644 --- a/src/pydase/data_service/data_service_observer.py +++ b/src/pydase/data_service/data_service_observer.py @@ -8,7 +8,10 @@ from pydase.observer_pattern.observable.observable_object import ObservableObjec from pydase.observer_pattern.observer.property_observer import ( PropertyObserver, ) -from pydase.utils.helpers import get_object_attr_from_path +from pydase.utils.helpers import ( + get_object_attr_from_path, + normalize_full_access_path_string, +) from pydase.utils.serialization.serializer import ( SerializationPathError, SerializedObject, @@ -99,7 +102,8 @@ class DataServiceObserver(PropertyObserver): ) def _notify_dependent_property_changes(self, changed_attr_path: str) -> None: - changed_props = self.property_deps_dict.get(changed_attr_path, []) + normalized_attr_path = normalize_full_access_path_string(changed_attr_path) + changed_props = self.property_deps_dict.get(normalized_attr_path, []) for prop in changed_props: # only notify about changing attribute if it is not currently being # "changed" e.g. when calling the getter of a property within another diff --git a/src/pydase/utils/helpers.py b/src/pydase/utils/helpers.py index f47af3e..748213d 100644 --- a/src/pydase/utils/helpers.py +++ b/src/pydase/utils/helpers.py @@ -223,3 +223,25 @@ def current_event_loop_exists() -> bool: import asyncio return asyncio.get_event_loop_policy()._local._loop is not None # type: ignore + + +def normalize_full_access_path_string(s: str) -> str: + """Normalizes a string representing a full access path by converting double quotes + to single quotes. + + This function is useful for ensuring consistency in strings that represent access + paths containing dictionary keys, by replacing all double quotes (`"`) with single + quotes (`'`). + + Args: + s (str): The input string to be normalized. + + Returns: + A new string with all double quotes replaced by single quotes. + + Example: + >>> normalize_full_access_path_string('dictionary["first"].my_task') + "dictionary['first'].my_task" + """ + + return s.replace('"', "'") diff --git a/tests/data_service/test_data_service_observer.py b/tests/data_service/test_data_service_observer.py index 974eca9..347ff54 100644 --- a/tests/data_service/test_data_service_observer.py +++ b/tests/data_service/test_data_service_observer.py @@ -5,7 +5,7 @@ import pydase import pytest from pydase.data_service.data_service_observer import DataServiceObserver from pydase.data_service.state_manager import StateManager -from pydase.utils.serialization.serializer import SerializationError +from pydase.utils.serialization.serializer import SerializationError, dump logger = logging.getLogger() @@ -146,3 +146,41 @@ def test_private_attribute_does_not_have_to_be_serializable() -> None: service_instance.change_publ_attr() service_instance.change_priv_attr() + + +def test_normalized_attr_path_in_dependent_property_changes( + caplog: pytest.LogCaptureFixture, +) -> None: + class SubService(pydase.DataService): + _prop = 10.0 + + @property + def prop(self) -> float: + return self._prop + + class MyService(pydase.DataService): + def __init__(self) -> None: + super().__init__() + self.service_dict = {"one": SubService()} + + service_instance = MyService() + state_manager = StateManager(service=service_instance) + observer = DataServiceObserver(state_manager=state_manager) + + assert observer.property_deps_dict["service_dict['one']._prop"] == [ + "service_dict['one'].prop" + ] + + # We can use dict key path encoded with double quotes + state_manager.set_service_attribute_value_by_path( + 'service_dict["one"]._prop', dump(11.0) + ) + assert service_instance.service_dict["one"].prop == 11.0 + assert "'service_dict[\"one\"].prop' changed to '11.0'" in caplog.text + + # We can use dict key path encoded with single quotes + state_manager.set_service_attribute_value_by_path( + "service_dict['one']._prop", dump(12.0) + ) + assert service_instance.service_dict["one"].prop == 12.0 + assert "'service_dict[\"one\"].prop' changed to '12.0'" in caplog.text