24 Commits

Author SHA1 Message Date
Mose Müller
aa85f6453f update version to v0.2.0 2023-10-11 14:20:35 +02:00
Mose Müller
343354e0ee Update README.md 2023-10-11 14:17:40 +02:00
Mose Müller
b38bb05c69 Merge pull request #28 from tiqi-group/5-adding-status-component
5 adding coloured enum component
2023-10-11 14:15:34 +02:00
Mose Müller
a0dab630f9 Update README.md 2023-10-11 14:03:07 +02:00
Mose Müller
a9db7848f7 fix: pytest failed after moving from StrEnum to Enum 2023-10-11 14:03:07 +02:00
Mose Müller
a8b14180ad fix: using Enum instead of StrEnum (>=3.11 only) 2023-10-11 14:03:07 +02:00
Mose Müller
26a366842a frontend: npm run build 2023-10-11 14:03:07 +02:00
Mose Müller
b0e7de2d2c docs: updating Readme 2023-10-11 14:03:07 +02:00
Mose Müller
bbcba8b39f test: adding test for ColouredEnum component 2023-10-11 14:03:07 +02:00
Mose Müller
34e46e05ee feat: adding ColouredEnum component 2023-10-11 14:03:07 +02:00
Mose Müller
93c2f5ab70 docs: updating mkdocs documentation
- adding user guide section
- removing "baselevel: 4"
2023-10-11 13:58:55 +02:00
Mose Müller
106ffbfc40 removing .python-version 2023-10-11 13:52:13 +02:00
Mose Müller
5702adbdbd update version 2023-10-10 14:54:17 +02:00
Mose Müller
e3a7932ac4 Merge pull request #26 from tiqi-group/24-issue-pydase-checks-if-protected-variables-inherit-from-dataservice
fix: only check inheritance of public attributes
2023-10-10 12:54:45 +02:00
Mose Müller
21cd039610 fix: only check inheritance of public attributes 2023-10-10 12:51:50 +02:00
Mose Müller
0b5dd5393a remove publish-to-testpypi workflow 2023-10-10 12:35:52 +02:00
Mose Müller
0f7a8ec63b Merge pull request #25 from tiqi-group/20-property-that-returns-an-enum-is-not-correctly-rendered
Fixes serialization for enum properties
2023-10-10 12:32:53 +02:00
Mose Müller
6fe30fc6ec update version 2023-10-10 12:31:43 +02:00
Mose Müller
5f64ec131c feat: adding enum_serialize test 2023-10-10 12:29:45 +02:00
Mose Müller
06bf5fb539 fix: serializing enum properties 2023-10-10 12:18:13 +02:00
Mose Müller
bdbb79e131 feat: removing "install_signal_handlers" method from protocol (would have been overwritten anyway) 2023-10-03 16:15:08 +02:00
Mose Müller
78b94d4fc8 feat: updating method docstring 2023-10-03 16:12:07 +02:00
Mose Müller
6d12371e7b docs: update installation instruction 2023-09-26 15:12:53 +02:00
Mose Müller
fb6146e01d docs: Updating links and formatting in Readme 2023-09-26 15:03:11 +02:00
22 changed files with 445 additions and 122 deletions

View File

