From 319a62bb017f7122a276cf60cb6e32f47c8a7ae6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mose=20M=C3=BCller?= Date: Wed, 2 Aug 2023 12:06:19 +0200 Subject: [PATCH] fix: property callback issues, implemented new tests --- .../data_service/data_service.py | 15 ++- tests/test_properties.py | 93 +++++++++++++++++++ 2 files changed, 107 insertions(+), 1 deletion(-) diff --git a/src/pyDataInterface/data_service/data_service.py b/src/pyDataInterface/data_service/data_service.py index 4b0b8b3..b6a5ffc 100644 --- a/src/pyDataInterface/data_service/data_service.py +++ b/src/pyDataInterface/data_service/data_service.py @@ -122,8 +122,20 @@ class DataService(rpyc.Service): ) if isinstance(attr_value, property): dependencies = attr_value.fget.__code__.co_names # type: ignore + source_code_string = inspect.getsource(attr_value.fget) # type: ignore for dependency in dependencies: + # check if the dependencies are attributes of obj + # This doesn't have to be the case like, for example, here: + # >>> @property + # >>> def power(self) -> float: + # >>> return self.class_attr.voltage * self.current + # + # The dependencies for this property are: + # ('class_attr', 'voltage', 'current') + if f"self.{dependency}" not in source_code_string: + continue + # use `obj` instead of `type(obj)` to get DataServiceList # instead of list dependency_value = getattr(obj, dependency) @@ -347,7 +359,8 @@ class DataService(rpyc.Service): """Handles registration of callbacks for DataService attributes""" # as the DataService is an attribute of self, change the root object - nested_attr.__root__ = self.__root__ + # use the dictionary to not trigger callbacks on initialised objects + nested_attr.__dict__["__root__"] = self.__root__ new_path = f"{parent_path}.{attr_name}" self._register_DataService_callbacks(nested_attr, new_path) diff --git a/tests/test_properties.py b/tests/test_properties.py index 27b0ccc..7654471 100644 --- a/tests/test_properties.py +++ b/tests/test_properties.py @@ -332,3 +332,96 @@ def test_subclass_properties_2(capsys: CaptureFixture) -> None: # a notification will be emitted. actual_output = sorted(set(captured.out.strip().split("\n"))) assert actual_output == expected_output + + +def test_subsubclass_properties(capsys: CaptureFixture) -> None: + class SubSubClass(DataService): + _voltage = 10.0 + + @property + def voltage(self) -> float: + return self._voltage + + @voltage.setter + def voltage(self, value: float) -> None: + self._voltage = value + + class SubClass(DataService): + class_attr = SubSubClass() + current = 0.5 + + @property + def power(self) -> float: + return self.class_attr.voltage * self.current + + class ServiceClass(DataService): + class_attr = [SubClass() for i in range(2)] + + @property + def power(self) -> float: + return self.class_attr[0].power + + test_service = ServiceClass() + + test_service.class_attr[1].class_attr.voltage = 100.0 + captured = capsys.readouterr() + expected_output = sorted( + { + "ServiceClass.class_attr[0].class_attr.voltage = 100.0", + "ServiceClass.class_attr[1].class_attr.voltage = 100.0", + "ServiceClass.class_attr[0].power = 50.0", + "ServiceClass.class_attr[1].power = 50.0", + "ServiceClass.power = 50.0", + } + ) + actual_output = sorted(set(captured.out.strip().split("\n"))) + assert actual_output == expected_output + + +def test_subsubclass_instance_properties(capsys: CaptureFixture) -> None: + class SubSubClass(DataService): + def __init__(self) -> None: + self._voltage = 10.0 + super().__init__() + + @property + def voltage(self) -> float: + return self._voltage + + @voltage.setter + def voltage(self, value: float) -> None: + self._voltage = value + + class SubClass(DataService): + def __init__(self) -> None: + self.attr = [SubSubClass()] + self.current = 0.5 + super().__init__() + + @property + def power(self) -> float: + return self.attr[0].voltage * self.current + + class ServiceClass(DataService): + class_attr = [SubClass() for i in range(2)] + + @property + def power(self) -> float: + return self.class_attr[0].power + + test_service = ServiceClass() + + test_service.class_attr[1].attr[0].voltage = 100.0 + captured = capsys.readouterr() + # again, changing an item in a list will trigger the callbacks. This is why a + # notification for `ServiceClass.power` is emitted although it did not change its + # value + expected_output = sorted( + { + "ServiceClass.class_attr[1].attr[0].voltage = 100.0", + "ServiceClass.class_attr[1].power = 50.0", + "ServiceClass.power = 5.0", + } + ) + actual_output = sorted(set(captured.out.strip().split("\n"))) + assert actual_output == expected_output