7 Commits

Author SHA1 Message Date
Mose Müller
a76035f443 Merge pull request #68 from tiqi-group/fix/only_load_state_properties_can_be_updated
fix: only load state properties can be updated
2023-11-09 17:35:28 +01:00
Mose Müller
2ab4d1c00a updates to v0.3.1 2023-11-09 17:33:03 +01:00
Mose Müller
a9d577820f updates tests 2023-11-09 17:32:35 +01:00
Mose Müller
f5e6dca16a moves check for load_state decorator to load_state method in StateManager 2023-11-09 17:32:30 +01:00
Mose Müller
4a45d0d438 npm run build 2023-11-09 17:10:56 +01:00
Mose Müller
3cc6399f60 frontend: update style (fix button appearance) 2023-11-09 17:10:21 +01:00
Mose Müller
dc1c7e80f4 docs: updates Readme TOC 2023-11-09 16:05:31 +01:00
26 changed files with 95 additions and 57 deletions

View File

@@ -23,6 +23,7 @@
- [Extending with New Components](#extending-with-new-components)
- [Customizing Web Interface Style](#customizing-web-interface-style)
- [Understanding Service Persistence](#understanding-service-persistence)
- [Controlling Property State Loading with `@load_state`](#controlling-property-state-loading-with-load_state)
- [Understanding Tasks in pydase](#understanding-tasks-in-pydase)
- [Understanding Units in pydase](#understanding-units-in-pydase)
- [Changing the Log Level](#changing-the-log-level)

View File

@@ -12,14 +12,28 @@ input.instantUpdate {
}
.navbarOffset {
padding-top: 60px !important;
right: 20;
}
/* .toastContainer {
position: fixed;
} */
.toastContainer {
position: fixed !important;
padding: 5px;
}
.notificationToast {
background-color: rgba(114, 214, 253, 0.5) !important;
}
.exceptionToast {
background-color: rgba(216, 41, 18, 0.678) !important;
}
}
.buttonComponent {
float: left !important;
margin-right: 10px !important;
}
.stringComponent {
float: left !important;
margin-right: 10px !important;
}
.numberComponent {
float: left !important;
margin-right: 10px !important;
width: 270px !important;
}

View File

@@ -91,7 +91,7 @@ export const AsyncMethodComponent = React.memo((props: AsyncMethodProps) => {
return (
<div className="align-items-center asyncMethodComponent" id={id}>
{process.env.NODE_ENV === 'development' && (
<p>Render count: {renderCount.current}</p>
<div>Render count: {renderCount.current}</div>
)}
<h5>
Function: {name}

View File

@@ -37,7 +37,7 @@ export const ButtonComponent = React.memo((props: ButtonComponentProps) => {
return (
<div className={'buttonComponent'} id={id}>
{process.env.NODE_ENV === 'development' && (
<p>Render count: {renderCount.current}</p>
<div>Render count: {renderCount.current}</div>
)}
<DocStringComponent docString={docString} />
@@ -49,7 +49,7 @@ export const ButtonComponent = React.memo((props: ButtonComponentProps) => {
value={parentPath}
disabled={readOnly}
onChange={(e) => setChecked(e.currentTarget.checked)}>
<p>{buttonName}</p>
{buttonName}
</ToggleButton>
</div>
);

View File

@@ -42,7 +42,7 @@ export const ColouredEnumComponent = React.memo((props: ColouredEnumComponentPro
return (
<div className={'enumComponent'} id={id}>
{process.env.NODE_ENV === 'development' && (
<p>Render count: {renderCount.current}</p>
<div>Render count: {renderCount.current}</div>
)}
<DocStringComponent docString={docString} />
<Row>

View File

@@ -68,7 +68,7 @@ export const ConnectionToast = React.memo(
const { message, bg, delay } = getToastContent();
return (
<ToastContainer position="bottom-center">
<ToastContainer position="bottom-center" className="toastContainer">
<Toast
show={show}
onClose={handleClose}

View File

@@ -39,7 +39,7 @@ export const EnumComponent = React.memo((props: EnumComponentProps) => {
return (
<div className={'enumComponent'} id={parentPath.concat('.' + name)}>
{process.env.NODE_ENV === 'development' && (
<p>Render count: {renderCount.current}</p>
<div>Render count: {renderCount.current}</div>
)}
<DocStringComponent docString={docString} />
<Row>

View File

@@ -31,6 +31,9 @@ export const ImageComponent = React.memo((props: ImageComponentProps) => {
return (
<div className={'imageComponent'} id={id}>
{process.env.NODE_ENV === 'development' && (
<div>Render count: {renderCount.current}</div>
)}
<Card>
<Card.Header
onClick={() => setOpen(!open)}

View File

@@ -26,7 +26,7 @@ export const ListComponent = React.memo((props: ListComponentProps) => {
return (
<div className={'listComponent'} id={id}>
{process.env.NODE_ENV === 'development' && (
<p>Render count: {renderCount.current}</p>
<div>Render count: {renderCount.current}</div>
)}
<DocStringComponent docString={docString} />
{value.map((item, index) => {

View File

@@ -76,7 +76,7 @@ export const MethodComponent = React.memo((props: MethodProps) => {
return (
<div className="align-items-center methodComponent" id={id}>
{process.env.NODE_ENV === 'development' && (
<p>Render count: {renderCount.current}</p>
<div>Render count: {renderCount.current}</div>
)}
<h5 onClick={() => setHideOutput(!hideOutput)} style={{ cursor: 'pointer' }}>
Function: {name}
@@ -84,11 +84,9 @@ export const MethodComponent = React.memo((props: MethodProps) => {
</h5>
<Form onSubmit={execute}>
{args}
<div>
<Button variant="primary" type="submit">
Execute
</Button>
</div>
<Button variant="primary" type="submit">
Execute
</Button>
</Form>
<Collapse in={!hideOutput}>

View File

@@ -25,10 +25,7 @@ export const Notifications = React.memo((props: NotificationProps) => {
} = props;
return (
<ToastContainer
className="navbarOffset toastContainer"
position="top-end"
style={{ position: 'fixed' }}>
<ToastContainer className="navbarOffset toastContainer" position="top-end">
{showNotification &&
notifications.map((notification) => (
<Toast

View File

@@ -289,8 +289,8 @@ export const NumberComponent = React.memo((props: NumberComponentProps) => {
return (
<div className="numberComponent" id={id}>
{process.env.NODE_ENV === 'development' && showName && (
<p>Render count: {renderCount.current}</p>
{process.env.NODE_ENV === 'development' && (
<div>Render count: {renderCount.current}</div>
)}
<DocStringComponent docString={docString} />
<div className="d-flex">

View File

@@ -106,7 +106,7 @@ export const SliderComponent = React.memo((props: SliderComponentProps) => {
return (
<div className="sliderComponent" id={id}>
{process.env.NODE_ENV === 'development' && (
<p>Render count: {renderCount.current}</p>
<div>Render count: {renderCount.current}</div>
)}
<DocStringComponent docString={docString} />

View File

@@ -60,7 +60,7 @@ export const StringComponent = React.memo((props: StringComponentProps) => {
return (
<div className={'stringComponent'} id={id}>
{process.env.NODE_ENV === 'development' && (
<p>Render count: {renderCount.current}</p>
<div>Render count: {renderCount.current}</div>
)}
<DocStringComponent docString={docString} />
<InputGroup>

View File

@@ -1,6 +1,6 @@
[tool.poetry]
name = "pydase"
version = "0.3.0"
version = "0.3.1"
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

@@ -148,7 +148,10 @@ class StateManager:
value, value_type = nested_json_dict["value"], nested_json_dict["type"]
class_attr_value_type = nested_class_dict.get("type", None)
if class_attr_value_type == value_type:
if (
class_attr_value_type == value_type
and self.__is_loadable_state_attribute(path)
):
self.set_service_attribute_value_by_path(path, value)
else:
logger.info(
@@ -231,21 +234,36 @@ class StateManager:
# Traverse the object according to the path parts
target_obj = get_object_attr_from_path_list(self.service, parent_path_list)
if self.__attr_value_should_change(target_obj, attr_name):
if attr_cache_type in ("ColouredEnum", "Enum"):
enum_attr = get_object_attr_from_path_list(target_obj, [attr_name])
setattr(target_obj, attr_name, enum_attr.__class__[value])
elif attr_cache_type == "list":
list_obj = get_object_attr_from_path_list(target_obj, [attr_name])
list_obj[index] = value
else:
setattr(target_obj, attr_name, value)
if attr_cache_type in ("ColouredEnum", "Enum"):
enum_attr = get_object_attr_from_path_list(target_obj, [attr_name])
setattr(target_obj, attr_name, enum_attr.__class__[value])
elif attr_cache_type == "list":
list_obj = get_object_attr_from_path_list(target_obj, [attr_name])
list_obj[index] = value
else:
setattr(target_obj, attr_name, value)
def __is_loadable_state_attribute(self, property_path: str) -> bool:
"""Checks if an attribute defined by a dot-separated path should be loaded from
storage.
For properties, it verifies the presence of the '@load_state' decorator. Regular
attributes default to being loadable.
"""
parent_object = get_object_attr_from_path_list(
self.service, property_path.split(".")[:-1]
)
attr_name = property_path.split(".")[-1]
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)
prop = getattr(type(parent_object), attr_name, None)
if isinstance(prop, property):
return has_load_state_decorator(prop)
has_decorator = has_load_state_decorator(prop)
if not has_decorator:
logger.debug(
f"Property {attr_name!r} has no '@load_state' decorator. "
"Ignoring value from JSON file..."
)
return has_decorator
return True

View File

@@ -1,13 +1,13 @@
{
"files": {
"main.css": "/static/css/main.c444b055.css",
"main.js": "/static/js/main.08edc629.js",
"main.css": "/static/css/main.32559665.css",
"main.js": "/static/js/main.6d4f9d3a.js",
"index.html": "/index.html",
"main.c444b055.css.map": "/static/css/main.c444b055.css.map",
"main.08edc629.js.map": "/static/js/main.08edc629.js.map"
"main.32559665.css.map": "/static/css/main.32559665.css.map",
"main.6d4f9d3a.js.map": "/static/js/main.6d4f9d3a.js.map"
},
"entrypoints": [
"static/css/main.c444b055.css",
"static/js/main.08edc629.js"
"static/css/main.32559665.css",
"static/js/main.6d4f9d3a.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.08edc629.js"></script><link href="/static/css/main.c444b055.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.6d4f9d3a.js"></script><link href="/static/css/main.32559665.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

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -153,7 +153,10 @@ def test_load_state(tmp_path: Path, caplog: LogCaptureFixture):
assert service.subservice.name == "SubService" # didn't change
assert "Service.some_unit changed to 12.0 A!" in caplog.text
assert "Attribute 'name' is read-only. Ignoring new value..." in caplog.text
assert (
"Property 'name' has no '@load_state' decorator. "
"Ignoring value from JSON file..." in caplog.text
)
assert (
"Attribute type of 'some_float' changed from 'int' to 'float'. "
"Ignoring value from JSON file..."
@@ -195,7 +198,11 @@ def test_readonly_attribute(tmp_path: Path, caplog: LogCaptureFixture):
service = Service()
manager = StateManager(service=service, filename=str(file))
manager.load_state()
assert "Attribute 'name' is read-only. Ignoring new value..." in caplog.text
assert service.name == "Service"
assert (
"Property 'name' has no '@load_state' decorator. "
"Ignoring value from JSON file..." in caplog.text
)
def test_changed_type(tmp_path: Path, caplog: LogCaptureFixture):