diff --git a/src/pydase/data_service/data_service_observer.py b/src/pydase/data_service/data_service_observer.py index 75a78b5..d6c073c 100644 --- a/src/pydase/data_service/data_service_observer.py +++ b/src/pydase/data_service/data_service_observer.py @@ -10,7 +10,6 @@ from pydase.observer_pattern.observer.property_observer import ( ) from pydase.utils.helpers import ( get_object_attr_from_path, - normalize_full_access_path_string, ) from pydase.utils.serialization.serializer import ( SerializationPathError, @@ -102,8 +101,7 @@ class DataServiceObserver(PropertyObserver): ) def _notify_dependent_property_changes(self, changed_attr_path: str) -> None: - normalized_attr_path = normalize_full_access_path_string(changed_attr_path) - changed_props = self.property_deps_dict.get(normalized_attr_path, []) + changed_props = self.property_deps_dict.get(changed_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/observer_pattern/observer/property_observer.py b/src/pydase/observer_pattern/observer/property_observer.py index 0e8ae4c..a686765 100644 --- a/src/pydase/observer_pattern/observer/property_observer.py +++ b/src/pydase/observer_pattern/observer/property_observer.py @@ -100,7 +100,7 @@ class PropertyObserver(Observer): elif isinstance(collection, dict): for key, val in collection.items(): if isinstance(val, Observable): - new_prefix = f"{parent_path}['{key}']" + new_prefix = f'{parent_path}["{key}"]' deps.update( self._get_properties_and_their_dependencies(val, new_prefix) ) diff --git a/src/pydase/utils/helpers.py b/src/pydase/utils/helpers.py index 748213d..f47af3e 100644 --- a/src/pydase/utils/helpers.py +++ b/src/pydase/utils/helpers.py @@ -223,25 +223,3 @@ 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 347ff54..fcadc60 100644 --- a/tests/data_service/test_data_service_observer.py +++ b/tests/data_service/test_data_service_observer.py @@ -167,8 +167,8 @@ def test_normalized_attr_path_in_dependent_property_changes( 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" + assert observer.property_deps_dict['service_dict["one"]._prop'] == [ + 'service_dict["one"].prop' ] # We can use dict key path encoded with double quotes @@ -184,3 +184,41 @@ def test_normalized_attr_path_in_dependent_property_changes( ) assert service_instance.service_dict["one"].prop == 12.0 assert "'service_dict[\"one\"].prop' changed to '12.0'" in caplog.text + + +def test_nested_dict_property_changes( + caplog: pytest.LogCaptureFixture, +) -> None: + def get_voltage() -> float: + """Mocking a remote device.""" + return 2.0 + + def set_voltage(value: float) -> None: + """Mocking a remote device.""" + + class OtherService(pydase.DataService): + _voltage = 1.0 + + @property + def voltage(self) -> float: + # Property dependency _voltage changes within the property itself. + # This should be handled gracefully, i.e. not introduce recursion + self._voltage = get_voltage() + return self._voltage + + @voltage.setter + def voltage(self, value: float) -> None: + self._voltage = value + set_voltage(self._voltage) + + class MyService(pydase.DataService): + def __init__(self) -> None: + super().__init__() + self.my_dict = {"key": OtherService()} + + service = MyService() + pydase.Server(service) + + # Changing the _voltage attribute should re-evaluate the voltage property, but avoid + # recursion + service.my_dict["key"].voltage = 1.2