Merge pull request #84 from tiqi-group/75-numberslider-component-is-not-working

75 numberslider component is not working
This commit is contained in:
Mose Müller 2023-12-13 11:12:56 +01:00 committed by GitHub
commit b4edc31030
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 349 additions and 148 deletions

121
README.md
View File

@ -42,8 +42,7 @@
- [Saving and restoring the service state for service persistence](#understanding-service-persistence) - [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) - [Automated task management with built-in start/stop controls and optional autostart](#understanding-tasks-in-pydase)
- [Support for units](#understanding-units-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 ## Installation
@ -286,26 +285,132 @@ if __name__ == "__main__":
#### `NumberSlider` #### `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 ```python
import pydase 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): 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__": if __name__ == "__main__":
service = MyService() service_instance = MyService()
pydase.Server(service).run() 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) ![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()
````
#### `ColouredEnum` #### `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. 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.

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} parentPath={parentPath}
docString={attribute.doc} docString={attribute.doc}
readOnly={attribute.readonly} readOnly={attribute.readonly}
value={attribute.value['value']['value']} value={attribute.value['value']}
min={attribute.value['min']['value']} min={attribute.value['min']}
max={attribute.value['max']['value']} max={attribute.value['max']}
stepSize={attribute.value['step_size']['value']} stepSize={attribute.value['step_size']}
isInstantUpdate={isInstantUpdate} isInstantUpdate={isInstantUpdate}
addNotification={addNotification} addNotification={addNotification}
/> />

View File

