10 Commits

Author SHA1 Message Date
Mose Müller
aca5aab1ef removes unused attribute 2024-02-01 13:25:53 +01:00
Mose Müller
4f1cc4787d Merge pull request #99 from tiqi-group/cleanup/removes_deprecated_code
Cleanup/removes deprecated code
2024-02-01 11:11:43 +01:00
Mose Müller
8efd67d9f3 fixes tests 2024-02-01 10:18:58 +01:00
Mose Müller
34fc0f8739 removes deprecated code 2024-02-01 10:18:49 +01:00
Mose Müller
e60880fd30 Merge pull request #98 from tiqi-group/refactor/passing_full_serialization_dict_to_frontend
Refactor: passing full serialization dict to frontend
2024-02-01 09:27:29 +01:00
Mose Müller
036b0c681a updates version to v0.6.0 (due to breaking changes) 2024-02-01 09:25:47 +01:00
Mose Müller
dd268a4f9b npm run build 2024-02-01 09:18:24 +01:00
Mose Müller
e8638f1f3a fixes tests 2024-02-01 08:45:40 +01:00
Mose Müller
7279fed2aa frontend will can now display any serialization dict 2024-02-01 08:45:40 +01:00
Mose Müller
a2518671da DataService's serialize method now returns whole serialization dict (also passed to frontend) 2024-02-01 08:45:40 +01:00
17 changed files with 55 additions and 193 deletions

View File

