17 Commits

Author SHA1 Message Date
Mose Müller
c45f1bd489 Merge pull request #258 from tiqi-group/release-v0.10.21
updates to v0.10.21
2025-08-28 13:31:09 +02:00
Mose Müller
5784818e5a updates to v0.10.21 2025-08-28 13:30:47 +02:00
Mose Müller
64a7097568 Merge pull request #257 from tiqi-group/remove-warning-on-type-change
removes _warn_on_type_change from DataService setattr
2025-08-28 13:27:36 +02:00
Mose Müller
5ef382728c removes warning test 2025-08-28 13:26:23 +02:00
Mose Müller
51d6189002 removes _warn_on_type_change from DataService setattr
Adding keys to dictionaries trigger this warning, so I would consider
this warning to not be useful any more.
2025-08-28 13:24:33 +02:00
Mose Müller
71e29c890e Merge pull request #256 from tiqi-group/release-v0.10.19
updates to version v0.10.19
2025-07-08 15:39:59 +02:00
Mose Müller
6e407ba1d6 updates to version v0.10.19 2025-07-08 15:39:40 +02:00
Mose Müller
4fb5e56aa8 Merge pull request #255 from tiqi-group/fix/property_observer_race_condition
fix: race-condition in PropertyObserver
2025-07-08 15:37:45 +02:00
Mose Müller
d55ba3a85f fix: race-condition in PropertyObserver
When a proxy of a pydase client initialised with
block_until_connected=False is set as an attribute of a data service, a
race condition can happen: when the client connects while the
DataServiceObserver is being initialised, the property_deps_dict
attribute might not be set yet while the DataServiceObserver was already
added as an observer to the client proxy. The proxy will then emit a
notification, which in turn tries to get the dependent properties from
the property_deps_dict attribute, which has not been initialised yet.
The resulting exception will not tell the proxy that the client has
connected.
2025-07-08 15:29:27 +02:00
Mose Müller
265d9a7ef5 Merge pull request #254 from tiqi-group/fix/serialize_exception
fix: serialize exception
2025-07-03 15:59:35 +02:00
Mose Müller
4cd36b4a2b tests: adds test for exception serialization 2025-07-03 15:55:41 +02:00
Mose Müller
1b2ff38aff fix: serializing exception that didn't take an argument
An exception that was instantiated without any argument could not be
serilaized before. Now, I check if any args were supplied and set the
value to an empty string if no args were passed.
2025-07-03 15:55:25 +02:00
Mose Müller
4b243985e8 Merge pull request #253 from tiqi-group/feat/reset_frontend_value_on_exception
feat: reset frontend value to last value on exception
2025-07-03 15:53:36 +02:00
Mose Müller
8615bdeadc npm run build 2025-07-03 15:52:32 +02:00
Mose Müller
d24893a989 feat: reset frontend value to last value on exception
When changing a value in the frontend and this operation triggers an
exception in the backend, reset the frontend value to the last known
value.
2025-07-03 15:52:22 +02:00
Mose Müller
661603ef71 Merge pull request #252 from tiqi-group/alert-autofix-4
Potential fix for code scanning alert no. 4: Workflow does not contain permissions
2025-06-27 08:56:48 +02:00
Mose Müller
d6947b0f43 Potential fix for code scanning alert no. 4: Workflow does not contain permissions
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
2025-06-27 08:54:57 +02:00
13 changed files with 62 additions and 69 deletions

View File

@@ -2,6 +2,8 @@
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
name: Python package
permissions:
contents: read
on:
push:

View File

