mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-06-10 23:28:49 +02:00
Compare commits
11 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d44eb69a92 | |||
| f4d51b6287 | |||
| c9fc0a82b9 | |||
| 668b1bd9cd | |||
| 1a6c8bf30f | |||
| c346bd0f18 | |||
| 5f86e41a03 | |||
| f7a48b5f6a | |||
| b4beb274da | |||
| 80694d151f | |||
| f03a5d9e85 |
@@ -1,6 +1,41 @@
|
||||
# CHANGELOG
|
||||
|
||||
|
||||
## v3.13.3 (2026-05-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **tests**: Rename description attribute to _description in FakeDevice
|
||||
([`668b1bd`](https://github.com/bec-project/bec_widgets/commit/668b1bd9cd158fc12cff2c340d7317f30a212121))
|
||||
|
||||
|
||||
## v3.13.2 (2026-05-22)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **tests**: Rename description attribute to _description in FakePositioner
|
||||
([`c346bd0`](https://github.com/bec-project/bec_widgets/commit/c346bd0f18ce873ff5ca6c59150c9581c9edca8d))
|
||||
|
||||
|
||||
## v3.13.1 (2026-05-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- Use .show instead of .start
|
||||
([`b4beb27`](https://github.com/bec-project/bec_widgets/commit/b4beb274da745da618f9b37ec241cd0109c088f1))
|
||||
|
||||
- **gui**: Replace window.show() with window.raise_window() and add hide() method
|
||||
([`f7a48b5`](https://github.com/bec-project/bec_widgets/commit/f7a48b5f6a51d391dca26ca42d03bad4f278ff22))
|
||||
|
||||
|
||||
## v3.13.0 (2026-05-21)
|
||||
|
||||
### Features
|
||||
|
||||
- **rpc-base**: Set default RPC timeout and allow customization
|
||||
([`f03a5d9`](https://github.com/bec-project/bec_widgets/commit/f03a5d9e853bd62b8ec1bad1c1e112fe01befe70))
|
||||
|
||||
|
||||
## v3.12.2 (2026-05-21)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -222,6 +222,7 @@ class BECGuiClient(RPCBase):
|
||||
self._ipython_registry: dict[str, RPCReference] = {}
|
||||
self.available_widgets = AvailableWidgetsNamespace()
|
||||
register_serializer_extension()
|
||||
self._rpc_timeout = 5
|
||||
|
||||
####################
|
||||
#### Client API ####
|
||||
@@ -232,6 +233,16 @@ class BECGuiClient(RPCBase):
|
||||
"""The launcher object."""
|
||||
return RPCBase(gui_id=f"{self._gui_id}:launcher", parent=self, object_name="launcher")
|
||||
|
||||
def set_rpc_timeout(self, timeout: float):
|
||||
"""Set the timeout for RPC calls to the GUI server.
|
||||
|
||||
Args:
|
||||
timeout(float): The timeout in seconds.
|
||||
"""
|
||||
if not isinstance(timeout, (int, float)) or timeout < 0:
|
||||
raise ValueError("Timeout must be a non-negative number.")
|
||||
self._rpc_timeout = timeout
|
||||
|
||||
def _safe_register_stream(self, endpoint: EndpointInfo, cb: Callable, **kwargs):
|
||||
"""Check if already registered for registration in idempotent functions."""
|
||||
if not self._client.connector.any_stream_is_registered(endpoint, cb=cb):
|
||||
@@ -358,7 +369,7 @@ class BECGuiClient(RPCBase):
|
||||
)
|
||||
|
||||
if not self._check_if_server_is_alive():
|
||||
self.start(wait=True)
|
||||
self.show(wait=True)
|
||||
if wait:
|
||||
with wait_for_server(self):
|
||||
return self._new_impl(
|
||||
@@ -550,7 +561,7 @@ class BECGuiClient(RPCBase):
|
||||
if self.launcher and len(self._top_level) == 0:
|
||||
self.launcher._run_rpc("show") # pylint: disable=protected-access
|
||||
for window in self._top_level.values():
|
||||
window.show()
|
||||
window.raise_window()
|
||||
|
||||
def _show_all(self):
|
||||
with wait_for_server(self):
|
||||
@@ -569,7 +580,7 @@ class BECGuiClient(RPCBase):
|
||||
if self.launcher and len(self._top_level) == 0:
|
||||
self.launcher._run_rpc("raise") # pylint: disable=protected-access
|
||||
for window in self._top_level.values():
|
||||
window._run_rpc("raise") # type: ignore[attr-defined]
|
||||
window.raise_window()
|
||||
|
||||
def _raise_all(self):
|
||||
with wait_for_server(self):
|
||||
|
||||
@@ -24,6 +24,8 @@ else:
|
||||
|
||||
# pylint: disable=protected-access
|
||||
|
||||
_DEFAULT_RPC_TIMEOUT = object()
|
||||
|
||||
|
||||
def _name_arg(arg):
|
||||
if isinstance(arg, DeviceBaseWithConfig):
|
||||
@@ -154,6 +156,7 @@ class RPCReference:
|
||||
|
||||
|
||||
class RPCBase:
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
gui_id: str | None = None,
|
||||
@@ -207,12 +210,16 @@ class RPCBase:
|
||||
# Use explicit call to ensure action name is 'raise' (not 'raise_')
|
||||
return self._run_rpc("raise")
|
||||
|
||||
def hide(self):
|
||||
"""Hide this widget (or its container)."""
|
||||
return self._run_rpc("hide")
|
||||
|
||||
def _run_rpc(
|
||||
self,
|
||||
method,
|
||||
*args,
|
||||
wait_for_rpc_response=True,
|
||||
timeout=5,
|
||||
wait_for_rpc_response: bool = True,
|
||||
timeout: float | None | object = _DEFAULT_RPC_TIMEOUT,
|
||||
gui_id: str | None = None,
|
||||
**kwargs,
|
||||
) -> Any:
|
||||
@@ -223,13 +230,16 @@ class RPCBase:
|
||||
method: The method to call.
|
||||
args: The arguments to pass to the method.
|
||||
wait_for_rpc_response: Whether to wait for the RPC response.
|
||||
timeout: The timeout for the RPC response.
|
||||
timeout: The timeout for the RPC response. If omitted, the client's default RPC
|
||||
timeout is used. If explicitly set to None, wait indefinitely.
|
||||
gui_id: The GUI ID to use for the RPC call. If None, the default GUI ID is used.
|
||||
kwargs: The keyword arguments to pass to the method.
|
||||
|
||||
Returns:
|
||||
The result of the RPC call.
|
||||
"""
|
||||
if timeout is _DEFAULT_RPC_TIMEOUT:
|
||||
timeout = self._root._rpc_timeout
|
||||
if method in ["show", "hide", "raise"] and gui_id is None:
|
||||
obj = self._root._server_registry.get(self._gui_id)
|
||||
if obj is None:
|
||||
|
||||
@@ -15,7 +15,7 @@ class FakeDevice(BECDevice):
|
||||
super().__init__(name=name)
|
||||
self._enabled = enabled
|
||||
self.signals = {self.name: {"value": 1.0}}
|
||||
self.description = {self.name: {"source": self.name, "dtype": "number", "shape": []}}
|
||||
self._description = {self.name: {"source": self.name, "dtype": "number", "shape": []}}
|
||||
self._readout_priority = readout_priority
|
||||
self._config = {
|
||||
"readoutPriority": "baseline",
|
||||
@@ -74,7 +74,7 @@ class FakeDevice(BECDevice):
|
||||
Returns:
|
||||
dict: Description of the device
|
||||
"""
|
||||
return self.description
|
||||
return self._description
|
||||
|
||||
|
||||
class FakePositioner(BECPositioner):
|
||||
@@ -96,7 +96,7 @@ class FakePositioner(BECPositioner):
|
||||
self._limits = limits
|
||||
self._readout_priority = readout_priority
|
||||
self.signals = {self.name: {"value": 1.0}}
|
||||
self.description = {self.name: {"source": self.name, "dtype": "number", "shape": []}}
|
||||
self._description = {self.name: {"source": self.name, "dtype": "number", "shape": []}}
|
||||
self._config = {
|
||||
"readoutPriority": "baseline",
|
||||
"deviceClass": "ophyd_devices.SimPositioner",
|
||||
@@ -176,7 +176,7 @@ class FakePositioner(BECPositioner):
|
||||
Returns:
|
||||
dict: Description of the device
|
||||
"""
|
||||
return self.description
|
||||
return self._description
|
||||
|
||||
@property
|
||||
def precision(self):
|
||||
|
||||
@@ -362,7 +362,7 @@ class BECWidget(BECConnector):
|
||||
"""Wrap the close even to ensure the rpc_register is cleaned up."""
|
||||
try:
|
||||
if not self._destroyed:
|
||||
self.cleanup()
|
||||
self._destroyed = True
|
||||
self.cleanup()
|
||||
finally:
|
||||
super().closeEvent(event) # pylint: disable=no-member
|
||||
|
||||
@@ -1465,6 +1465,16 @@ class DockAreaWidget(BECWidget, QWidget):
|
||||
for dock in self.dock_list():
|
||||
self._delete_dock(dock)
|
||||
|
||||
def cleanup(self):
|
||||
"""Tear down all docks via the Qt ADS API before the base BECWidget cleanup runs.
|
||||
|
||||
Explicitly releasing dock widgets through the CDockManager API first prevents crashes
|
||||
in PySide6 6.11.0 / PySide6-QtAds 4.5.x where the CDockManager destructor interacts
|
||||
badly with dock widgets that are deleted outside of it.
|
||||
"""
|
||||
self.delete_all()
|
||||
super().cleanup()
|
||||
|
||||
|
||||
if __name__ == "__main__": # pragma: no cover
|
||||
import sys
|
||||
|
||||
@@ -120,7 +120,12 @@ class BecConsoleRegistry:
|
||||
return None
|
||||
|
||||
window = console.window()
|
||||
if window is not None and window is not console and self._is_valid_qobject(window):
|
||||
if (
|
||||
window is not None
|
||||
and window is not console
|
||||
and self._is_valid_qobject(window)
|
||||
and not getattr(window, "_destroyed", False)
|
||||
):
|
||||
return window
|
||||
|
||||
if not avoid_console:
|
||||
|
||||
+7
-3
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "bec_widgets"
|
||||
version = "3.12.2"
|
||||
version = "3.13.3"
|
||||
description = "BEC Widgets"
|
||||
requires-python = ">=3.11"
|
||||
classifiers = [
|
||||
@@ -10,8 +10,8 @@ classifiers = [
|
||||
]
|
||||
dependencies = [
|
||||
"PyJWT~=2.9",
|
||||
"PySide6==6.9.0",
|
||||
"PySide6-QtAds==4.4.0",
|
||||
"PySide6==6.11.0",
|
||||
"PySide6-QtAds==4.5.0.4",
|
||||
"bec_ipython_client~=3.107,>=3.107.2", # needed for jupyter console
|
||||
"bec_lib~=3.107,>=3.107.2",
|
||||
"bec_qthemes~=1.0, >=1.3.4",
|
||||
@@ -67,6 +67,10 @@ qtermwidget = ["pyside6_qtermwidget"]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
@@ -45,7 +45,7 @@ def connected_client_gui_obj(qtbot, gui_id, bec_client_lib):
|
||||
"""
|
||||
gui = BECGuiClient(gui_id=gui_id)
|
||||
try:
|
||||
gui.start(wait=True)
|
||||
gui.show(wait=True)
|
||||
qtbot.waitUntil(lambda: hasattr(gui, "bec"), timeout=5000)
|
||||
gui.bec.delete_all() # ensure clean state
|
||||
qtbot.waitUntil(lambda: len(gui.bec.widget_list()) == 0, timeout=10000)
|
||||
|
||||
@@ -143,11 +143,11 @@ def test_rpc_gui_obj(connected_client_gui_obj: BECGuiClient, qtbot):
|
||||
qtbot.wait(500)
|
||||
gui.kill_server()
|
||||
assert not gui._gui_is_alive()
|
||||
gui.start(wait=True)
|
||||
gui.show(wait=True)
|
||||
assert gui._gui_is_alive()
|
||||
# calling start multiple times should not change anything
|
||||
gui.start(wait=True)
|
||||
gui.start(wait=True)
|
||||
# calling show multiple times should not change anything
|
||||
gui.show(wait=True)
|
||||
gui.show(wait=True)
|
||||
|
||||
def wait_for_gui_started():
|
||||
return "bec" in gui.windows
|
||||
|
||||
@@ -75,7 +75,7 @@ def connected_client_gui_obj(qtbot_scope_module, gui_id, bec_client_lib):
|
||||
"""
|
||||
gui = BECGuiClient(gui_id=gui_id)
|
||||
try:
|
||||
gui.start(wait=True)
|
||||
gui.show(wait=True)
|
||||
qtbot_scope_module.waitUntil(lambda: hasattr(gui, "bec"), timeout=5000)
|
||||
gui.bec.delete_all() # ensure clean state
|
||||
qtbot_scope_module.waitUntil(lambda: len(gui.bec.widget_list()) == 0, timeout=10000)
|
||||
|
||||
@@ -5,6 +5,7 @@ import pytest
|
||||
|
||||
from bec_widgets.cli.client import BECDockArea
|
||||
from bec_widgets.cli.client_utils import BECGuiClient, _start_plot_process
|
||||
from bec_widgets.cli.rpc.rpc_base import RPCBase, RPCResponseTimeoutError, rpc_timeout
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -220,7 +221,7 @@ def test_client_utils_new_starts_server_when_not_alive():
|
||||
with mock.patch("bec_widgets.cli.client_utils.wait_for_server", _no_wait_for_server):
|
||||
with (
|
||||
mock.patch.object(gui, "_check_if_server_is_alive", return_value=False),
|
||||
mock.patch.object(gui, "start") as mock_start,
|
||||
mock.patch.object(gui, "show") as mock_start,
|
||||
):
|
||||
gui.new(wait=False, startup_profile=None)
|
||||
|
||||
@@ -257,3 +258,11 @@ def test_client_utils_delete_falls_back_to_direct_close():
|
||||
gui.delete("dock")
|
||||
|
||||
widget._run_rpc.assert_called_once_with("close")
|
||||
|
||||
|
||||
def test_client_utils_gui_client_set_rpc_timeout():
|
||||
gui = BECGuiClient()
|
||||
assert gui._rpc_timeout == 5
|
||||
|
||||
gui.set_rpc_timeout(10)
|
||||
assert gui._rpc_timeout == 10
|
||||
|
||||
Reference in New Issue
Block a user