@@ -1,10 +1,6 @@
import { useCallback, useEffect, useReducer, useState } from 'react';
import { Navbar, Form, Offcanvas, Container } from 'react-bootstrap';
import { hostname, port, socket } from './socket';
import {
DataServiceComponent,
DataServiceJSON
} from './components/DataServiceComponent';
import './App.css';
import {
Notifications,
@@ -14,6 +10,7 @@ import {
import { ConnectionToast } from './components/ConnectionToast';
import { SerializedValue, setNestedValueByPath, State } from './utils/stateUtils';
import { WebSettingsContext, WebSetting } from './WebSettings';
import { Attribute, GenericComponent } from './components/GenericComponent';
type Action =
| { type: 'SET_DATA'; data: State }
@@ -35,7 +32,10 @@ const reducer = (state: State, action: Action): State => {
case 'SET_DATA':
return action.data;
case 'UPDATE_ATTRIBUTE': {
return setNestedValueByPath(state, action.fullAccessPath, action.newValue);
return {
...state,
value: setNestedValueByPath(state.value, action.fullAccessPath, action.newValue)
};
}
default:
throw new Error();
@@ -184,9 +184,10 @@ const App = () => {
<div className="App navbarOffset">
<WebSettingsContext.Provider value={webSettings}>
<DataServiceComponent
name={''}
props={state as DataServiceJSON}
<GenericComponent
name=""
parentPath=""
attribute={state as Attribute}
isInstantUpdate={isInstantUpdate}
addNotification={addNotification}
/>

View File

@@ -6,7 +6,12 @@ export interface SerializedValue {
async?: boolean;
parameters?: unknown;
}
export type State = Record<string, SerializedValue> | null;
export type State = {
type: string;
value: Record<string, SerializedValue> | null;
readonly: boolean;
doc: string | null;
};
export function setNestedValueByPath(
serializationDict: Record<string, SerializedValue>,

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "pydase"
version = "0.5.2"
version = "0.6.0"
description = "A flexible and robust Python library for creating, managing, and interacting with data services, with built-in support for web and RPC servers, and customizable features for diverse use cases."
authors = ["Mose Mueller <mosmuell@ethz.ch>"]
readme = "README.md"

View File

@@ -1,8 +1,7 @@
import inspect
import logging
import warnings
from enum import Enum
from typing import TYPE_CHECKING, Any, get_type_hints
from typing import Any, get_type_hints
import rpyc # type: ignore[import-untyped]
@@ -15,20 +14,12 @@ from pydase.observer_pattern.observable.observable import (
from pydase.utils.helpers import (
convert_arguments_to_hinted_types,
get_class_and_instance_attributes,
get_object_attr_from_path_list,
is_property_attribute,
parse_list_attr_and_index,
update_value_if_changed,
)
from pydase.utils.serializer import (
Serializer,
generate_serialized_data_paths,
get_nested_dict_by_path,
)
if TYPE_CHECKING:
from pathlib import Path
logger = logging.getLogger(__name__)
@@ -51,18 +42,7 @@ class DataService(rpyc.Service, AbstractDataService):
if not hasattr(self, "_autostart_tasks"):
self._autostart_tasks = {}
filename = kwargs.pop("filename", None)
if filename is not None:
warnings.warn(
"The 'filename' argument is deprecated and will be removed in a future "
"version. Please pass the 'filename' argument to `pydase.Server`.",
DeprecationWarning,
stacklevel=2,
)
self._filename: str | Path = filename
self.__check_instance_classes()
self._initialised = True
def __setattr__(self, __name: str, __value: Any) -> None:
# Check and warn for unexpected type changes in attributes
@@ -125,27 +105,6 @@ class DataService(rpyc.Service, AbstractDataService):
):
self.__warn_if_not_observable(attr_value)
def __set_attribute_based_on_type( # noqa: PLR0913
self,
target_obj: Any,
attr_name: str,
attr: Any,
value: Any,
index: int | None,
path_list: list[str],
) -> None:
if isinstance(attr, Enum):
update_value_if_changed(target_obj, attr_name, attr.__class__[value])
elif isinstance(attr, list) and index is not None:
update_value_if_changed(attr, index, value)
elif isinstance(attr, DataService) and isinstance(value, dict):
for key, v in value.items():
self.update_DataService_attribute([*path_list, attr_name], key, v)
elif callable(attr):
process_callable_attribute(attr, value["args"])
else:
update_value_if_changed(target_obj, attr_name, value)
def _rpyc_getattr(self, name: str) -> Any:
if name.startswith("_"):
# disallow special and private attributes
@@ -166,71 +125,6 @@ class DataService(rpyc.Service, AbstractDataService):
# allow all other attributes
setattr(self, name, value)
def write_to_file(self) -> None:
"""
Serialize the DataService instance and write it to a JSON file.
This method is deprecated and will be removed in a future version.
Service persistence is handled by `pydase.Server` now, instead.
"""
warnings.warn(
"'write_to_file' is deprecated and will be removed in a future version. "
"Service persistence is handled by `pydase.Server` now, instead.",
DeprecationWarning,
stacklevel=2,
)
if hasattr(self, "_state_manager"):
self._state_manager.save_state()
def load_DataService_from_JSON( # noqa: N802
self, json_dict: dict[str, Any]
) -> None:
warnings.warn(
"'load_DataService_from_JSON' is deprecated and will be removed in a "
"future version. "
"Service persistence is handled by `pydase.Server` now, instead.",
DeprecationWarning,
stacklevel=2,
)
# Traverse the serialized representation and set the attributes of the class
serialized_class = self.serialize()
for path in generate_serialized_data_paths(json_dict):
nested_json_dict = get_nested_dict_by_path(json_dict, path)
value = nested_json_dict["value"]
value_type = nested_json_dict["type"]
nested_class_dict = get_nested_dict_by_path(serialized_class, path)
class_value_type = nested_class_dict.get("type", None)
if class_value_type == value_type:
class_attr_is_read_only = nested_class_dict["readonly"]
if class_attr_is_read_only:
logger.debug(
"Attribute '%s' is read-only. Ignoring value from JSON "
"file...",
path,
)
continue
# Split the path into parts
parts = path.split(".")
attr_name = parts[-1]
# Convert dictionary into Quantity
if class_value_type == "Quantity":
value = u.convert_to_quantity(value)
self.update_DataService_attribute(parts[:-1], attr_name, value)
else:
logger.info(
"Attribute type of '%s' changed from '%s' to "
"'%s'. Ignoring value from JSON file...",
path,
value_type,
class_value_type,
)
def serialize(self) -> dict[str, dict[str, Any]]:
"""
Serializes the instance into a dictionary, preserving the structure of the
@@ -248,38 +142,4 @@ class DataService(rpyc.Service, AbstractDataService):
Returns:
dict: The serialized instance.
"""
return Serializer.serialize_object(self)["value"]
def update_DataService_attribute( # noqa: N802
self,
path_list: list[str],
attr_name: str,
value: Any,
) -> None:
warnings.warn(
"'update_DataService_attribute' is deprecated and will be removed in a "
"future version. "
"Service state management is handled by `pydase.data_service.state_manager`"
"now, instead.",
DeprecationWarning,
stacklevel=2,
)
# If attr_name corresponds to a list entry, extract the attr_name and the index
attr_name, index = parse_list_attr_and_index(attr_name)
# Traverse the object according to the path parts
target_obj = get_object_attr_from_path_list(self, path_list)
# 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(target_obj, attr_name):
setattr(target_obj, attr_name, value)
return
attr = get_object_attr_from_path_list(target_obj, [attr_name])
if attr is None:
return
self.__set_attribute_based_on_type(
target_obj, attr_name, attr, value, index, path_list
)
return Serializer.serialize_object(self)

View File

@@ -30,10 +30,10 @@ class DataServiceCache:
self._cache = self.service.serialize()
def update_cache(self, full_access_path: str, value: Any) -> None:
set_nested_value_by_path(self._cache, full_access_path, value)
set_nested_value_by_path(self._cache["value"], full_access_path, value)
def get_value_dict_from_cache(self, full_access_path: str) -> dict[str, Any]:
try:
return get_nested_dict_by_path(self._cache, full_access_path)
return get_nested_dict_by_path(self._cache["value"], full_access_path)
except (SerializationPathError, SerializationValueError, KeyError):
return {}

View File

@@ -126,7 +126,7 @@ class StateManager:
if self.filename is not None:
with open(self.filename, "w") as f:
json.dump(self.cache, f, indent=4)
json.dump(self.cache["value"], f, indent=4)
else:
logger.info(
"State manager was not initialised with a filename. Skipping "
@@ -191,7 +191,7 @@ class StateManager:
value: The new value to set for the attribute.
"""
current_value_dict = get_nested_dict_by_path(self.cache, path)
current_value_dict = get_nested_dict_by_path(self.cache["value"], path)
# This will also filter out methods as they are 'read-only'
if current_value_dict["readonly"]:
@@ -234,7 +234,7 @@ class StateManager:
# Update path to reflect the attribute without list indices
path = ".".join([*parent_path_list, attr_name])
attr_cache_type = get_nested_dict_by_path(self.cache, path)["type"]
attr_cache_type = get_nested_dict_by_path(self.cache["value"], path)["type"]
# Traverse the object according to the path parts
target_obj = get_object_attr_from_path_list(self.service, parent_path_list)
@@ -273,7 +273,7 @@ class StateManager:
return has_decorator
cached_serialization_dict = get_nested_dict_by_path(
self.cache, full_access_path
self.cache["value"], full_access_path
)
if cached_serialization_dict["value"] == "method":

View File

@@ -1,13 +1,13 @@
{
"files": {
"main.css": "/static/css/main.2d8458eb.css",
"main.js": "/static/js/main.dba067e7.js",
"main.js": "/static/js/main.1b1d7066.js",
"index.html": "/index.html",
"main.2d8458eb.css.map": "/static/css/main.2d8458eb.css.map",
"main.dba067e7.js.map": "/static/js/main.dba067e7.js.map"
"main.1b1d7066.js.map": "/static/js/main.1b1d7066.js.map"
},
"entrypoints": [
"static/css/main.2d8458eb.css",
"static/js/main.dba067e7.js"
"static/js/main.1b1d7066.js"
]
}

View File

@@ -1 +1 @@
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Web site displaying a pydase UI."/><link rel="apple-touch-icon" href="/logo192.png"/><link rel="manifest" href="/manifest.json"/><title>pydase App</title><script defer="defer" src="/static/js/main.dba067e7.js"></script><link href="/static/css/main.2d8458eb.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>
<!doctype html><html lang="en"><head><meta charset="utf-8"/><link rel="icon" href="/favicon.ico"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><meta name="description" content="Web site displaying a pydase UI."/><link rel="apple-touch-icon" href="/logo192.png"/><link rel="manifest" href="/manifest.json"/><title>pydase App</title><script defer="defer" src="/static/js/main.1b1d7066.js"></script><link href="/static/css/main.2d8458eb.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"></div></body></html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -177,8 +177,6 @@ class Server:
self.servers: dict[str, asyncio.Future[Any]] = {}
self.executor: ThreadPoolExecutor | None = None
self._state_manager = StateManager(self._service, filename)
if getattr(self._service, "_filename", None) is not None:
self._service._state_manager = self._state_manager
self._observer = DataServiceObserver(self._state_manager)
self._state_manager.load_state()

View File

@@ -126,7 +126,7 @@ class WebServer:
@property
def web_settings(self) -> dict[str, dict[str, Any]]:
current_web_settings = self._get_web_settings_from_file()
for path in generate_serialized_data_paths(self.state_manager.cache):
for path in generate_serialized_data_paths(self.state_manager.cache["value"]):
if path in current_web_settings:
continue

View File

@@ -5,7 +5,6 @@ from typing import Any
import pydase
import pydase.components
import pydase.units as u
import pytest
from pydase.data_service.data_service_observer import DataServiceObserver
from pydase.data_service.state_manager import (
StateManager,
@@ -91,7 +90,7 @@ class Service(pydase.DataService):
self._property_attr = value
CURRENT_STATE = Service().serialize()
CURRENT_STATE = Service().serialize()["value"]
LOAD_STATE = {
"list_attr": {
@@ -251,16 +250,6 @@ def test_load_state(tmp_path: Path, caplog: LogCaptureFixture) -> None:
assert "'my_slider.step_size' changed to '2.0'" in caplog.text
def test_filename_warning(tmp_path: Path, caplog: LogCaptureFixture) -> None:
file = tmp_path / "test_state.json"
with pytest.warns(DeprecationWarning):
service = Service(filename=str(file))
StateManager(service=service, filename=str(file))
assert f"Overwriting filename {str(file)!r} with {str(file)!r}." in caplog.text
def test_filename_error(caplog: LogCaptureFixture) -> None:
service = Service()
manager = StateManager(service=service)

View File

@@ -1,9 +1,10 @@
from typing import Any
import pydase
import pydase.units as u
from pydase.data_service.data_service import DataService
from pydase.data_service.data_service_observer import DataServiceObserver
from pydase.data_service.state_manager import StateManager
from pydase.data_service.state_manager import StateManager, load_state
from pytest import LogCaptureFixture
@@ -99,7 +100,10 @@ def test_autoconvert_offset_to_baseunit() -> None:
def test_loading_from_json(caplog: LogCaptureFixture) -> None:
"""This function tests if the quantity read from the json description is actually
passed as a quantity to the property setter."""
JSON_DICT = {
import json
import tempfile
serialization_dict = {
"some_unit": {
"type": "Quantity",
"value": {"magnitude": 10.0, "unit": "A"},
@@ -118,14 +122,17 @@ def test_loading_from_json(caplog: LogCaptureFixture) -> None:
return self._unit
@some_unit.setter
@load_state
def some_unit(self, value: u.Quantity) -> None:
assert isinstance(value, u.Quantity)
self._unit = value
service_instance = ServiceClass()
state_manager = StateManager(service_instance)
DataServiceObserver(state_manager)
service_instance.load_DataService_from_JSON(JSON_DICT)
fp = tempfile.NamedTemporaryFile("w+")
json.dump(serialization_dict, fp)
fp.seek(0)
pydase.Server(service_instance, filename=fp.name)
assert "'some_unit' changed to '10.0 A'" in caplog.text

View File

@@ -323,7 +323,7 @@ def test_derived_data_service_serialization() -> None:
@pytest.fixture
def setup_dict():
def setup_dict() -> dict[str, Any]:
class MySubclass(pydase.DataService):
attr3 = 1.0
list_attr = [1.0, 1]
@@ -333,30 +333,32 @@ def setup_dict():
attr2 = MySubclass()
attr_list = [0, 1, MySubclass()]
return ServiceClass().serialize()
return ServiceClass().serialize()["value"]
def test_update_attribute(setup_dict):
def test_update_attribute(setup_dict) -> None:
set_nested_value_by_path(setup_dict, "attr1", 15)
assert setup_dict["attr1"]["value"] == 15
def test_update_nested_attribute(setup_dict):
def test_update_nested_attribute(setup_dict) -> None:
set_nested_value_by_path(setup_dict, "attr2.attr3", 25.0)
assert setup_dict["attr2"]["value"]["attr3"]["value"] == 25.0
def test_update_list_entry(setup_dict):
def test_update_list_entry(setup_dict) -> None:
set_nested_value_by_path(setup_dict, "attr_list[1]", 20)
assert setup_dict["attr_list"]["value"][1]["value"] == 20
def test_update_list_append(setup_dict):
def test_update_list_append(setup_dict) -> None:
set_nested_value_by_path(setup_dict, "attr_list[3]", 20)
assert setup_dict["attr_list"]["value"][3]["value"] == 20
def test_update_invalid_list_index(setup_dict, caplog: pytest.LogCaptureFixture):
def test_update_invalid_list_index(
setup_dict, caplog: pytest.LogCaptureFixture
) -> None:
set_nested_value_by_path(setup_dict, "attr_list[10]", 30)
assert (
"Error occured trying to change 'attr_list[10]': list index "