@@ -50,7 +50,7 @@ const createDisplayNameFromAccessPath = (fullAccessPath: string): string => {
function changeCallback(
value: SerializedObject,
callback: (ack: unknown) => void = () => {},
callback: (ack: undefined | SerializedObject) => void = () => {},
) {
updateValue(value, callback);
}

View File

@@ -38,7 +38,10 @@ interface NumberComponentProps {
isInstantUpdate: boolean;
unit?: string;
addNotification: (message: string, levelname?: LevelName) => void;
changeCallback?: (value: SerializedObject, callback?: (ack: unknown) => void) => void;
changeCallback?: (
value: SerializedObject,
callback?: (ack: undefined | SerializedObject) => void,
) => void;
displayName?: string;
id: string;
}
@@ -217,6 +220,15 @@ export const NumberComponent = React.memo((props: NumberComponentProps) => {
id,
} = props;
const handleChange = (newValue: SerializedObject) => {
changeCallback(newValue, (result: undefined | SerializedObject) => {
if (result === undefined) return;
if (result.type == "Exception") {
setInputString(value.toString());
}
});
};
// Create a state for the cursor position
const cursorPositionRef = useRef<number | null>(null);
@@ -319,7 +331,7 @@ export const NumberComponent = React.memo((props: NumberComponentProps) => {
};
}
changeCallback(serializedObject);
handleChange(serializedObject);
return;
} else {
console.debug(key);
@@ -350,7 +362,7 @@ export const NumberComponent = React.memo((props: NumberComponentProps) => {
};
}
changeCallback(serializedObject);
handleChange(serializedObject);
}
setInputString(newValue);
@@ -384,7 +396,7 @@ export const NumberComponent = React.memo((props: NumberComponentProps) => {
};
}
changeCallback(serializedObject);
handleChange(serializedObject);
}
};
useEffect(() => {

View File

@@ -19,7 +19,10 @@ interface SliderComponentProps {
stepSize: NumberObject;
isInstantUpdate: boolean;
addNotification: (message: string, levelname?: LevelName) => void;
changeCallback?: (value: SerializedObject, callback?: (ack: unknown) => void) => void;
changeCallback?: (
value: SerializedObject,
callback?: (ack: undefined | SerializedObject) => void,
) => void;
displayName: string;
id: string;
}

View File

@@ -28,7 +28,7 @@ export const socket = io(URL, {
export const updateValue = (
serializedObject: SerializedObject,
callback?: (ack: unknown) => void,
callback?: (ack: undefined | SerializedObject) => void,
) => {
if (callback) {
socket.emit(

View File

@@ -1,6 +1,6 @@
[project]
name = "pydase"
version = "0.10.18"
version = "0.10.21"
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 = [
{name = "Mose Müller",email = "mosemueller@gmail.com"}

View File

@@ -12,7 +12,6 @@ from pydase.observer_pattern.observable.observable import (
from pydase.utils.helpers import (
get_class_and_instance_attributes,
is_descriptor,
is_property_attribute,
)
from pydase.utils.serialization.serializer import (
Serializer,
@@ -28,9 +27,6 @@ class DataService(AbstractDataService):
self.__check_instance_classes()
def __setattr__(self, name: str, value: Any, /) -> None:
# Check and warn for unexpected type changes in attributes
self._warn_on_type_change(name, value)
# every class defined by the user should inherit from DataService if it is
# assigned to a public attribute
if not name.startswith("_") and not inspect.isfunction(value):
@@ -39,21 +35,6 @@ class DataService(AbstractDataService):
# Set the attribute
super().__setattr__(name, value)
def _warn_on_type_change(self, attr_name: str, new_value: Any) -> None:
if is_property_attribute(self, attr_name):
return
current_value = getattr(self, attr_name, None)
if self._is_unexpected_type_change(current_value, new_value):
logger.warning(
"Type of '%s' changed from '%s' to '%s'. This may have unwanted "
"side effects! Consider setting it to '%s' directly.",
attr_name,
type(current_value).__name__,
type(new_value).__name__,
type(current_value).__name__,
)
def _is_unexpected_type_change(self, current_value: Any, new_value: Any) -> bool:
return (
isinstance(current_value, float) and not isinstance(new_value, float)

View File

@@ -7,7 +7,7 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="theme-color" content="#000000" />
<meta name="description" content="Web site displaying a pydase UI." />
<script type="module" crossorigin src="/assets/index-XZbNXHJp.js"></script>
<script type="module" crossorigin src="/assets/index-CKS_bS2p.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-Cs09d5Pk.css">
</head>

View File

@@ -29,6 +29,7 @@ def get_property_dependencies(prop: property, prefix: str = "") -> list[str]:
class PropertyObserver(Observer):
def __init__(self, observable: Observable) -> None:
self.property_deps_dict: dict[str, list[str]] = {}
super().__init__(observable)
self._update_property_deps_dict()

View File

@@ -158,7 +158,7 @@ class Serializer:
"doc": None,
"readonly": True,
"type": "Exception",
"value": obj.args[0],
"value": obj.args[0] if len(obj.args) > 0 else "",
"name": obj.__class__.__name__,
}

View File

@@ -1,38 +1,13 @@
from enum import Enum
from typing import Any
import pydase
import pydase.units as u
import pytest
from pydase import DataService
from pydase.data_service.data_service_observer import DataServiceObserver
from pydase.data_service.state_manager import StateManager
from pydase.utils.decorators import FunctionDefinitionError, frontend
from pytest import LogCaptureFixture
def test_unexpected_type_change_warning(caplog: LogCaptureFixture) -> None:
class ServiceClass(DataService):
attr_1 = 1.0
current = 1.0 * u.units.A
service_instance = ServiceClass()
state_manager = StateManager(service_instance)
DataServiceObserver(state_manager)
service_instance.attr_1 = 2
assert "'attr_1' changed to '2'" in caplog.text
assert (
"Type of 'attr_1' changed from 'float' to 'int'. This may have unwanted "
"side effects! Consider setting it to 'float' directly." in caplog.text
)
service_instance.current = 2
assert "'current' changed to '2'" in caplog.text
assert (
"Type of 'current' changed from 'Quantity' to 'int'. This may have unwanted "
"side effects! Consider setting it to 'Quantity' directly." in caplog.text
)
import pydase
import pydase.units as u
from pydase import DataService
from pydase.utils.decorators import FunctionDefinitionError, frontend
def test_basic_inheritance_warning(caplog: LogCaptureFixture) -> None:

View File

@@ -1225,3 +1225,22 @@ def test_add_prefix_to_full_access_path(
serialized_obj: SerializedObject, prefix: str, expected: SerializedObject
) -> None:
assert add_prefix_to_full_access_path(serialized_obj, prefix) == expected
def test_serialize_exception() -> None:
assert dump(Exception()) == {
"doc": None,
"full_access_path": "",
"name": "Exception",
"readonly": True,
"type": "Exception",
"value": "",
}
assert dump(Exception("Exception message")) == {
"doc": None,
"full_access_path": "",
"name": "Exception",
"readonly": True,
"type": "Exception",
"value": "Exception message",
}