1
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2026-04-08 01:37:53 +02:00

Compare commits

..

24 Commits

Author SHA1 Message Date
semantic-release
7926969996 2.30.6
Automatically generated by python-semantic-release
2025-07-26 12:44:29 +00:00
61e5bde15f fix(waveform): autorange is applied with 150ms delay after curve is added 2025-07-26 14:43:51 +02:00
semantic-release
c8aa770de3 2.30.5
Automatically generated by python-semantic-release
2025-07-25 17:44:39 +00:00
4d5df9608a refactor(positioner-box): cleanup, accept float precision 2025-07-25 19:43:52 +02:00
b718b438ba fix(positioner-box): Test to fix handling of none integer values for precision 2025-07-25 19:43:52 +02:00
semantic-release
2f978c93c4 2.30.4
Automatically generated by python-semantic-release
2025-07-25 10:18:28 +00:00
b4e0664011 fix(cli): remove stderr from cli output when not using rpc 2025-07-25 12:17:44 +02:00
semantic-release
45fbf4015d 2.30.3
Automatically generated by python-semantic-release
2025-07-23 08:01:36 +00:00
David Perl
0d81bdd4dd fix: cleanup subscriptions in device browser 2025-07-23 10:00:43 +02:00
semantic-release
bb4c30ad80 2.30.2
Automatically generated by python-semantic-release
2025-07-23 06:57:35 +00:00
3fd09fceef test(test_plotting_framework_e2e): added test for waveform with passing device from dev container 2025-07-23 08:56:52 +02:00
8eb8225a7f fix: factor out device name function and add test 2025-07-23 08:56:52 +02:00
491d04467c fix(rpc_base): rpc_call wrapper passes full_name for Devices indeed of name 2025-07-23 08:56:52 +02:00
semantic-release
3bcff75107 2.30.1
Automatically generated by python-semantic-release
2025-07-22 18:19:10 +00:00
608590c542 fix: ignore KeyError in SignalLabel 2025-07-22 20:18:28 +02:00
semantic-release
012f7cf970 2.30.0
Automatically generated by python-semantic-release
2025-07-22 14:24:47 +00:00
cd17a4aad9 fix(signal_label): rewrite reading selection logic 2025-07-22 15:24:03 +01:00
f0dc992586 fix(signal_label): use read() instead of get() for init 2025-07-22 15:24:03 +01:00
fd1f9941e0 chore: update client.py 2025-07-22 15:24:03 +01:00
3384ca02bd fix(device_browser): display signal for signals 2025-07-22 15:24:03 +01:00
959cedbbd5 fix(signal_label): update signal from dialog correctly 2025-07-22 15:24:03 +01:00
ca4f97503b feat(signal_label): property to display array data or not 2025-07-22 15:24:03 +01:00
22beadcad0 fix(signal_label): show all signals by default 2025-07-22 15:24:03 +01:00
b9af36a4f1 fix(device_signal_display): don't read omitted 2025-07-22 15:24:03 +01:00
17 changed files with 431 additions and 68 deletions

View File