@ -8,6 +8,29 @@ import { LevelName } from './NotificationsComponent';
// TODO: add button functionality // 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 { interface NumberComponentProps {
name: string; name: string;
type: 'float' | 'int'; type: 'float' | 'int';
@ -18,12 +41,6 @@ interface NumberComponentProps {
isInstantUpdate: boolean; isInstantUpdate: boolean;
unit?: string; unit?: string;
showName?: boolean; showName?: boolean;
customEmitUpdate?: (
name: string,
parent_path: string,
value: number,
callback?: (ack: unknown) => void
) => void;
addNotification: (message: string, levelname?: LevelName) => 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) // Whether to show the name infront of the component (false if used with a slider)
const showName = props.showName !== undefined ? props.showName : true; 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); const renderCount = useRef(0);
// Create a state for the cursor position // Create a state for the cursor position
@ -263,7 +276,7 @@ export const NumberComponent = React.memo((props: NumberComponentProps) => {
selectionEnd selectionEnd
)); ));
} else if (key === 'Enter' && !isInstantUpdate) { } else if (key === 'Enter' && !isInstantUpdate) {
emitUpdate(name, parentPath, Number(newValue)); setAttribute(name, parentPath, Number(newValue));
return; return;
} else { } else {
console.debug(key); console.debug(key);
@ -272,7 +285,7 @@ export const NumberComponent = React.memo((props: NumberComponentProps) => {
// Update the input value and maintain the cursor position // Update the input value and maintain the cursor position
if (isInstantUpdate) { if (isInstantUpdate) {
emitUpdate(name, parentPath, Number(newValue)); setAttribute(name, parentPath, Number(newValue));
} }
setInputString(newValue); setInputString(newValue);
@ -284,7 +297,7 @@ export const NumberComponent = React.memo((props: NumberComponentProps) => {
const handleBlur = () => { const handleBlur = () => {
if (!isInstantUpdate) { if (!isInstantUpdate) {
// If not in "instant update" mode, emit an update when the input field loses focus // 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 { setAttribute } from '../socket';
import { DocStringComponent } from './DocStringComponent'; import { DocStringComponent } from './DocStringComponent';
import { Slider } from '@mui/material'; import { Slider } from '@mui/material';
import { NumberComponent } from './NumberComponent'; import { NumberComponent, NumberObject } from './NumberComponent';
import { getIdFromFullAccessPath } from '../utils/stringUtils'; import { getIdFromFullAccessPath } from '../utils/stringUtils';
import { LevelName } from './NotificationsComponent'; import { LevelName } from './NotificationsComponent';
interface SliderComponentProps { interface SliderComponentProps {
name: string; name: string;
min: number; min: NumberObject;
max: number; max: NumberObject;
parentPath?: string; parentPath?: string;
value: number; value: NumberObject;
readOnly: boolean; readOnly: boolean;
docString: string; docString: string;
stepSize: number; stepSize: NumberObject;
isInstantUpdate: boolean; isInstantUpdate: boolean;
addNotification: (message: string, levelname?: LevelName) => void; addNotification: (message: string, levelname?: LevelName) => void;
} }
@ -30,7 +30,6 @@ export const SliderComponent = React.memo((props: SliderComponentProps) => {
min, min,
max, max,
stepSize, stepSize,
readOnly,
docString, docString,
isInstantUpdate, isInstantUpdate,
addNotification addNotification
@ -58,52 +57,41 @@ export const SliderComponent = React.memo((props: SliderComponentProps) => {
addNotification(`${parentPath}.${name}.stepSize changed to ${stepSize}.`); addNotification(`${parentPath}.${name}.stepSize changed to ${stepSize}.`);
}, [props.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[]) => { const handleOnChange = (event, newNumber: number | number[]) => {
// This will never be the case as we do not have a range slider. However, we should // This will never be the case as we do not have a range slider. However, we should
// make sure this is properly handled. // make sure this is properly handled.
if (Array.isArray(newNumber)) { if (Array.isArray(newNumber)) {
newNumber = newNumber[0]; newNumber = newNumber[0];
} }
emitSliderUpdate(name, parentPath, newNumber); setAttribute(`${name}.value`, parentPath, newNumber);
}; };
const handleValueChange = (newValue: number, valueType: string) => { const handleValueChange = (newValue: number, valueType: string) => {
switch (valueType) { setAttribute(`${name}.${valueType}`, parentPath, newValue);
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;
}
}; };
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 ( return (
<div className="sliderComponent" id={id}> <div className="sliderComponent" id={id}>
{process.env.NODE_ENV === 'development' && ( {process.env.NODE_ENV === 'development' && (
@ -120,15 +108,15 @@ export const SliderComponent = React.memo((props: SliderComponentProps) => {
style={{ margin: '0px 0px 10px 0px' }} style={{ margin: '0px 0px 10px 0px' }}
aria-label="Always visible" aria-label="Always visible"
// valueLabelDisplay="on" // valueLabelDisplay="on"
disabled={readOnly} disabled={valueReadOnly}
value={value} value={valueMagnitude}
onChange={(event, newNumber) => handleOnChange(event, newNumber)} onChange={(event, newNumber) => handleOnChange(event, newNumber)}
min={min} min={minMagnitude}
max={max} max={maxMagnitude}
step={stepSize} step={stepSizeMagnitude}
marks={[ marks={[
{ value: min, label: `${min}` }, { value: minMagnitude, label: `${minMagnitude}` },
{ value: max, label: `${max}` } { value: maxMagnitude, label: `${maxMagnitude}` }
]} ]}
/> />
</Col> </Col>
@ -136,13 +124,13 @@ export const SliderComponent = React.memo((props: SliderComponentProps) => {
<NumberComponent <NumberComponent
isInstantUpdate={isInstantUpdate} isInstantUpdate={isInstantUpdate}
parentPath={parentPath} parentPath={parentPath}
name={name} name={`${name}.value`}
docString="" docString=""
readOnly={readOnly} readOnly={valueReadOnly}
type="float" type="float"
value={value} value={valueMagnitude}
unit={valueUnit}
showName={false} showName={false}
customEmitUpdate={emitSliderUpdate}
addNotification={() => null} addNotification={() => null}
/> />
</Col> </Col>
@ -178,7 +166,8 @@ export const SliderComponent = React.memo((props: SliderComponentProps) => {
<Form.Label>Min Value</Form.Label> <Form.Label>Min Value</Form.Label>
<Form.Control <Form.Control
type="number" type="number"
value={min} value={minMagnitude}
disabled={minReadOnly}
onChange={(e) => handleValueChange(Number(e.target.value), 'min')} onChange={(e) => handleValueChange(Number(e.target.value), 'min')}
/> />
</Col> </Col>
@ -187,7 +176,8 @@ export const SliderComponent = React.memo((props: SliderComponentProps) => {
<Form.Label>Max Value</Form.Label> <Form.Label>Max Value</Form.Label>
<Form.Control <Form.Control
type="number" type="number"
value={max} value={maxMagnitude}
disabled={maxReadOnly}
onChange={(e) => handleValueChange(Number(e.target.value), 'max')} onChange={(e) => handleValueChange(Number(e.target.value), 'max')}
/> />
</Col> </Col>
@ -196,8 +186,9 @@ export const SliderComponent = React.memo((props: SliderComponentProps) => {
<Form.Label>Step Size</Form.Label> <Form.Label>Step Size</Form.Label>
<Form.Control <Form.Control
type="number" type="number"
value={stepSize} value={stepSizeMagnitude}
onChange={(e) => handleValueChange(Number(e.target.value), 'stepSize')} disabled={stepSizeReadOnly}
onChange={(e) => handleValueChange(Number(e.target.value), 'step_size')}
/> />
</Col> </Col>
</Row> </Row>

View File

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

@ -1,46 +1,83 @@
import logging
from collections.abc import Callable
from pydase.components.number_slider import NumberSlider from pydase.components.number_slider import NumberSlider
from pydase.data_service.data_service import DataService from pydase.data_service.data_service import DataService
from pydase.data_service.data_service_observer import DataServiceObserver 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
from pytest import LogCaptureFixture from pytest import LogCaptureFixture
from tests.utils.test_serializer import pytest
def test_NumberSlider(caplog: LogCaptureFixture) -> None: logger = logging.getLogger(__name__)
class ServiceClass(DataService):
number_slider = NumberSlider(1, 0, 10, 1)
int_number_slider = NumberSlider(1, 0, 10, 1, "int")
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) state_manager = StateManager(service_instance)
DataServiceObserver(state_manager) DataServiceObserver(state_manager)
assert service_instance.number_slider.value == 1 service_instance.my_slider.value = 10.0
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)
assert service_instance.int_number_slider.value == 1 assert "'my_slider.value' changed to '10.0'" in caplog.text
assert isinstance(service_instance.int_number_slider.value, int) assert "Slider changed to '10.0'" in caplog.text
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
caplog.clear() 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: assert "'my_slider.step_size' changed to '0.1'" in caplog.text
number_slider = NumberSlider(type_="str") # type: ignore # noqa 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]