Merge pull request #67 from tiqi-group/46-setter-functions-being-called-at-startup-when-loading-json-file

46 setter functions being called at startup when loading json file
This commit is contained in:
Mose Müller 2023-11-09 15:54:05 +01:00 committed by GitHub
commit 675fe86e7e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 129 additions and 15 deletions

View File

@ -398,7 +398,32 @@ if __name__ == "__main__":
In this example, the state of the `Device` service will be saved to `device_state.json` when the service is shut down. If `device_state.json` exists when the server is started, the state manager will restore the state of the service from this file.
Note: If the service class structure has changed since the last time its state was saved, only the attributes that have remained the same will be restored from the settings file.
### Controlling Property State Loading with `@load_state`
By default, the state manager only restores values for public attributes of your service. If you have properties that you want to control the loading for, you can use the `@load_state` decorator on your property setters. This indicates to the state manager that the value of the property should be loaded from the state file.
Here is how you can apply the `@load_state` decorator:
```python
from pydase import DataService
from pydase.data_service.state_manager import load_state
class Device(DataService):
_name = "Default Device Name"
@property
def name(self) -> str:
return self._name
@name.setter
@load_state
def name(self, value: str) -> None:
self._name = value
```
With the `@load_state` decorator applied to the `name` property setter, the state manager will load and apply the `name` property's value from the file storing the state upon server startup, assuming it exists.
Note: If the service class structure has changed since the last time its state was saved, only the attributes and properties decorated with `@load_state` that have remained the same will be restored from the settings file.
## Understanding Tasks in pydase

View File

@ -1,6 +1,7 @@
import json
import logging
import os
from collections.abc import Callable
from pathlib import Path
from typing import TYPE_CHECKING, Any, Optional, cast
@ -8,7 +9,6 @@ import pydase.units as u
from pydase.data_service.data_service_cache import DataServiceCache
from pydase.utils.helpers import (
get_object_attr_from_path_list,
is_property_attribute,
parse_list_attr_and_index,
)
from pydase.utils.serializer import (
@ -23,6 +23,39 @@ if TYPE_CHECKING:
logger = logging.getLogger(__name__)
def load_state(func: Callable[..., Any]) -> Callable[..., Any]:
"""This function should be used as a decorator on property setters to indicate that
the value should be loaded from the JSON file.
Example:
>>> class Service(pydase.DataService):
... _name = "Service"
...
... @property
... def name(self) -> str:
... return self._name
...
... @name.setter
... @load_state
... def name(self, value: str) -> None:
... self._name = value
"""
func._load_state = True
return func
def has_load_state_decorator(prop: property):
"""Determines if the property's setter method is decorated with the `@load_state`
decorator.
"""
try:
return getattr(prop.fset, "_load_state")
except AttributeError:
return False
class StateManager:
"""
Manages the state of a DataService instance, serving as both a cache and a
@ -211,6 +244,8 @@ class StateManager:
def __attr_value_should_change(self, parent_object: Any, attr_name: str) -> bool:
# If the attribute is a property, change it using the setter without getting
# the property value (would otherwise be bad for expensive getter methods)
if is_property_attribute(parent_object, attr_name):
return True
prop = getattr(type(parent_object), attr_name, None)
if isinstance(prop, property):
return has_load_state_decorator(prop)
return True

View File

@ -7,7 +7,11 @@ from pytest import LogCaptureFixture
import pydase
import pydase.units as u
from pydase.components.coloured_enum import ColouredEnum
from pydase.data_service.state_manager import StateManager
from pydase.data_service.state_manager import (
StateManager,
has_load_state_decorator,
load_state,
)
class SubService(pydase.DataService):
@ -28,7 +32,7 @@ class Service(pydase.DataService):
self.list_attr = [1.0, 2.0]
self._property_attr = 1337.0
self._name = "Service"
self._state = State.RUNNING
self.state = State.RUNNING
super().__init__(**kwargs)
@property
@ -43,14 +47,6 @@ class Service(pydase.DataService):
def property_attr(self, value: float) -> None:
self._property_attr = value
@property
def state(self) -> State:
return self._state
@state.setter
def state(self, value: State) -> None:
self._state = value
CURRENT_STATE = Service().serialize()
@ -148,7 +144,9 @@ def test_load_state(tmp_path: Path, caplog: LogCaptureFixture):
assert service.some_unit == u.Quantity(12, "A") # has changed
assert service.list_attr[0] == 1.4 # has changed
assert service.list_attr[1] == 2.0 # has not changed
assert service.property_attr == 1337.1 # has changed
assert (
service.property_attr == 1337
) # has not changed as property has not @load_state decorator
assert service.state == State.FAILED # has changed
assert service.name == "Service" # has not changed as readonly
assert service.some_float == 1.0 # has not changed due to different type
@ -215,3 +213,59 @@ def test_changed_type(tmp_path: Path, caplog: LogCaptureFixture):
"Attribute type of 'some_float' changed from 'int' to "
"'float'. Ignoring value from JSON file..."
) in caplog.text
def test_property_load_state(tmp_path: Path):
# Create a StateManager instance with a temporary file
file = tmp_path / "test_state.json"
LOAD_STATE = {
"name": {
"type": "str",
"value": "Some other name",
"readonly": False,
"doc": None,
},
"not_loadable_attr": {
"type": "str",
"value": "But I AM loadable!?",
"readonly": False,
"doc": None,
},
}
# Write a temporary JSON file to read back
with open(file, "w") as f:
json.dump(LOAD_STATE, f, indent=4)
class Service(pydase.DataService):
_name = "Service"
_not_loadable_attr = "Not loadable"
@property
def name(self) -> str:
return self._name
@name.setter
@load_state
def name(self, value: str) -> None:
self._name = value
@property
def not_loadable_attr(self) -> str:
return self._not_loadable_attr
@not_loadable_attr.setter
def not_loadable_attr(self, value: str) -> None:
self._not_loadable_attr = value
@property
def property_without_setter(self) -> None:
return
service_instance = Service()
StateManager(service_instance, filename=file).load_state()
assert service_instance.name == "Some other name"
assert service_instance.not_loadable_attr == "Not loadable"
assert not has_load_state_decorator(type(service_instance).property_without_setter)