import logging from typing import Any 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, dump logger = logging.getLogger() def test_static_property_dependencies() -> None: class SubClass(pydase.DataService): _name = "SubClass" @property def name(self) -> str: return self._name @name.setter def name(self, value: str) -> None: self._name = value class ServiceClass(pydase.DataService): def __init__(self) -> None: super().__init__() self.list_attr = [SubClass()] self._name = "ServiceClass" @property def name(self) -> str: return self._name @name.setter def name(self, value: str) -> None: self._name = value service_instance = ServiceClass() state_manager = StateManager(service_instance) observer = DataServiceObserver(state_manager) logger.debug(observer.property_deps_dict) assert observer.property_deps_dict == { "list_attr[0]._name": ["list_attr[0].name"], "_name": ["name"], } def test_dynamic_list_property_dependencies() -> None: class SubClass(pydase.DataService): _name = "SubClass" @property def name(self) -> str: return self._name @name.setter def name(self, value: str) -> None: self._name = value class ServiceClass(pydase.DataService): def __init__(self) -> None: super().__init__() self.list_attr = [SubClass()] service_instance = ServiceClass() state_manager = StateManager(service_instance) observer = DataServiceObserver(state_manager) assert observer.property_deps_dict == { "list_attr[0]._name": ["list_attr[0].name"], } service_instance.list_attr.append(SubClass()) assert observer.property_deps_dict == { "list_attr[0]._name": ["list_attr[0].name"], "list_attr[1]._name": ["list_attr[1].name"], } def test_protected_or_private_change_logs(caplog: pytest.LogCaptureFixture) -> None: class OtherService(pydase.DataService): def __init__(self) -> None: super().__init__() self._name = "Hi" class MyService(pydase.DataService): def __init__(self) -> None: super().__init__() self.subclass = OtherService() service = MyService() state_manager = StateManager(service) DataServiceObserver(state_manager) service.subclass._name = "Hello" assert "'subclass._name' changed to 'Hello'" not in caplog.text def test_dynamic_list_entry_with_property(caplog: pytest.LogCaptureFixture) -> None: class PropertyClass(pydase.DataService): _name = "Hello" @property def name(self) -> str: """The name property.""" return self._name class MyService(pydase.DataService): def __init__(self) -> None: super().__init__() self.list_attr = [] def toggle_high_voltage(self) -> None: self.list_attr = [] self.list_attr.append(PropertyClass()) self.list_attr[0]._name = "Hoooo" service = MyService() state_manager = StateManager(service) DataServiceObserver(state_manager) service.toggle_high_voltage() assert "'list_attr[0].name' changed to 'Hello'" not in caplog.text assert "'list_attr[0].name' changed to 'Hoooo'" in caplog.text def test_private_attribute_does_not_have_to_be_serializable() -> None: class MyService(pydase.DataService): def __init__(self) -> None: super().__init__() self.publ_attr: Any = 1 self.__priv_attr = (1,) def change_publ_attr(self) -> None: self.publ_attr = (2,) # cannot be serialized def change_priv_attr(self) -> None: self.__priv_attr = (2,) service_instance = MyService() pydase.Server(service_instance) with pytest.raises(SerializationError): 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 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