@@ -86,26 +86,26 @@ jobs:
'${{ github.ref_name }}' dist/**
--repo '${{ github.repository }}'
publish-to-testpypi:
name: Publish Python 🐍 distribution 📦 to TestPyPI
needs:
- build
runs-on: ubuntu-latest
environment:
name: testpypi
url: https://test.pypi.org/p/pydase
permissions:
id-token: write # IMPORTANT: mandatory for trusted publishing
steps:
- name: Download all the dists
uses: actions/download-artifact@v3
with:
name: python-package-distributions
path: dist/
- name: Publish distribution 📦 to TestPyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
repository-url: https://test.pypi.org/legacy/
# publish-to-testpypi:
# name: Publish Python 🐍 distribution 📦 to TestPyPI
# needs:
# - build
# runs-on: ubuntu-latest
#
# environment:
# name: testpypi
# url: https://test.pypi.org/p/pydase
#
# permissions:
# id-token: write # IMPORTANT: mandatory for trusted publishing
#
# steps:
# - name: Download all the dists
# uses: actions/download-artifact@v3
# with:
# name: python-package-distributions
# path: dist/
# - name: Publish distribution 📦 to TestPyPI
# uses: pypa/gh-action-pypi-publish@release/v1
# with:
# repository-url: https://test.pypi.org/legacy/

View File

@@ -44,13 +44,13 @@
Install pydase using [`poetry`](https://python-poetry.org/):
```bash
poetry add git+https://github.com/tiqi-group/pydase.git
poetry add pydase
```
or `pip`:
```bash
pip install git+https://github.com/tiqi-group/pydase.git
pip install pydase
```
<!--installation-end-->
@@ -129,7 +129,7 @@ if __name__ == "__main__":
Server(service).run()
```
This will start the server, making your Device service accessible via RPC and a web server at http://localhost:8001.
This will start the server, making your Device service accessible via RPC and a web server at [http://localhost:8001](http://localhost:8001).
### Accessing the Web Interface
@@ -159,11 +159,15 @@ In this example, replace `<ip_addr>` with the IP address of the machine where th
<!--usage-end-->
## Understanding the Component System
<!-- Component User Guide Start -->
In `pydase`, components are fundamental building blocks that bridge the Python backend logic with frontend visual representation and interactions. This system can be understood based on the following categories:
### Built-in Type and Enum Components
`pydase` automatically maps standard Python data types to their corresponding frontend components:
- `str`: Translated into a `StringComponent` on the frontend.
- `int` and `float`: Manifested as the `NumberComponent`.
- `bool`: Rendered as a `ButtonComponent`.
@@ -173,6 +177,7 @@ In `pydase`, components are fundamental building blocks that bridge the Python b
### Method Components
Methods within the `DataService` class have frontend representations:
- Regular Methods: These are rendered as a `MethodComponent` in the frontend, allowing users to execute the method via an "execute" button.
- Asynchronous Methods: These are manifested as the `AsyncMethodComponent` with "start"/"stop" buttons to manage the execution of [tasks](#understanding-tasks-in-pydase).
@@ -228,7 +233,11 @@ The custom components in `pydase` have two main parts:
Below are the components available in the `pydase.components` module, accompanied by their Python usage:
- `Image`: This component allows users to display and update images within the application.
#### `Image`
This component provides a versatile interface for displaying images within the application. Users can update and manage images from various sources, including local paths, URLs, and even matplotlib figures.
The component offers methods to load images seamlessly, ensuring that visual content is easily integrated and displayed within the data service.
```python
import matplotlib.pyplot as plt
@@ -262,7 +271,11 @@ Below are the components available in the `pydase.components` module, accompanie
![Image Component](docs/images/Image_component.png)
- `NumberSlider`: An interactive slider component to adjust numerical values, including floats and integers, on the frontend while synchronizing the data with the backend in real-time.
#### `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 slider can be customized with initial values, minimum and maximum limits, and step sizes to fit various use cases.
```python
import pydase
@@ -270,7 +283,7 @@ Below are the components available in the `pydase.components` module, accompanie
class MyService(pydase.DataService):
slider = NumberSlider(value=3.5, min=0, max=10, step_size=0.1)
slider = NumberSlider(value=3.5, min=0, max=10, step_size=0.1, type="float")
if __name__ == "__main__":
@@ -280,9 +293,51 @@ Below are the components available in the `pydase.components` module, accompanie
![Slider Component](docs/images/Slider_component.png)
#### `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.
If the property associated with the `ColouredEnum` has a setter function, the keys of the enum will be rendered as a dropdown menu, allowing users to interact and select different options. Without a setter function, the selected key will simply be displayed as a coloured box with text inside, serving as a visual indicator.
```python
import pydase
import pydase.components as pyc
class MyStatus(pyc.ColouredEnum):
PENDING = "#FFA500" # Hexadecimal colour (Orange)
RUNNING = "#0000FF80" # Hexadecimal colour with transparency (Blue)
PAUSED = "rgb(169, 169, 169)" # RGB colour (Dark Gray)
RETRYING = "rgba(255, 255, 0, 0.3)" # RGB colour with transparency (Yellow)
COMPLETED = "hsl(120, 100%, 50%)" # HSL colour (Green)
FAILED = "hsla(0, 100%, 50%, 0.7)" # HSL colour with transparency (Red)
CANCELLED = "SlateGray" # Cross-browser colour name (Slate Gray)
class StatusTest(pydase.DataService):
_status = MyStatus.RUNNING
@property
def status(self) -> MyStatus:
return self._status
@status.setter
def status(self, value: MyStatus) -> None:
# do something ...
self._status = value
# Modifying or accessing the status value:
my_service = StatusExample()
my_service.status = MyStatus.FAILED
```
![ColouredEnum Component](docs/images/ColouredEnum_component.png)
#### Extending with New Components
Users can also extend the library by creating custom components. This involves defining the behavior on the Python backend and the visual representation on the frontend. For those looking to introduce new components, the [guide on adding components](./docs/dev-guide/Adding_Components.md) provides detailed steps on achieving this.
Users can also extend the library by creating custom components. This involves defining the behavior on the Python backend and the visual representation on the frontend. For those looking to introduce new components, the [guide on adding components](https://pydase.readthedocs.io/en/latest/dev-guide/Adding_Components/) provides detailed steps on achieving this.
<!-- Component User Guide End -->
## Understanding Service Persistence
@@ -422,6 +477,7 @@ For more information about what you can do with the units, please consult the do
## Changing the Log Level
You can change the log level of loguru by either
1. (RECOMMENDED) setting the `ENVIRONMENT` environment variable to "production" or "development"
```bash
@@ -442,12 +498,12 @@ You can change the log level of loguru by either
## Documentation
The full documentation provides more detailed information about `pydase`, including advanced usage examples, API references, and tips for troubleshooting common issues. See the [full documentation](URL_TO_YOUR_DOCUMENTATION) for more information.
The full documentation provides more detailed information about `pydase`, including advanced usage examples, API references, and tips for troubleshooting common issues. See the [full documentation](https://pydase.readthedocs.io/en/latest/) for more information.
## Contributing
We welcome contributions! Please see [contributing.md](./docs/about/contributing.md) for details on how to contribute.
We welcome contributions! Please see [contributing.md](https://pydase.readthedocs.io/en/latest/about/contributing/) for details on how to contribute.
## License
`pydase` is licensed under the [MIT License](./LICENSE).
`pydase` is licensed under the [MIT License](https://github.com/tiqi-group/pydase/blob/main/LICENSE).

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -0,0 +1,7 @@
# Components Guide
{%
include-markdown "../../README.md"
start="<!-- Component User Guide Start -->"
end="<!-- Component User Guide End -->"
heading-offset=-2
%}

View File

@@ -0,0 +1,75 @@
import React, { useEffect, useRef } from 'react';
import { InputGroup, Form, Row, Col } from 'react-bootstrap';
import { emit_update } from '../socket';
import { DocStringComponent } from './DocStringComponent';
interface ColouredEnumComponentProps {
name: string;
parentPath: string;
value: string;
docString?: string;
readOnly: boolean;
enumDict: Record<string, string>;
addNotification: (string) => void;
}
export const ColouredEnumComponent = React.memo((props: ColouredEnumComponentProps) => {
const {
name,
parentPath: parentPath,
value,
docString,
enumDict,
readOnly,
addNotification
} = props;
const renderCount = useRef(0);
useEffect(() => {
renderCount.current++;
});
useEffect(() => {
addNotification(`${parentPath}.${name} changed to ${value}.`);
}, [props.value]);
const handleValueChange = (newValue) => {
console.log(newValue);
emit_update(name, parentPath, newValue);
};
return (
<div className={'enumComponent'} id={parentPath.concat('.' + name)}>
{process.env.NODE_ENV === 'development' && (
<p>Render count: {renderCount.current}</p>
)}
<DocStringComponent docString={docString} />
<Row>
<Col className="d-flex align-items-center">
<InputGroup.Text>{name}</InputGroup.Text>
{readOnly ? (
// Display the Form.Control when readOnly is true
<Form.Control
value={value}
disabled={true}
style={{ backgroundColor: enumDict[value] }}
/>
) : (
// Display the Form.Select when readOnly is false
<Form.Select
aria-label="coloured-enum-select"
value={value}
style={{ backgroundColor: enumDict[value] }}
onChange={(event) => handleValueChange(event.target.value)}>
{Object.entries(enumDict).map(([key, val]) => (
<option key={key} value={key}>
{key}
</option>
))}
</Form.Select>
)}
</Col>
</Row>
</div>
);
});

View File

@@ -9,6 +9,7 @@ import { StringComponent } from './StringComponent';
import { ListComponent } from './ListComponent';
import { DataServiceComponent, DataServiceJSON } from './DataServiceComponent';
import { ImageComponent } from './ImageComponent';
import { ColouredEnumComponent } from './ColouredEnumComponent';
type AttributeType =
| 'str'
@@ -21,7 +22,8 @@ type AttributeType =
| 'DataService'
| 'Enum'
| 'NumberSlider'
| 'Image';
| 'Image'
| 'ColouredEnum';
type ValueType = boolean | string | number | object;
export interface Attribute {
@@ -181,6 +183,19 @@ export const GenericComponent = React.memo(
addNotification={addNotification}
/>
);
} else if (attribute.type === 'ColouredEnum') {
console.log(attribute);
return (
<ColouredEnumComponent
name={name}
parentPath={parentPath}
docString={attribute.doc}
value={String(attribute.value)}
readOnly={attribute.readonly}
enumDict={attribute.enum}
addNotification={addNotification}
/>
);
} else {
return <div key={name}>{name}</div>;
}

View File

@@ -4,6 +4,8 @@ edit_uri: blob/docs/docs/
nav:
- Home: index.md
- Getting Started: getting-started.md
- User Guide:
- Components Guide: user-guide/Components.md
- Developer Guide:
- Developer Guide: dev-guide/README.md
- API Reference: dev-guide/api.md
@@ -22,7 +24,6 @@ markdown_extensions:
- smarty
- toc:
permalink: true
baselevel: 4
- pymdownx.highlight:
anchor_linenums: true
- pymdownx.snippets
@@ -38,5 +39,3 @@ plugins:
watch:
- src/pydase

View File

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

@@ -27,10 +27,12 @@ print(my_service.voltage.value) # Output: 5
```
"""
from pydase.components.coloured_enum import ColouredEnum
from pydase.components.image import Image
from pydase.components.number_slider import NumberSlider
__all__ = [
"NumberSlider",
"Image",
"ColouredEnum",
]

View File

@@ -0,0 +1,61 @@
from enum import Enum
class ColouredEnum(Enum):
"""
Represents a UI element that can display colour-coded text based on its value.
This class extends the standard Enum but requires its values to be valid CSS
colour codes. Supported colour formats include:
- Hexadecimal colours
- Hexadecimal colours with transparency
- RGB colours
- RGBA colours
- HSL colours
- HSLA colours
- Predefined/Cross-browser colour names
Refer to the this website for more details on colour formats:
(https://www.w3schools.com/cssref/css_colours_legal.php)
The behavior of this component in the UI depends on how it's defined in the data
service:
- As property with a setter or as attribute: Renders as a dropdown menu,
allowing users to select and change its value from the frontend.
- As property without a setter: Displays as a coloured box with the key of the
`ColouredEnum` as text inside, serving as a visual indicator without user
interaction.
Example:
--------
```python
import pydase.components as pyc
import pydase
class MyStatus(pyc.ColouredEnum):
PENDING = "#FFA500" # Orange
RUNNING = "#0000FF80" # Transparent Blue
PAUSED = "rgb(169, 169, 169)" # Dark Gray
RETRYING = "rgba(255, 255, 0, 0.3)" # Transparent Yellow
COMPLETED = "hsl(120, 100%, 50%)" # Green
FAILED = "hsla(0, 100%, 50%, 0.7)" # Transparent Red
CANCELLED = "SlateGray" # Slate Gray
class StatusExample(pydase.DataService):
_status = MyStatus.RUNNING
@property
def status(self) -> MyStatus:
return self._status
@status.setter
def status(self, value: MyStatus) -> None:
# Custom logic here...
self._status = value
# Example usage:
my_service = StatusExample()
my_service.status = MyStatus.FAILED
```
"""
pass

View File

@@ -83,8 +83,9 @@ class DataService(rpyc.Service, AbstractDataService):
def __check_instance_classes(self) -> None:
for attr_name, attr_value in get_class_and_instance_attributes(self).items():
# every class defined by the user should inherit from DataService
if not attr_name.startswith("_DataService__"):
# every class defined by the user should inherit from DataService if it is
# assigned to a public attribute
if not attr_name.startswith("_"):
warn_if_instance_class_does_not_inherit_from_DataService(attr_value)
def __set_attribute_based_on_type( # noqa:CFQ002
@@ -198,22 +199,10 @@ class DataService(rpyc.Service, AbstractDataService):
value, readonly status, and documentation if any in the resulting dictionary.
Attributes and methods starting with an underscore are ignored.
For attributes, methods, and properties unique to the class (not inherited from
the base class), the method uses the format "<prefix>.<key>" for keys in the
dictionary. If no prefix is provided, the key format is simply "<key>".
For nested DataService instances, the method serializes recursively and appends
the key of the nested instance to the prefix in the format "<prefix>.<key>".
For nested DataService instances, the method serializes recursively.
For attributes of type list, each item in the list is serialized individually.
If an item in the list is an instance of DataService, it is serialized
recursively with its key in the format "<prefix>.<key>.<item_id>", where
"item_id" is the id of the item itself.
Args:
prefix (str, optional): The prefix for each key in the serialized
dictionary. This is mainly used when this method is called recursively to
maintain the structure of nested instances.
recursively.
Returns:
dict: The serialized instance.
@@ -321,19 +310,13 @@ class DataService(rpyc.Service, AbstractDataService):
"readonly": True,
"value": running_task_info,
}
elif isinstance(getattr(self.__class__, key, None), property):
prop: property = getattr(self.__class__, key)
result[key] = {
"type": type(value).__name__,
"value": value
if not isinstance(value, u.Quantity)
else {"magnitude": value.m, "unit": str(value.u)},
"readonly": prop.fset is None,
"doc": get_attribute_doc(prop),
}
elif isinstance(value, Enum):
if type(value).__base__.__name__ == "ColouredEnum":
val_type = "ColouredEnum"
else:
val_type = "Enum"
result[key] = {
"type": "Enum",
"type": val_type,
"value": value.name,
"enum": {
name: member.value
@@ -352,6 +335,11 @@ class DataService(rpyc.Service, AbstractDataService):
"doc": get_attribute_doc(value),
}
if isinstance(getattr(self.__class__, key, None), property):
prop: property = getattr(self.__class__, key)
result[key]["readonly"] = prop.fset is None
result[key]["doc"] = get_attribute_doc(prop)
return result
def update_DataService_attribute(

View File

@@ -1,13 +1,13 @@
{
"files": {
"main.css": "/static/css/main.398bc7f8.css",
"main.js": "/static/js/main.c348625e.js",
"main.js": "/static/js/main.553ca0c9.js",
"index.html": "/index.html",
"main.398bc7f8.css.map": "/static/css/main.398bc7f8.css.map",
"main.c348625e.js.map": "/static/js/main.c348625e.js.map"
"main.553ca0c9.js.map": "/static/js/main.553ca0c9.js.map"
},
"entrypoints": [
"static/css/main.398bc7f8.css",
"static/js/main.c348625e.js"
"static/js/main.553ca0c9.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.c348625e.js"></script><link href="/static/css/main.398bc7f8.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.553ca0c9.js"></script><link href="/static/css/main.398bc7f8.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

@@ -28,9 +28,8 @@ class AdditionalServerProtocol(Protocol):
This protocol sets the standard for how additional servers should be implemented
to ensure compatibility with the main Server class. The protocol requires that
any server implementing it should have an __init__ method for initialization, a
serve method for starting the server, and an install_signal_handlers method for
setting up signal handlers.
any server implementing it should have an __init__ method for initialization and a
serve method for starting the server.
Parameters:
-----------
@@ -62,12 +61,6 @@ class AdditionalServerProtocol(Protocol):
"""
...
def install_signal_handlers(self) -> None:
"""Sets up signal handlers for the server. This method is used to define how the
server should respond to various system signals, such as SIGINT and SIGTERM.
"""
...
class AdditionalServer(TypedDict):
"""
@@ -257,13 +250,6 @@ class Server:
info=self._info,
**server["kwargs"],
)
try:
addin_server.install_signal_handlers = lambda: None # type: ignore
except Exception:
logger.debug(
"Additional server does not have a method called "
"'install_signal_handlers'."
)
server_name = (
addin_server.__module__ + "." + addin_server.__class__.__name__

View File

@@ -1,5 +1,7 @@
from loguru import logger
from pydase.utils.helpers import get_component_class_names
def warn_if_instance_class_does_not_inherit_from_DataService(__value: object) -> None:
base_class_name = __value.__class__.__base__.__name__
@@ -13,7 +15,8 @@ def warn_if_instance_class_does_not_inherit_from_DataService(__value: object) ->
"asyncio.unix_events",
"_abc",
]
and base_class_name not in ["DataService", "list", "Enum"]
and base_class_name
not in ["DataService", "list", "Enum"] + get_component_class_names()
and type(__value).__name__ not in ["CallbackManager", "TaskManager", "Quantity"]
):
logger.warning(

View File

@@ -0,0 +1,51 @@
from pytest import CaptureFixture, LogCaptureFixture
from pydase.components.coloured_enum import ColouredEnum
from pydase.data_service.data_service import DataService
from .. import caplog # noqa
def test_ColouredEnum(capsys: CaptureFixture) -> None:
class MyStatus(ColouredEnum):
RUNNING = "#00FF00"
FAILING = "#FF0000"
class ServiceClass(DataService):
_status = MyStatus.RUNNING
@property
def status(self) -> MyStatus:
return self._status
@status.setter
def status(self, value: MyStatus) -> None:
# do something ...
self._status = value
service = ServiceClass()
service.status = MyStatus.FAILING
captured = capsys.readouterr()
expected_output = sorted(
[
"ServiceClass.status = MyStatus.FAILING",
]
)
actual_output = sorted(captured.out.strip().split("\n")) # type: ignore
assert actual_output == expected_output
def test_warning(caplog: LogCaptureFixture) -> None: # noqa
class MyStatus(ColouredEnum):
RUNNING = "#00FF00"
FAILING = "#FF0000"
class ServiceClass(DataService):
status = MyStatus.RUNNING
assert (
"Warning: Class MyStatus does not inherit from DataService." not in caplog.text
)

View File

@@ -0,0 +1,64 @@
from enum import Enum
import pydase
def test_enum_serialize() -> None:
class EnumClass(Enum):
FOO = "foo"
BAR = "bar"
class EnumAttribute(pydase.DataService):
def __init__(self) -> None:
self.some_enum = EnumClass.FOO
super().__init__()
class EnumPropertyWithoutSetter(pydase.DataService):
def __init__(self) -> None:
self._some_enum = EnumClass.FOO
super().__init__()
@property
def some_enum(self) -> EnumClass:
return self._some_enum
class EnumPropertyWithSetter(pydase.DataService):
def __init__(self) -> None:
self._some_enum = EnumClass.FOO
super().__init__()
@property
def some_enum(self) -> EnumClass:
return self._some_enum
@some_enum.setter
def some_enum(self, value: EnumClass) -> None:
self._some_enum = value
assert EnumAttribute().serialize() == {
"some_enum": {
"type": "Enum",
"value": "FOO",
"enum": {"FOO": "foo", "BAR": "bar"},
"readonly": False,
"doc": None,
}
}
assert EnumPropertyWithoutSetter().serialize() == {
"some_enum": {
"type": "Enum",
"value": "FOO",
"enum": {"FOO": "foo", "BAR": "bar"},
"readonly": True,
"doc": None,
}
}
assert EnumPropertyWithSetter().serialize() == {
"some_enum": {
"type": "Enum",
"value": "FOO",
"enum": {"FOO": "foo", "BAR": "bar"},
"readonly": False,
"doc": None,
}
}

View File

@@ -2,7 +2,7 @@ from pytest import LogCaptureFixture
from pydase import DataService
from . import caplog # noqa
from .. import caplog # noqa
def test_setattr_warnings(caplog: LogCaptureFixture) -> None: # noqa
@@ -32,3 +32,19 @@ def test_private_attribute_warning(caplog: LogCaptureFixture) -> None: # noqa
" Warning: You should not set private but rather protected attributes! Use "
"_something instead of __something." in caplog.text
)
def test_protected_attribute_warning(caplog: LogCaptureFixture) -> None: # noqa
class SubClass:
name = "Hello"
class ServiceClass(DataService):
def __init__(self) -> None:
self._subclass = SubClass
super().__init__()
ServiceClass()
assert (
"Warning: Class SubClass does not inherit from DataService." not in caplog.text
)