@@ -1,6 +1,100 @@
# CHANGELOG
## v2.30.6 (2025-07-26)
### Bug Fixes
- **waveform**: Autorange is applied with 150ms delay after curve is added
([`61e5bde`](https://github.com/bec-project/bec_widgets/commit/61e5bde15f0e1ebe185ddbe81cd71ad581ae6009))
## v2.30.5 (2025-07-25)
### Bug Fixes
- **positioner-box**: Test to fix handling of none integer values for precision
([`b718b43`](https://github.com/bec-project/bec_widgets/commit/b718b438bacff6eb6cd6015f1a67dcf75c05dce4))
### Refactoring
- **positioner-box**: Cleanup, accept float precision
([`4d5df96`](https://github.com/bec-project/bec_widgets/commit/4d5df9608a9438b9f6d7508c323eb3772e53f37d))
## v2.30.4 (2025-07-25)
### Bug Fixes
- **cli**: Remove stderr from cli output when not using rpc
([`b4e0664`](https://github.com/bec-project/bec_widgets/commit/b4e0664011682cae9966aa2632210a6b60e11714))
## v2.30.3 (2025-07-23)
### Bug Fixes
- Cleanup subscriptions in device browser
([`0d81bdd`](https://github.com/bec-project/bec_widgets/commit/0d81bdd4ddb4ec474a414b107cbc7fc865253934))
## v2.30.2 (2025-07-23)
### Bug Fixes
- Factor out device name function and add test
([`8eb8225`](https://github.com/bec-project/bec_widgets/commit/8eb8225a7f56014d6093aa142b3a5d071837982e))
- **rpc_base**: Rpc_call wrapper passes full_name for Devices indeed of name
([`491d044`](https://github.com/bec-project/bec_widgets/commit/491d04467c8ce4e116d61e614895d1dcc6b4b201))
### Testing
- **test_plotting_framework_e2e**: Added test for waveform with passing device from dev container
([`3fd09fc`](https://github.com/bec-project/bec_widgets/commit/3fd09fceef2ffa7e7c3eee20176304bafb00d0db))
## v2.30.1 (2025-07-22)
### Bug Fixes
- Ignore KeyError in SignalLabel
([`608590c`](https://github.com/bec-project/bec_widgets/commit/608590c5421368d5bba0e4b0f5187d90cac323be))
## v2.30.0 (2025-07-22)
### Bug Fixes
- **device_browser**: Display signal for signals
([`3384ca0`](https://github.com/bec-project/bec_widgets/commit/3384ca02bdb5a2798ad3339ecf3e2ba7c121e28f))
- **device_signal_display**: Don't read omitted
([`b9af36a`](https://github.com/bec-project/bec_widgets/commit/b9af36a4f1c91e910d4fc738b17b90e92287a7e3))
- **signal_label**: Rewrite reading selection logic
([`cd17a4a`](https://github.com/bec-project/bec_widgets/commit/cd17a4aad905296eb0460ecc27e5920f5c2e8fe5))
- **signal_label**: Show all signals by default
([`22beadc`](https://github.com/bec-project/bec_widgets/commit/22beadcad061b328c986414f30fef57b64bad693))
- **signal_label**: Update signal from dialog correctly
([`959cedb`](https://github.com/bec-project/bec_widgets/commit/959cedbbd5a123eef5f3370287bf6476c48caab9))
- **signal_label**: Use read() instead of get() for init
([`f0dc992`](https://github.com/bec-project/bec_widgets/commit/f0dc99258607a5cc8af51686d01f7fd54ae2779f))
### Chores
- Update client.py
([`fd1f994`](https://github.com/bec-project/bec_widgets/commit/fd1f9941e046b7ae1e247dde39c20bcbc37ac189))
### Features
- **signal_label**: Property to display array data or not
([`ca4f975`](https://github.com/bec-project/bec_widgets/commit/ca4f97503bf06363e8e8a5d494a9857223da4104))
## v2.29.0 (2025-07-22)
### Features

View File

@@ -4378,6 +4378,62 @@ class SignalLabel(RPCBase):
Show the button to select the signal to display
"""
@property
@rpc_call
def show_hinted_signals(self) -> "bool":
"""
In the signal selection menu, show hinted signals
"""
@show_hinted_signals.setter
@rpc_call
def show_hinted_signals(self) -> "bool":
"""
In the signal selection menu, show hinted signals
"""
@property
@rpc_call
def show_normal_signals(self) -> "bool":
"""
In the signal selection menu, show normal signals
"""
@show_normal_signals.setter
@rpc_call
def show_normal_signals(self) -> "bool":
"""
In the signal selection menu, show normal signals
"""
@property
@rpc_call
def show_config_signals(self) -> "bool":
"""
In the signal selection menu, show config signals
"""
@show_config_signals.setter
@rpc_call
def show_config_signals(self) -> "bool":
"""
In the signal selection menu, show config signals
"""
@property
@rpc_call
def display_array_data(self) -> "bool":
"""
Displays the full data from array signals if set to True.
"""
@display_array_data.setter
@rpc_call
def display_array_data(self) -> "bool":
"""
Displays the full data from array signals if set to True.
"""
class SignalLineEdit(RPCBase):
"""Line edit widget for device input with autocomplete for device names."""

View File

@@ -51,7 +51,7 @@ def _filter_output(output: str) -> str:
def _get_output(process, logger) -> None:
log_func = {process.stdout: logger.debug, process.stderr: logger.error}
log_func = {process.stdout: logger.debug, process.stderr: logger.info}
stream_buffer = {process.stdout: [], process.stderr: []}
try:
os.set_blocking(process.stdout.fileno(), False)

View File

@@ -7,6 +7,7 @@ from functools import wraps
from typing import TYPE_CHECKING, Any, cast
from bec_lib.client import BECClient
from bec_lib.device import DeviceBaseWithConfig
from bec_lib.endpoints import MessageEndpoints
from bec_lib.utils.import_utils import lazy_import, lazy_import_from
@@ -24,6 +25,20 @@ else:
# pylint: disable=protected-access
def _name_arg(arg):
if isinstance(arg, DeviceBaseWithConfig):
# if dev.<device> is passed to GUI, it passes full_name
if hasattr(arg, "full_name"):
return arg.full_name
elif hasattr(arg, "name"):
return arg.name
return arg
def _transform_args_kwargs(args, kwargs) -> tuple[tuple, dict]:
return tuple(_name_arg(arg) for arg in args), {k: _name_arg(v) for k, v in kwargs.items()}
def rpc_call(func):
"""
A decorator for calling a function on the server.
@@ -47,15 +62,7 @@ def rpc_call(func):
return None # func(*args, **kwargs)
caller_frame = caller_frame.f_back
out = []
for arg in args:
if hasattr(arg, "name"):
arg = arg.name
out.append(arg)
args = tuple(out)
for key, val in kwargs.items():
if hasattr(val, "name"):
kwargs[key] = val.name
args, kwargs = _transform_args_kwargs(args, kwargs)
if not self._root._gui_is_alive():
raise RuntimeError("GUI is not alive")
return self._run_rpc(func.__name__, *args, **kwargs)

View File

@@ -28,6 +28,10 @@ class EntryValidator:
if not available_entries:
available_entries = [name]
# edge case for if name is passed instead of full_name, should not happen
if entry in signals_dict:
entry = signals_dict[entry].get("obj_name", entry)
if entry is None or entry == "":
entry = next(iter(device._hints), name) if hasattr(device, "_hints") else name
if entry not in available_entries:

View File

@@ -138,7 +138,11 @@ class PositionerBoxBase(BECWidget, CompactPopupWidget):
signals = msg_content.get("signals", {})
# pylint: disable=protected-access
hinted_signals = self.dev[device]._hints
precision = self.dev[device].precision
precision = getattr(self.dev[device], "precision", 8)
try:
precision = int(precision)
except (TypeError, ValueError):
precision = int(8)
spinner = ui_components["spinner"]
position_indicator = ui_components["position_indicator"]
@@ -178,11 +182,13 @@ class PositionerBoxBase(BECWidget, CompactPopupWidget):
spinner.setVisible(False)
if readback_val is not None:
readback.setText(f"{readback_val:.{precision}f}")
text = f"{readback_val:.{precision}f}"
readback.setText(text)
position_emit(readback_val)
if setpoint_val is not None:
setpoint.setText(f"{setpoint_val:.{precision}f}")
text = f"{setpoint_val:.{precision}f}"
setpoint.setText(text)
limits = self.dev[device].limits
limit_update(limits)
@@ -205,10 +211,13 @@ class PositionerBoxBase(BECWidget, CompactPopupWidget):
ui["readback"].setToolTip(f"{device} readback")
ui["setpoint"].setToolTip(f"{device} setpoint")
ui["step_size"].setToolTip(f"Step size for {device}")
precision = self.dev[device].precision
if precision is not None:
ui["step_size"].setDecimals(precision)
ui["step_size"].setValue(10**-precision * 10)
precision = getattr(self.dev[device], "precision", 8)
try:
precision = int(precision)
except (TypeError, ValueError):
precision = int(8)
ui["step_size"].setDecimals(precision)
ui["step_size"].setValue(10**-precision * 10)
def _swap_readback_signal_connection(self, slot, old_device, new_device):
self.bec_dispatcher.disconnect_slot(slot, MessageEndpoints.device_readback(old_device))

View File

@@ -45,7 +45,12 @@ class PositionerGroupBox(QGroupBox):
def _on_position_update(self, pos: float):
self.position_update.emit(pos)
self.widget.label = f"%.{self.widget.dev[self.widget.device].precision}f" % pos
precision = getattr(self.widget.dev[self.widget.device], "precision", 8)
try:
precision = int(precision)
except (TypeError, ValueError):
precision = int(8)
self.widget.label = f"{pos:.{precision}f}"
def close(self):
self.widget.close()

View File

@@ -908,6 +908,10 @@ class Waveform(PlotBase):
self.roi_enable.emit(True) # Enable the ROI toolbar action
self.request_dap() # Request DAP update directly without blocking proxy
QTimer.singleShot(
150, self.auto_range
) # autorange with a delay to ensure the plot is updated
return curve
def _add_curve_object(self, name: str, config: CurveConfig) -> Curve:

View File

@@ -64,10 +64,10 @@ class DeviceBrowser(BECWidget, QWidget):
self.proxy_device_update = SignalProxy(
self.ui.filter_input.textChanged, rateLimit=500, slot=self.update_device_list
)
self.bec_dispatcher.client.callbacks.register(
self._device_update_callback_id = self.bec_dispatcher.client.callbacks.register(
EventType.DEVICE_UPDATE, self.on_device_update
)
self.bec_dispatcher.client.callbacks.register(
self._scan_status_callback_id = self.bec_dispatcher.client.callbacks.register(
EventType.SCAN_STATUS, self.scan_status_changed
)
self._default_config_dir = os.path.abspath(
@@ -229,6 +229,11 @@ class DeviceBrowser(BECWidget, QWidget):
if file_path:
self._config_helper.save_current_session(file_path)
def cleanup(self):
super().cleanup()
self.bec_dispatcher.client.callbacks.remove(self._scan_status_callback_id)
self.bec_dispatcher.client.callbacks.remove(self._device_update_callback_id)
if __name__ == "__main__": # pragma: no cover
import sys

View File

@@ -1,3 +1,4 @@
from bec_lib.device import Device
from bec_qthemes import material_icon
from qtpy.QtCore import Qt
from qtpy.QtWidgets import QHBoxLayout, QLabel, QToolButton, QVBoxLayout, QWidget
@@ -5,6 +6,7 @@ from qtpy.QtWidgets import QHBoxLayout, QLabel, QToolButton, QVBoxLayout, QWidge
from bec_widgets.utils.bec_connector import ConnectionConfig
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
from bec_widgets.utils.ophyd_kind_util import Kind
from bec_widgets.widgets.containers.dock.dock import BECDock
from bec_widgets.widgets.utility.signal_label.signal_label import SignalLabel
@@ -35,9 +37,9 @@ class SignalDisplay(BECWidget, QWidget):
@SafeSlot()
def _refresh(self):
if self.device in self.dev:
self.dev.get(self.device).read(cached=False)
self.dev.get(self.device).read_configuration(cached=False)
if (dev := self.dev.get(self.device)) is not None:
dev.read()
dev.read_configuration()
def _add_refresh_button(self):
button_holder = QWidget()
@@ -63,11 +65,26 @@ class SignalDisplay(BECWidget, QWidget):
self._add_refresh_button()
if self._device in self.dev:
for sig in self.dev[self.device]._info.get("signals", {}).keys():
if isinstance(self.dev[self.device], Device):
for sig, info in self.dev[self.device]._info.get("signals", {}).items():
if info.get("kind_str") in [
Kind.hinted.name,
Kind.normal.name,
Kind.config.name,
]:
self._content_layout.addWidget(
SignalLabel(
device=self._device,
signal=sig,
show_select_button=False,
show_default_units=True,
)
)
else:
self._content_layout.addWidget(
SignalLabel(
device=self._device,
signal=sig,
signal=self._device,
show_select_button=False,
show_default_units=True,
)

View File

@@ -2,7 +2,7 @@ from __future__ import annotations
import sys
import traceback
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Any
from bec_lib.device import Device, Signal
from bec_lib.endpoints import MessageEndpoints
@@ -143,6 +143,14 @@ class SignalLabel(BECWidget, QWidget):
"show_default_units.setter",
"show_select_button",
"show_select_button.setter",
"show_hinted_signals",
"show_hinted_signals.setter",
"show_normal_signals",
"show_normal_signals.setter",
"show_config_signals",
"show_config_signals.setter",
"display_array_data",
"display_array_data.setter",
]
def __init__(
@@ -183,8 +191,9 @@ class SignalLabel(BECWidget, QWidget):
self._dtype = None
self._show_hinted_signals: bool = True
self._show_normal_signals: bool = False
self._show_config_signals: bool = False
self._show_normal_signals: bool = True
self._show_config_signals: bool = True
self._display_array_data: bool = False
self._outer_layout = QHBoxLayout()
self._layout = QHBoxLayout()
@@ -197,7 +206,7 @@ class SignalLabel(BECWidget, QWidget):
self._update_label()
self._label.setLayout(self._layout)
self._value: str = ""
self._value: Any = ""
self._display = QLabel()
self._layout.addWidget(self._display)
@@ -210,6 +219,8 @@ class SignalLabel(BECWidget, QWidget):
self._select_button.clicked.connect(self.show_choice_dialog)
self.get_bec_shortcuts()
self._device_obj = self.dev.get(self._device)
self._signal_key, self._signal_info = "", {}
self._connected: bool = False
self.connect_device()
@@ -226,6 +237,7 @@ class SignalLabel(BECWidget, QWidget):
@SafeSlot()
def _process_dialog(self, device: str, signal: str):
signal = signal or device
self.disconnect_device()
self.device = device
self.signal = signal
@@ -241,45 +253,34 @@ class SignalLabel(BECWidget, QWidget):
def connect_device(self):
"""Subscribe to the Redis topic for the device to display"""
if not self._connected and self._device and self._device in self.dev:
self._connected = True
self._read_endpoint = MessageEndpoints.device_read(self._device)
self._signal_key, self._signal_info = self._signal_key_and_info()
self._manual_read()
self._read_endpoint = MessageEndpoints.device_readback(self._device)
self._read_config_endpoint = MessageEndpoints.device_read_configuration(self._device)
self.bec_dispatcher.connect_slot(self.on_device_readback, self._read_endpoint)
self.bec_dispatcher.connect_slot(self.on_device_readback, self._read_config_endpoint)
self._manual_read()
self._connected = True
self.set_display_value(self._value)
def disconnect_device(self):
"""Unsubscribe from the Redis topic for the device to display"""
if self._connected:
self._connected = False
self.bec_dispatcher.disconnect_slot(self.on_device_readback, self._read_endpoint)
self.bec_dispatcher.disconnect_slot(self.on_device_readback, self._read_config_endpoint)
self._connected = False
def _manual_read(self):
if self._device is None or not isinstance(
(device := self.dev.get(self._device)), Device | Signal
):
self._units = ""
self._value = "__"
if not isinstance(self._device_obj, Device | Signal):
self._value, self._units = "__", ""
return
signal, info = (
(
getattr(device, self.signal, None),
device._info.get("signals", {}).get(self._signal, {}).get("describe", {}),
)
if isinstance(device, Device)
else (device, device.describe().get(self._device))
)
if not isinstance(signal, Signal): # Avoid getting other attributes of device, e.g. methods
signal = None
if signal is None:
self._units = ""
self._value = "__"
reading = (self._device_obj.read() or {}) | (self._device_obj.read_configuration() or {})
value = reading.get(self._signal_key, {}).get("value")
if value is None:
self._value, self._units = "__", ""
return
self._value = signal.get()
self._units = info.get("egu", "")
self._dtype = info.get("dtype", "float")
self._value = value
self._units = self._signal_info.get("egu", "")
self._dtype = self._signal_info.get("dtype", "float")
@SafeSlot(dict, dict)
def on_device_readback(self, msg: dict, metadata: dict) -> None:
@@ -287,8 +288,7 @@ class SignalLabel(BECWidget, QWidget):
Update the display with the new value.
"""
try:
signal_to_read = self._patch_hinted_signal()
_value = msg["signals"].get(signal_to_read, {}).get("value")
_value = msg["signals"].get(self._signal_key, {}).get("value")
if _value is not None:
self._value = _value
self.set_display_value(self._value)
@@ -298,13 +298,19 @@ class SignalLabel(BECWidget, QWidget):
f"Error processing incoming reading: {msg}, handled with exception: {''.join(traceback.format_exception(e))}"
)
def _patch_hinted_signal(self):
if self.dev[self._device]._info["signals"] == {}:
return self._signal
signal_info = self.dev[self._device]._info["signals"][self._signal]
return (
signal_info["obj_name"] if signal_info["kind_str"] == Kind.hinted.name else self._signal
)
def _signal_key_and_info(self) -> tuple[str, dict]:
if isinstance(self._device_obj, Device):
try:
signal_info = self._device_obj._info["signals"][self._signal]
except KeyError:
return "", {}
if signal_info["kind_str"] == Kind.hinted.name:
return signal_info["obj_name"], signal_info
else:
return f"{self._device}_{self._signal}", signal_info
elif isinstance(self._device_obj, Signal):
return self._device, self._device_obj._info["describe_configuration"]
return "", {}
@SafeProperty(str)
def device(self) -> str:
@@ -315,6 +321,7 @@ class SignalLabel(BECWidget, QWidget):
def device(self, value: str) -> None:
self.disconnect_device()
self._device = value
self._device_obj = self.dev.get(self._device)
self._config.device = value
self.connect_device()
self._update_label()
@@ -382,6 +389,16 @@ class SignalLabel(BECWidget, QWidget):
self._decimal_places = value
self._update_label()
@SafeProperty(bool)
def display_array_data(self) -> bool:
"""Displays the full data from array signals if set to True."""
return self._display_array_data
@display_array_data.setter
def display_array_data(self, value: bool) -> None:
self._display_array_data = value
self.set_display_value(self._value)
@SafeProperty(bool)
def show_hinted_signals(self) -> bool:
"""In the signal selection menu, show hinted signals"""
@@ -409,7 +426,9 @@ class SignalLabel(BECWidget, QWidget):
def show_normal_signals(self, value: bool) -> None:
self._show_normal_signals = value
def _format_value(self, value: str):
def _format_value(self, value: Any):
if self._dtype == "array" and not self.display_array_data:
return "ARRAY DATA"
if self._decimal_places == 0:
return value
try:

View File

@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "bec_widgets"
version = "2.29.0"
version = "2.30.6"
description = "BEC Widgets"
requires-python = ">=3.10"
classifiers = [

View File

@@ -254,3 +254,35 @@ def test_dap_rpc(qtbot, bec_client_lib, connected_client_gui_obj):
res.wait()
qtbot.waitUntil(wait_for_fit, timeout=10000)
def test_waveform_passing_device(qtbot, bec_client_lib, connected_client_gui_obj):
gui = connected_client_gui_obj
client = bec_client_lib
dev = client.device_manager.devices
scans = client.scans
dock = gui.bec
wf = dock.new("wf_dock").new("Waveform")
c1 = wf.plot(
y_name=dev.samx, y_entry=dev.samx.setpoint
) # using setpoint to not use readback signal
assert c1.object_name == "samx_samx_setpoint"
status = scans.line_scan(dev.samx, -5, 5, steps=5, exp_time=0.05, relative=False)
status.wait()
# Wait for the scan to finish and the data to be available in history
# Wait until scan_id is in history
def _wait_for_scan_in_history():
if len(client.history) == 0:
return False
# Once items appear in storage, the last one hast to be the one we just scanned
return client.history[-1].metadata.bec["scan_id"] == status.scan.scan_id
qtbot.waitUntil(_wait_for_scan_in_history, timeout=10000)
last_scan_data = client.history[-1]
# check plotted data
x_data, y_data = c1.get_data()
assert np.array_equal(y_data, last_scan_data.devices.samx.samx_setpoint.read().get("value"))

View File

@@ -2,6 +2,7 @@ from typing import TYPE_CHECKING
from unittest import mock
import pytest
from bec_lib.device import Device
from qtpy.QtCore import QPoint, Qt
from qtpy.QtWidgets import QTabWidget
@@ -9,6 +10,9 @@ from bec_widgets.widgets.services.device_browser.device_browser import DeviceBro
from bec_widgets.widgets.services.device_browser.device_item.device_config_form import (
DeviceConfigForm,
)
from bec_widgets.widgets.services.device_browser.device_item.device_signal_display import (
SignalDisplay,
)
from .client_mocks import mocked_client
@@ -142,3 +146,39 @@ def test_device_deletion(device_browser, qtbot):
assert widget.device in device_browser._device_items
qtbot.mouseClick(widget.delete_button, Qt.LeftButton)
qtbot.waitUntil(lambda: widget.device not in device_browser._device_items, timeout=10000)
def test_signal_display(mocked_client, qtbot):
signal_display = SignalDisplay(client=mocked_client, device="test_device")
qtbot.addWidget(signal_display)
device_mock = mock.MagicMock()
signal_display.dev = {"test_device": device_mock}
signal_display._refresh()
device_mock.read.assert_called()
device_mock.read_configuration.assert_called()
def test_signal_display_no_device(mocked_client, qtbot):
device_mock = mock.MagicMock()
mocked_client.client.device_manager.devices = {"test_device_1": device_mock}
signal_display = SignalDisplay(client=mocked_client, device="test_device_2")
qtbot.addWidget(signal_display)
assert (
signal_display._content_layout.itemAt(1).widget().text()
== "Device test_device_2 not found in device manager!"
)
signal_display._refresh()
device_mock.read.assert_not_called()
device_mock.read_configuration.assert_not_called()
def test_signal_display_omitted_not_added(mocked_client, qtbot):
device_mock = mock.MagicMock(spec=Device)
device_mock._info = {"signals": {"signal_1": {"kind_str": "omitted"}}}
signal_display = SignalDisplay(client=mocked_client, device="test_device_1")
signal_display.dev = {"test_device_1": device_mock}
signal_display._populate()
qtbot.addWidget(signal_display)
assert signal_display._content_layout.itemAt(1).widget() is None

View File

@@ -7,6 +7,7 @@ from qtpy.QtCore import Qt, QTimer
from qtpy.QtGui import QValidator
from qtpy.QtWidgets import QPushButton
from bec_widgets.tests.utils import Positioner
from bec_widgets.widgets.control.device_control.positioner_box import (
PositionerBox,
PositionerControlLine,
@@ -19,6 +20,18 @@ from .client_mocks import mocked_client
from .conftest import create_widget
class PositionerWithoutPrecision(Positioner):
"""just placeholder for testing embedded isinstance check in DeviceCombobox"""
def __init__(self, precision, name="test", limits=None, read_value=1.0, enabled=True):
super().__init__(name, limits=limits, read_value=read_value, enabled=enabled)
self._precision = precision
@property
def precision(self):
return self._precision
@pytest.fixture
def positioner_box(qtbot, mocked_client):
"""Fixture for PositionerBox widget"""
@@ -165,3 +178,26 @@ def test_device_validity_check_rejects_non_positioner():
positioner_box = mock.MagicMock(spec=PositionerBox)
positioner_box.dev = {"test": 5.123}
assert not PositionerBox._check_device_is_valid(positioner_box, "test")
def test_positioner_box_device_without_precision(qtbot, positioner_box):
"""Test positioner box with device without precision"""
for ii, mock_return in enumerate([None, 2, 2.0, True, "tmp"]):
dev_name = f"samy_{ii}"
device = PositionerWithoutPrecision(
precision=mock_return, name=dev_name, limits=[-5, 5], read_value=3.0
)
positioner_box.bec_dispatcher.client.device_manager.add_devices(devices=[device])
positioner_box.device = dev_name
def check_title():
return positioner_box.ui.device_box.title() == dev_name
qtbot.waitUntil(check_title, timeout=3000)
if isinstance(mock_return, (int, float)):
mock_return = int(mock_return)
assert positioner_box.ui.step_size.value() == 10**-mock_return * 10
else:
assert positioner_box.ui.step_size.value() == 10**-8 * 10

View File

@@ -1,6 +1,14 @@
import pytest
from unittest.mock import MagicMock
from bec_widgets.cli.rpc.rpc_base import DeletedWidgetError, RPCBase, RPCReference
import pytest
from bec_lib.device import DeviceBaseWithConfig, Signal
from bec_widgets.cli.rpc.rpc_base import (
DeletedWidgetError,
RPCBase,
RPCReference,
_transform_args_kwargs,
)
@pytest.fixture
@@ -26,3 +34,20 @@ def test_rpc_base(rpc_base):
with pytest.raises(DeletedWidgetError):
ref._root # Object no longer referenced in registry
def test_transform_args_kwargs():
device_mock = MagicMock(spec=DeviceBaseWithConfig)
device_mock.full_name = "full name"
fallthrough_device_mock = MagicMock()
fallthrough_device_mock.name = "short name"
string_arg = "string_arg"
signal_mock = MagicMock(spec=Signal)
signal_mock.full_name = "full name"
args, kwargs = _transform_args_kwargs(
(device_mock, fallthrough_device_mock, string_arg, signal_mock),
{"a": device_mock, "b": fallthrough_device_mock, "c": string_arg, "d": signal_mock},
)
assert args == ("full name", "short name", "string_arg", "full name")
assert kwargs == {"a": "full name", "b": "short name", "c": "string_arg", "d": "full name"}

View File

@@ -84,7 +84,15 @@ def test_initialization(signal_label: SignalLabel):
def test_initialization_with_device(qtbot, mocked_client: MagicMock):
with patch.object(mocked_client.device_manager.devices.samx, "_info", SAMX_INFO_DICT):
with (
patch.object(mocked_client.device_manager.devices.samx, "_info", SAMX_INFO_DICT),
patch.object(
mocked_client.device_manager.devices.samx,
"_get_root_recursively",
lambda *_: (MagicMock(),),
),
):
widget = SignalLabel(device="samx", signal="readback", client=mocked_client)
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
@@ -188,6 +196,8 @@ def test_choice_dialog_with_no_client(qtbot):
def test_dialog_has_signals(signal_label: SignalLabel, qtbot):
signal_label.show_config_signals = False
signal_label.show_normal_signals = False
signal_label._process_dialog = MagicMock()
dialog = signal_label.show_choice_dialog()
qtbot.waitUntil(dialog.button_box.button(QDialogButtonBox.Ok).isVisible, timeout=500)