16 Commits

Author SHA1 Message Date
Mose Müller
8c24f5dd67 updates version number 2023-12-13 11:29:28 +01:00
Mose Müller
1c4a878aa8 Merge pull request #86 from tiqi-group/9-add-units-support-for-numberslider
updates Readme explaining how to use units with number sliders
2023-12-13 11:25:58 +01:00
Mose Müller
31967d0d43 updates Readme explaining how to use units with number sliders 2023-12-13 11:23:44 +01:00
Mose Müller
b4edc31030 Merge pull request #84 from tiqi-group/75-numberslider-component-is-not-working
75 numberslider component is not working
2023-12-13 11:12:56 +01:00
Mose Müller
ff7c92547e updates Readme 2023-12-13 11:09:18 +01:00
Mose Müller
fab91f3221 updates number slider test file 2023-12-13 10:39:26 +01:00
Mose Müller
bd77995d96 npm run build 2023-12-13 10:36:00 +01:00
Mose Müller
729f375901 adds support for quantities in slider component (passing object instead of number) 2023-12-13 10:35:28 +01:00
Mose Müller
e643dd6f5c adds number object types to NumberComponent 2023-12-13 10:34:32 +01:00
Mose Müller
53f4cf6690 removes setters for min, max and step_size in NumberSlider, updates docstring 2023-12-13 09:30:21 +01:00
Mose Müller
c0c8591fc4 updates number slider component 2023-12-11 17:46:08 +01:00
Mose Müller
13fba6d3d6 npm run build 2023-12-11 17:30:12 +01:00
Mose Müller
dc4c9ff58f removes unused customEmitUpdate prop from NumberComponent 2023-12-11 17:30:12 +01:00
Mose Müller
83cd07feee updates SliderComponent to emit attribute updates (instead of full state dict) 2023-12-11 17:30:12 +01:00
Mose Müller
09f73a2b1d Merge pull request #83 from tiqi-group/feat/improve_data_service_serialization
fixed serialization of class deriving from class which derives from DataService
2023-12-11 17:28:11 +01:00
Mose Müller
88886e3fd6 fixed serialization of class deriving from class which derives from DataService 2023-12-11 17:25:03 +01:00
17 changed files with 442 additions and 172 deletions

177
README.md
View File

