mirror of
https://github.com/tiqi-group/pydase.git
synced 2025-12-23 06:21:20 +01:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bc0c69f9e1 | ||
|
|
b2314f7e33 | ||
|
|
eb43e7b380 | ||
|
|
5dc28b0b55 | ||
|
|
c327215b5f | ||
|
|
04a3b225f8 | ||
|
|
86c4514e1a | ||
|
|
cac74e90db | ||
|
|
c24d63f4c0 | ||
|
|
b0dd5835a3 | ||
|
|
b0c8af0108 | ||
|
|
c0016673a8 | ||
|
|
eadc1df763 | ||
|
|
922fdf8fd0 | ||
|
|
8b21c42ef7 | ||
|
|
2399b3ca9f | ||
|
|
db43f5dbbb | ||
|
|
f2c0a94904 |
@@ -244,6 +244,14 @@ The full documentation provides more detailed information about `pydase`, includ
|
||||
|
||||
We welcome contributions! Please see [contributing.md](https://pydase.readthedocs.io/en/stable/about/contributing/) for details on how to contribute.
|
||||
|
||||
## Acknowledgements
|
||||
|
||||
This work was funded by the [ETH Zurich-PSI Quantum Computing Hub](https://www.psi.ch/en/lnq/qchub).
|
||||
|
||||
The main idea behind `pydase` is based on a previous project called `tiqi-plugin`, which
|
||||
was developed within the same research group. While the concept was inspired by that
|
||||
project, `pydase` was implemented from the ground up with a new architecture and design.
|
||||
|
||||
## License
|
||||
|
||||
`pydase` is licensed under the [MIT License][License].
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Form, InputGroup } from "react-bootstrap";
|
||||
import { DocStringComponent } from "./DocStringComponent";
|
||||
import "../App.css";
|
||||
@@ -175,6 +175,33 @@ const handleNumericKey = (
|
||||
return { value: newValue, selectionStart: selectionStart + 1 };
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates the new cursor position after moving left by a specified step size.
|
||||
*
|
||||
* @param cursorPosition - The current position of the cursor.
|
||||
* @param step - The number of positions to move left.
|
||||
* @returns The new cursor position, clamped to a minimum of 0.
|
||||
*/
|
||||
const getCursorLeftPosition = (cursorPosition: number, step: number): number => {
|
||||
return Math.max(0, cursorPosition - step);
|
||||
};
|
||||
|
||||
/**
|
||||
* Calculates the new cursor position after moving right by a specified step size.
|
||||
*
|
||||
* @param cursorPosition - The current position of the cursor.
|
||||
* @param step - The number of positions to move right.
|
||||
* @param maxPosition - The maximum allowed cursor position (e.g., value.length).
|
||||
* @returns The new cursor position, clamped to a maximum of maxPosition.
|
||||
*/
|
||||
const getCursorRightPosition = (
|
||||
cursorPosition: number,
|
||||
step: number,
|
||||
maxPosition: number,
|
||||
): number => {
|
||||
return Math.min(maxPosition, cursorPosition + step);
|
||||
};
|
||||
|
||||
export const NumberComponent = React.memo((props: NumberComponentProps) => {
|
||||
const {
|
||||
fullAccessPath,
|
||||
@@ -191,7 +218,8 @@ export const NumberComponent = React.memo((props: NumberComponentProps) => {
|
||||
} = props;
|
||||
|
||||
// Create a state for the cursor position
|
||||
const [cursorPosition, setCursorPosition] = useState<number | null>(null);
|
||||
const cursorPositionRef = useRef<number | null>(null);
|
||||
|
||||
// Create a state for the input string
|
||||
const [inputString, setInputString] = useState(value.toString());
|
||||
const renderCount = useRenderCount();
|
||||
@@ -200,26 +228,40 @@ export const NumberComponent = React.memo((props: NumberComponentProps) => {
|
||||
const { key, target } = event;
|
||||
|
||||
const inputTarget = target as HTMLInputElement;
|
||||
if (key === "F1" || key === "F5" || key === "F12" || key === "Tab") {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
|
||||
// Get the current input value and cursor position
|
||||
const { value } = inputTarget;
|
||||
const valueLength = value.length;
|
||||
const selectionEnd = inputTarget.selectionEnd ?? 0;
|
||||
let selectionStart = inputTarget.selectionStart ?? 0;
|
||||
|
||||
if (key === "F1" || key === "F5" || key === "F12" || key === "Tab") {
|
||||
return;
|
||||
} else if (key === "ArrowLeft" || key === "ArrowRight") {
|
||||
const hasSelection = selectionEnd > selectionStart;
|
||||
|
||||
if (hasSelection && !event.shiftKey) {
|
||||
// Collapse selection: ArrowLeft -> start, ArrowRight -> end
|
||||
const collapseTo = key === "ArrowLeft" ? selectionStart : selectionEnd;
|
||||
cursorPositionRef.current = collapseTo;
|
||||
} else {
|
||||
// No selection or shift key is pressed, just move cursor by one
|
||||
const newSelectionStart =
|
||||
key === "ArrowLeft"
|
||||
? getCursorLeftPosition(selectionStart, 1)
|
||||
: getCursorRightPosition(selectionEnd, 1, valueLength);
|
||||
|
||||
cursorPositionRef.current = newSelectionStart;
|
||||
}
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
|
||||
let newValue: string = value;
|
||||
if (event.ctrlKey && key === "a") {
|
||||
// Select everything when pressing Ctrl + a
|
||||
inputTarget.setSelectionRange(0, value.length);
|
||||
return;
|
||||
} else if (key === "ArrowRight" || key === "ArrowLeft") {
|
||||
// Move the cursor with the arrow keys and store its position
|
||||
selectionStart = key === "ArrowRight" ? selectionStart + 1 : selectionStart - 1;
|
||||
setCursorPosition(selectionStart);
|
||||
return;
|
||||
} else if ((key >= "0" && key <= "9") || key === "-") {
|
||||
// Check if a number key or a decimal point key is pressed
|
||||
({ value: newValue, selectionStart } = handleNumericKey(
|
||||
@@ -314,7 +356,7 @@ export const NumberComponent = React.memo((props: NumberComponentProps) => {
|
||||
setInputString(newValue);
|
||||
|
||||
// Save the current cursor position before the component re-renders
|
||||
setCursorPosition(selectionStart);
|
||||
cursorPositionRef.current = selectionStart;
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
@@ -367,8 +409,11 @@ export const NumberComponent = React.memo((props: NumberComponentProps) => {
|
||||
useEffect(() => {
|
||||
// Set the cursor position after the component re-renders
|
||||
const inputElement = document.getElementsByName(id)[0] as HTMLInputElement;
|
||||
if (inputElement && cursorPosition !== null) {
|
||||
inputElement.setSelectionRange(cursorPosition, cursorPosition);
|
||||
if (inputElement && cursorPositionRef.current !== null) {
|
||||
inputElement.setSelectionRange(
|
||||
cursorPositionRef.current,
|
||||
cursorPositionRef.current,
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -55,7 +55,7 @@ plugins:
|
||||
handlers:
|
||||
python:
|
||||
paths: [src] # search packages in the src folder
|
||||
import:
|
||||
inventories:
|
||||
- https://docs.python.org/3/objects.inv
|
||||
- https://docs.pydantic.dev/latest/objects.inv
|
||||
- https://confz.readthedocs.io/en/latest/objects.inv
|
||||
|
||||
1091
poetry.lock
generated
1091
poetry.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = "pydase"
|
||||
version = "0.10.10"
|
||||
version = "0.10.12"
|
||||
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"
|
||||
@@ -39,9 +39,9 @@ optional = true
|
||||
[tool.poetry.group.docs.dependencies]
|
||||
mkdocs-material = "^9.5.30"
|
||||
mkdocs-include-markdown-plugin = "^3.9.1"
|
||||
mkdocstrings = {extras = ["python"], version = "^0.25.2"}
|
||||
mkdocstrings = {extras = ["python"], version = "^0.29.0"}
|
||||
pymdown-extensions = "^10.1"
|
||||
mkdocs-swagger-ui-tag = "^0.6.10"
|
||||
mkdocs-swagger-ui-tag = "^0.7.0"
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -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-DpoEqi_N.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-BLJetjaQ.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DJzFvk4W.css">
|
||||
</head>
|
||||
|
||||
|
||||
@@ -55,6 +55,10 @@ class Observable(ObservableObject):
|
||||
value = super().__getattribute__(name)
|
||||
|
||||
if is_property_attribute(self, name):
|
||||
# fixes https://github.com/tiqi-group/pydase/issues/187 and
|
||||
# https://github.com/tiqi-group/pydase/issues/192
|
||||
if isinstance(value, ObservableObject):
|
||||
value.add_observer(self, name)
|
||||
self._notify_changed(name, value)
|
||||
|
||||
return value
|
||||
|
||||
@@ -87,8 +87,10 @@ class Server:
|
||||
service: The DataService instance that this server will manage.
|
||||
host: The host address for the server. Defaults to `'0.0.0.0'`, which means all
|
||||
available network interfaces.
|
||||
web_port: The port number for the web server. Defaults to
|
||||
[`ServiceConfig().web_port`][pydase.config.ServiceConfig.web_port].
|
||||
web_port: The port number for the web server. If set to None, it will use the
|
||||
port defined in
|
||||
[`ServiceConfig().web_port`][pydase.config.ServiceConfig.web_port]. Defaults
|
||||
to None.
|
||||
enable_web: Whether to enable the web server.
|
||||
filename: Filename of the file managing the service state persistence.
|
||||
additional_servers: A list of additional servers to run alongside the main
|
||||
@@ -140,7 +142,7 @@ class Server:
|
||||
self,
|
||||
service: DataService,
|
||||
host: str = "0.0.0.0",
|
||||
web_port: int = ServiceConfig().web_port,
|
||||
web_port: int | None = None,
|
||||
enable_web: bool = True,
|
||||
filename: str | Path | None = None,
|
||||
additional_servers: list[AdditionalServer] | None = None,
|
||||
@@ -151,7 +153,10 @@ class Server:
|
||||
additional_servers = []
|
||||
self._service = service
|
||||
self._host = host
|
||||
self._web_port = web_port
|
||||
if web_port is None:
|
||||
self._web_port = ServiceConfig().web_port
|
||||
else:
|
||||
self._web_port = web_port
|
||||
self._enable_web = enable_web
|
||||
self._kwargs = kwargs
|
||||
self._additional_servers = additional_servers
|
||||
|
||||
@@ -222,3 +222,22 @@ def test_nested_dict_property_changes(
|
||||
# Changing the _voltage attribute should re-evaluate the voltage property, but avoid
|
||||
# recursion
|
||||
service.my_dict["key"].voltage = 1.2
|
||||
|
||||
|
||||
def test_read_only_dict_property(caplog: pytest.LogCaptureFixture) -> None:
|
||||
class MyObservable(pydase.DataService):
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self._dict_attr = {"dotted.key": 1.0}
|
||||
|
||||
@property
|
||||
def dict_attr(self) -> dict[str, Any]:
|
||||
return self._dict_attr
|
||||
|
||||
service_instance = MyObservable()
|
||||
state_manager = StateManager(service=service_instance)
|
||||
DataServiceObserver(state_manager)
|
||||
|
||||
service_instance._dict_attr["dotted.key"] = 2.0
|
||||
|
||||
assert "'dict_attr[\"dotted.key\"]' changed to '2.0'" in caplog.text
|
||||
|
||||
Reference in New Issue
Block a user