mirror of
https://github.com/tiqi-group/pydase.git
synced 2025-06-05 05:00:40 +02:00
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:
commit
675fe86e7e
27
README.md
27
README.md
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user