mirror of
https://github.com/tiqi-group/pydase.git
synced 2025-12-19 04:31:19 +01:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a76035f443 | ||
|
|
2ab4d1c00a | ||
|
|
a9d577820f | ||
|
|
f5e6dca16a | ||
|
|
4a45d0d438 | ||
|
|
3cc6399f60 | ||
|
|
dc1c7e80f4 |
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -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
1
src/pydase/frontend/static/css/main.32559665.css.map
Normal file
1
src/pydase/frontend/static/css/main.32559665.css.map
Normal file
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
1
src/pydase/frontend/static/js/main.6d4f9d3a.js.map
Normal file
1
src/pydase/frontend/static/js/main.6d4f9d3a.js.map
Normal file
File diff suppressed because one or more lines are too long
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user