@@ -42,8 +42,7 @@
- [Saving and restoring the service state for service persistence](#understanding-service-persistence)
- [Automated task management with built-in start/stop controls and optional autostart](#understanding-tasks-in-pydase)
- [Support for units](#understanding-units-in-pydase)
<!-- * Event-based callback functionality for real-time updates
- Support for additional servers for specific use-cases -->
<!-- Support for additional servers for specific use-cases -->
## Installation
@@ -286,26 +285,172 @@ if __name__ == "__main__":
#### `NumberSlider`
This component provides an interactive slider interface for adjusting numerical values on the frontend. It supports both floats and integers. The values adjusted on the frontend are synchronized with the backend in real-time, ensuring consistent data representation.
The `NumberSlider` component in the `pydase` package provides an interactive slider interface for adjusting numerical values on the frontend. It is designed to support both numbers and quantities and ensures that values adjusted on the frontend are synchronized with the backend.
The slider can be customized with initial values, minimum and maximum limits, and step sizes to fit various use cases.
To utilize the `NumberSlider`, users should implement a class that derives from `NumberSlider`. This class can then define the initial values, minimum and maximum limits, step sizes, and additional logic as needed.
Here's an example of how to implement and use a custom slider:
```python
import pydase
from pydase.components import NumberSlider
import pydase.components
class MySlider(pydase.components.NumberSlider):
def __init__(
self,
value: float = 0.0,
min_: float = 0.0,
max_: float = 100.0,
step_size: float = 1.0,
) -> None:
super().__init__(value, min_, max_, step_size)
@property
def min(self) -> float:
return self._min
@min.setter
def min(self, value: float) -> None:
self._min = value
@property
def max(self) -> float:
return self._max
@max.setter
def max(self, value: float) -> None:
self._max = value
@property
def step_size(self) -> float:
return self._step_size
@step_size.setter
def step_size(self, value: float) -> None:
self._step_size = value
@property
def value(self) -> float:
return self._value
@value.setter
def value(self, value: float) -> None:
if value < self._min or value > self._max:
raise ValueError("Value is either below allowed min or above max value.")
self._value = value
class MyService(pydase.DataService):
slider = NumberSlider(value=3.5, min=0, max=10, step_size=0.1, type="float")
def __init__(self) -> None:
super().__init__()
self.voltage = MySlider()
if __name__ == "__main__":
service = MyService()
pydase.Server(service).run()
service_instance = MyService()
service_instance.voltage.value = 5
print(service_instance.voltage.value) # Output: 5
pydase.Server(service_instance).run()
```
In this example, `MySlider` overrides the `min`, `max`, `step_size`, and `value` properties. Users can make any of these properties read-only by omitting the corresponding setter method.
![Slider Component](docs/images/Slider_component.png)
- Accessing parent class resources in `NumberSlider`
In scenarios where you need the slider component to interact with or access resources from its parent class, you can achieve this by passing a callback function to it. This method avoids directly passing the entire parent class instance (`self`) and offers a more encapsulated approach. The callback function can be designed to utilize specific attributes or methods of the parent class, allowing the slider to perform actions or retrieve data in response to slider events.
Here's an illustrative example:
```python
from collections.abc import Callable
import pydase
import pydase.components
class MySlider(pydase.components.NumberSlider):
def __init__(
self,
value: float,
on_change: Callable[[float], None],
) -> None:
super().__init__(value=value)
self._on_change = on_change
# ... other properties ...
@property
def value(self) -> float:
return self._value
@value.setter
def value(self, new_value: float) -> None:
if new_value < self._min or new_value > self._max:
raise ValueError("Value is either below allowed min or above max value.")
self._value = new_value
self._on_change(new_value)
class MyService(pydase.DataService):
def __init__(self) -> None:
self.voltage = MySlider(
5,
on_change=self.handle_voltage_change,
)
def handle_voltage_change(self, new_voltage: float) -> None:
print(f"Voltage changed to: {new_voltage}")
# Additional logic here
if __name__ == "__main__":
service_instance = MyService()
my_service.voltage.value = 7 # Output: "Voltage changed to: 7"
pydase.Server(service_instance).run()
```
- Incorporating units in `NumberSlider`
The `NumberSlider` is capable of displaying units alongside values, enhancing its usability in contexts where unit representation is crucial. When utilizing `pydase.units`, you can specify units for the slider's value, allowing the component to reflect these units in the frontend.
Here's how to implement a `NumberSlider` with unit display:
```python
import pydase
import pydase.components
import pydase.units as u
class MySlider(pydase.components.NumberSlider):
def __init__(
self,
value: u.Quantity = 0.0 * u.units.V,
) -> None:
super().__init__(value)
@property
def value(self) -> u.Quantity:
return self._value
@value.setter
def value(self, value: u.Quantity) -> None:
if value.m < self._min or value.m > self._max:
raise ValueError("Value is either below allowed min or above max value.")
self._value = value
class MyService(pydase.DataService):
def __init__(self) -> None:
super().__init__()
self.voltage = MySlider()
if __name__ == "__main__":
service_instance = MyService()
service_instance.voltage.value = 5 * u.units.V
print(service_instance.voltage.value) # Output: 5 V
pydase.Server(service_instance).run()
```
#### `ColouredEnum`
This component provides a way to visually represent different states or categories in a data service using colour-coded options. It behaves similarly to a standard `Enum`, but the values encode colours in a format understood by CSS. The colours can be defined using various methods like Hexadecimal, RGB, HSL, and more.
@@ -542,34 +687,34 @@ You have two primary ways to adjust the log levels in `pydase`:
1. directly targeting `pydase` loggers
You can set the log level for any `pydase` logger directly in your code. This method is useful for fine-tuning logging levels for specific modules within `pydase`. For instance, if you want to change the log level of the main `pydase` logger or target a submodule like `pydase.data_service`, you can do so as follows:
```python
# <your_script.py>
import logging
# Set the log level for the main pydase logger
logging.getLogger("pydase").setLevel(logging.INFO)
# Optionally, target a specific submodule logger
# logging.getLogger("pydase.data_service").setLevel(logging.DEBUG)
# Your logger for the current script
logger = logging.getLogger(__name__)
logger.info("My info message.")
```
This approach allows for specific control over different parts of the `pydase` library, depending on your logging needs.
2. using the `ENVIRONMENT` environment variable
For a more global setting that affects the entire `pydase` library, you can utilize the `ENVIRONMENT` environment variable. Setting this variable to "production" will configure all `pydase` loggers to only log messages of level "INFO" and above, filtering out more verbose logging. This is particularly useful for production environments where excessive logging can be overwhelming or unnecessary.
```bash
ENVIRONMENT="production" python -m <module_using_pydase>
```
In the absence of this setting, the default behavior is to log everything of level "DEBUG" and above, suitable for development environments where more detailed logs are beneficial.
**Note**: It is recommended to avoid calling the `pydase.utils.logging.setup_logging` function directly, as this may result in duplicated logging messages.
## Documentation

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 20 KiB

View File

@@ -97,10 +97,10 @@ export const GenericComponent = React.memo(
parentPath={parentPath}
docString={attribute.doc}
readOnly={attribute.readonly}
value={attribute.value['value']['value']}
min={attribute.value['min']['value']}
max={attribute.value['max']['value']}
stepSize={attribute.value['step_size']['value']}
value={attribute.value['value']}
min={attribute.value['min']}
max={attribute.value['max']}
stepSize={attribute.value['step_size']}
isInstantUpdate={isInstantUpdate}
addNotification={addNotification}
/>

View File

@@ -8,6 +8,29 @@ import { LevelName } from './NotificationsComponent';
// TODO: add button functionality
export type QuantityObject = {
type: 'Quantity';
readonly: boolean;
value: {
magnitude: number;
unit: string;
};
doc?: string;
};
export type IntObject = {
type: 'int';
readonly: boolean;
value: number;
doc?: string;
};
export type FloatObject = {
type: 'float';
readonly: boolean;
value: number;
doc?: string;
};
export type NumberObject = IntObject | FloatObject | QuantityObject;
interface NumberComponentProps {
name: string;
type: 'float' | 'int';
@@ -18,12 +41,6 @@ interface NumberComponentProps {
isInstantUpdate: boolean;
unit?: string;
showName?: boolean;
customEmitUpdate?: (
name: string,
parent_path: string,
value: number,
callback?: (ack: unknown) => void
) => void;
addNotification: (message: string, levelname?: LevelName) => void;
}
@@ -123,10 +140,6 @@ export const NumberComponent = React.memo((props: NumberComponentProps) => {
// Whether to show the name infront of the component (false if used with a slider)
const showName = props.showName !== undefined ? props.showName : true;
// If emitUpdate is passed, use this instead of the setAttribute from the socket
// Also used when used with a slider
const emitUpdate =
props.customEmitUpdate !== undefined ? props.customEmitUpdate : setAttribute;
const renderCount = useRef(0);
// Create a state for the cursor position
@@ -263,7 +276,7 @@ export const NumberComponent = React.memo((props: NumberComponentProps) => {
selectionEnd
));
} else if (key === 'Enter' && !isInstantUpdate) {
emitUpdate(name, parentPath, Number(newValue));
setAttribute(name, parentPath, Number(newValue));
return;
} else {
console.debug(key);
@@ -272,7 +285,7 @@ export const NumberComponent = React.memo((props: NumberComponentProps) => {
// Update the input value and maintain the cursor position
if (isInstantUpdate) {
emitUpdate(name, parentPath, Number(newValue));
setAttribute(name, parentPath, Number(newValue));
}
setInputString(newValue);
@@ -284,7 +297,7 @@ export const NumberComponent = React.memo((props: NumberComponentProps) => {
const handleBlur = () => {
if (!isInstantUpdate) {
// If not in "instant update" mode, emit an update when the input field loses focus
emitUpdate(name, parentPath, Number(inputString));
setAttribute(name, parentPath, Number(inputString));
}
};

View File

@@ -3,19 +3,19 @@ import { InputGroup, Form, Row, Col, Collapse, ToggleButton } from 'react-bootst
import { setAttribute } from '../socket';
import { DocStringComponent } from './DocStringComponent';
import { Slider } from '@mui/material';
import { NumberComponent } from './NumberComponent';
import { NumberComponent, NumberObject } from './NumberComponent';
import { getIdFromFullAccessPath } from '../utils/stringUtils';
import { LevelName } from './NotificationsComponent';
interface SliderComponentProps {
name: string;
min: number;
max: number;
min: NumberObject;
max: NumberObject;
parentPath?: string;
value: number;
value: NumberObject;
readOnly: boolean;
docString: string;
stepSize: number;
stepSize: NumberObject;
isInstantUpdate: boolean;
addNotification: (message: string, levelname?: LevelName) => void;
}
@@ -30,7 +30,6 @@ export const SliderComponent = React.memo((props: SliderComponentProps) => {
min,
max,
stepSize,
readOnly,
docString,
isInstantUpdate,
addNotification
@@ -58,52 +57,41 @@ export const SliderComponent = React.memo((props: SliderComponentProps) => {
addNotification(`${parentPath}.${name}.stepSize changed to ${stepSize}.`);
}, [props.stepSize]);
const emitSliderUpdate = (
name: string,
parentPath: string,
value: number,
callback?: (ack: unknown) => void,
min: number = props.min,
max: number = props.max,
stepSize: number = props.stepSize
) => {
setAttribute(
name,
parentPath,
{
value: value,
min: min,
max: max,
step_size: stepSize
},
callback
);
};
const handleOnChange = (event, newNumber: number | number[]) => {
// This will never be the case as we do not have a range slider. However, we should
// make sure this is properly handled.
if (Array.isArray(newNumber)) {
newNumber = newNumber[0];
}
emitSliderUpdate(name, parentPath, newNumber);
setAttribute(`${name}.value`, parentPath, newNumber);
};
const handleValueChange = (newValue: number, valueType: string) => {
switch (valueType) {
case 'min':
emitSliderUpdate(name, parentPath, value, undefined, newValue);
break;
case 'max':
emitSliderUpdate(name, parentPath, value, undefined, min, newValue);
break;
case 'stepSize':
emitSliderUpdate(name, parentPath, value, undefined, min, max, newValue);
break;
default:
break;
}
setAttribute(`${name}.${valueType}`, parentPath, newValue);
};
const deconstructNumberDict = (
numberDict: NumberObject
): [number, boolean, string | null] => {
let numberMagnitude: number;
let numberUnit: string | null = null;
const numberReadOnly = numberDict.readonly;
if (numberDict.type === 'int' || numberDict.type === 'float') {
numberMagnitude = numberDict.value;
} else if (numberDict.type === 'Quantity') {
numberMagnitude = numberDict.value.magnitude;
numberUnit = numberDict.value.unit;
}
return [numberMagnitude, numberReadOnly, numberUnit];
};
const [valueMagnitude, valueReadOnly, valueUnit] = deconstructNumberDict(value);
const [minMagnitude, minReadOnly] = deconstructNumberDict(min);
const [maxMagnitude, maxReadOnly] = deconstructNumberDict(max);
const [stepSizeMagnitude, stepSizeReadOnly] = deconstructNumberDict(stepSize);
return (
<div className="sliderComponent" id={id}>
{process.env.NODE_ENV === 'development' && (
@@ -120,15 +108,15 @@ export const SliderComponent = React.memo((props: SliderComponentProps) => {
style={{ margin: '0px 0px 10px 0px' }}
aria-label="Always visible"
// valueLabelDisplay="on"
disabled={readOnly}
value={value}
disabled={valueReadOnly}
value={valueMagnitude}
onChange={(event, newNumber) => handleOnChange(event, newNumber)}
min={min}
max={max}
step={stepSize}
min={minMagnitude}
max={maxMagnitude}
step={stepSizeMagnitude}
marks={[
{ value: min, label: `${min}` },
{ value: max, label: `${max}` }
{ value: minMagnitude, label: `${minMagnitude}` },
{ value: maxMagnitude, label: `${maxMagnitude}` }
]}
/>
</Col>
@@ -136,13 +124,13 @@ export const SliderComponent = React.memo((props: SliderComponentProps) => {
<NumberComponent
isInstantUpdate={isInstantUpdate}
parentPath={parentPath}
name={name}
name={`${name}.value`}
docString=""
readOnly={readOnly}
readOnly={valueReadOnly}
type="float"
value={value}
value={valueMagnitude}
unit={valueUnit}
showName={false}
customEmitUpdate={emitSliderUpdate}
addNotification={() => null}
/>
</Col>
@@ -178,7 +166,8 @@ export const SliderComponent = React.memo((props: SliderComponentProps) => {
<Form.Label>Min Value</Form.Label>
<Form.Control
type="number"
value={min}
value={minMagnitude}
disabled={minReadOnly}
onChange={(e) => handleValueChange(Number(e.target.value), 'min')}
/>
</Col>
@@ -187,7 +176,8 @@ export const SliderComponent = React.memo((props: SliderComponentProps) => {
<Form.Label>Max Value</Form.Label>
<Form.Control
type="number"
value={max}
value={maxMagnitude}
disabled={maxReadOnly}
onChange={(e) => handleValueChange(Number(e.target.value), 'max')}
/>
</Col>
@@ -196,8 +186,9 @@ export const SliderComponent = React.memo((props: SliderComponentProps) => {
<Form.Label>Step Size</Form.Label>
<Form.Control
type="number"
value={stepSize}
onChange={(e) => handleValueChange(Number(e.target.value), 'stepSize')}
value={stepSizeMagnitude}
disabled={stepSizeReadOnly}
onChange={(e) => handleValueChange(Number(e.target.value), 'step_size')}
/>
</Col>
</Row>

View File

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

@@ -1,5 +1,5 @@
import logging
from typing import Any, Literal
from typing import Any
from pydase.data_service.data_service import DataService
@@ -21,15 +21,60 @@ class NumberSlider(DataService):
The maximum value of the slider. Defaults to 100.
step_size (float, optional):
The increment/decrement step size of the slider. Defaults to 1.0.
type (Literal["int", "float"], optional):
The type of the slider value. Determines if the value is an integer or float.
Defaults to "float".
Example:
--------
```python
class MyService(DataService):
voltage = NumberSlider(1, 0, 10, 0.1, "int")
class MySlider(pydase.components.NumberSlider):
def __init__(
self,
value: float = 0.0,
min_: float = 0.0,
max_: float = 100.0,
step_size: float = 1.0,
) -> None:
super().__init__(value, min_, max_, step_size)
@property
def min(self) -> float:
return self._min
@min.setter
def min(self, value: float) -> None:
self._min = value
@property
def max(self) -> float:
return self._max
@max.setter
def max(self, value: float) -> None:
self._max = value
@property
def step_size(self) -> float:
return self._step_size
@step_size.setter
def step_size(self, value: float) -> None:
self._step_size = value
@property
def value(self) -> float:
return self._value
@value.setter
def value(self, value: float) -> None:
if value < self._min or value > self._max:
raise ValueError(
"Value is either below allowed min or above max value."
)
self._value = value
class MyService(pydase.DataService):
def __init__(self) -> None:
self.voltage = MyService()
# Modifying or accessing the voltage value:
my_service = MyService()
@@ -38,29 +83,39 @@ class NumberSlider(DataService):
```
"""
def __init__( # noqa: PLR0913
def __init__(
self,
value: float = 0,
value: Any = 0.0,
min_: float = 0.0,
max_: float = 100.0,
step_size: float = 1.0,
type_: Literal["int", "float"] = "float",
) -> None:
super().__init__()
if type_ not in {"float", "int"}:
logger.error("Unknown type '%s'. Using 'float'.", type_)
type_ = "float"
self._step_size = step_size
self._value = value
self._min = min_
self._max = max_
self._type = type_
self.step_size = step_size
self.value = value
self.min = min_
self.max = max_
@property
def min(self) -> float:
"""The min property."""
return self._min
def __setattr__(self, name: str, value: Any) -> None:
if name in ["value", "step_size"]:
value = int(value) if self._type == "int" else float(value)
elif not name.startswith("_"):
value = float(value)
@property
def max(self) -> float:
"""The min property."""
return self._max
return super().__setattr__(name, value)
@property
def step_size(self) -> float:
"""The min property."""
return self._step_size
@property
def value(self) -> Any:
"""The value property."""
return self._value
@value.setter
def value(self, value: Any) -> None:
self._value = value

View File

@@ -1,13 +1,13 @@
{
"files": {
"main.css": "/static/css/main.2d8458eb.css",
"main.js": "/static/js/main.7f907b0f.js",
"main.js": "/static/js/main.da9f921a.js",
"index.html": "/index.html",
"main.2d8458eb.css.map": "/static/css/main.2d8458eb.css.map",
"main.7f907b0f.js.map": "/static/js/main.7f907b0f.js.map"
"main.da9f921a.js.map": "/static/js/main.da9f921a.js.map"
},
"entrypoints": [
"static/css/main.2d8458eb.css",
"static/js/main.7f907b0f.js"
"static/js/main.da9f921a.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.7f907b0f.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.da9f921a.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,19 +177,21 @@ def parse_list_attr_and_index(attr_string: str) -> tuple[str, int | None]:
return attr_name, index
def get_component_class_names() -> list[str]:
def get_component_classes() -> list[type]:
"""
Returns the names of the component classes in a list.
It takes the names from the pydase/components/__init__.py file, so this file should
always be up-to-date with the currently available components.
Returns:
list[str]: List of component class names
Returns references to the component classes in a list.
"""
import pydase.components
return pydase.components.__all__
return [
getattr(pydase.components, cls_name) for cls_name in pydase.components.__all__
]
def get_data_service_class_reference() -> Any:
import pydase.data_service.data_service
return getattr(pydase.data_service.data_service, "DataService")
def is_property_attribute(target_obj: Any, attr_name: str) -> bool:

View File

@@ -8,7 +8,8 @@ import pydase.units as u
from pydase.data_service.abstract_data_service import AbstractDataService
from pydase.utils.helpers import (
get_attribute_doc,
get_component_class_names,
get_component_classes,
get_data_service_class_reference,
parse_list_attr_and_index,
)
@@ -157,24 +158,26 @@ class Serializer:
def _serialize_data_service(obj: AbstractDataService) -> dict[str, Any]:
readonly = False
doc = get_attribute_doc(obj)
obj_type = type(obj).__name__
if type(obj).__name__ not in get_component_class_names():
obj_type = "DataService"
obj_type = "DataService"
# Get the dictionary of the base class
base_set = set(type(obj).__base__.__dict__)
# Get the dictionary of the derived class
derived_set = set(type(obj).__dict__)
# Get the difference between the two dictionaries
derived_only_set = derived_set - base_set
# Get component base class if any
component_base_cls = next(
(cls for cls in get_component_classes() if isinstance(obj, cls)), None
)
if component_base_cls:
obj_type = component_base_cls.__name__
# Get the set of DataService class attributes
data_service_attr_set = set(dir(get_data_service_class_reference()))
# Get the set of the object attributes
obj_attr_set = set(dir(obj))
# Get the difference between the two sets
derived_only_attr_set = obj_attr_set - data_service_attr_set
instance_dict = set(obj.__dict__)
# Merge the class and instance dictionaries
merged_set = derived_only_set | instance_dict
value = {}
# Iterate over attributes, properties, class attributes, and methods
for key in sorted(merged_set):
for key in sorted(derived_only_attr_set):
if key.startswith("_"):
continue # Skip attributes that start with underscore

View File

@@ -1,46 +1,83 @@
import logging
from collections.abc import Callable
from pydase.components.number_slider import NumberSlider
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 pytest import LogCaptureFixture
from tests.utils.test_serializer import pytest
def test_NumberSlider(caplog: LogCaptureFixture) -> None:
class ServiceClass(DataService):
number_slider = NumberSlider(1, 0, 10, 1)
int_number_slider = NumberSlider(1, 0, 10, 1, "int")
logger = logging.getLogger(__name__)
service_instance = ServiceClass()
def test_number_slider(caplog: LogCaptureFixture) -> None:
class MySlider(NumberSlider):
def __init__(
self,
value: float = 0,
min_: float = 0,
max_: float = 100,
step_size: float = 1,
callback: Callable[..., None] = lambda: None,
) -> None:
super().__init__(value, min_, max_, step_size)
self._callback = callback
@property
def value(self) -> float:
return self._value
@value.setter
def value(self, value: float) -> None:
self._callback(value)
self._value = value
@property
def max(self) -> float:
return self._max
@max.setter
def max(self, value: float) -> None:
self._max = value
@property
def step_size(self) -> float:
return self._step_size
@step_size.setter
def step_size(self, value: float) -> None:
self._step_size = value
class MyService(DataService):
def __init__(self) -> None:
super().__init__()
self.my_slider = MySlider(callback=self.some_method)
def some_method(self, slider_value: float) -> None:
logger.info("Slider changed to '%s'", slider_value)
service_instance = MyService()
state_manager = StateManager(service_instance)
DataServiceObserver(state_manager)
assert service_instance.number_slider.value == 1
assert isinstance(service_instance.number_slider.value, float)
assert service_instance.number_slider.min == 0
assert isinstance(service_instance.number_slider.min, float)
assert service_instance.number_slider.max == 10
assert isinstance(service_instance.number_slider.max, float)
assert service_instance.number_slider.step_size == 1
assert isinstance(service_instance.number_slider.step_size, float)
service_instance.my_slider.value = 10.0
assert service_instance.int_number_slider.value == 1
assert isinstance(service_instance.int_number_slider.value, int)
assert service_instance.int_number_slider.step_size == 1
assert isinstance(service_instance.int_number_slider.step_size, int)
service_instance.number_slider.value = 10.0
service_instance.int_number_slider.value = 10.1
assert "'number_slider.value' changed to '10.0'" in caplog.text
assert "'int_number_slider.value' changed to '10'" in caplog.text
assert "'my_slider.value' changed to '10.0'" in caplog.text
assert "Slider changed to '10.0'" in caplog.text
caplog.clear()
service_instance.number_slider.min = 1.1
service_instance.my_slider.max = 12.0
assert "'number_slider.min' changed to '1.1'" in caplog.text
assert "'my_slider.max' changed to '12.0'" in caplog.text
caplog.clear()
service_instance.my_slider.step_size = 0.1
def test_init_error(caplog: LogCaptureFixture) -> None:
number_slider = NumberSlider(type_="str") # type: ignore # noqa
assert "'my_slider.step_size' changed to '0.1'" in caplog.text
caplog.clear()
assert "Unknown type 'str'. Using 'float'" in caplog.text
# by overriding the getter only you can make the property read-only
with pytest.raises(AttributeError):
service_instance.my_slider.min = 1.1 # type: ignore[reportGeneralTypeIssues, misc]

View File

@@ -294,6 +294,30 @@ def test_dict_serialization() -> None:
}
def test_derived_data_service_serialization() -> None:
class BaseService(pydase.DataService):
class_attr = 1337
def __init__(self) -> None:
super().__init__()
self._name = "Service"
@property
def name(self) -> str:
return self._name
@name.setter
def name(self, value: str) -> None:
self._name = value
class DerivedService(BaseService):
...
base_instance = BaseService()
service_instance = DerivedService()
assert service_instance.serialize() == base_instance.serialize()
@pytest.fixture
def setup_dict():
class MySubclass(pydase.DataService):