fix(device_combobox): include signals with write_access filter; include in scan control

This commit is contained in:
2026-07-01 20:43:23 +02:00
committed by Jan Wyzula
parent 843765a60c
commit c0a1f8e1ff
5 changed files with 96 additions and 4 deletions
+7 -2
View File
@@ -17,6 +17,7 @@ class FakeDevice(BECDevice):
enabled=True,
readout_priority=ReadoutPriority.MONITORED,
signal_class: str | None = None,
write_access: bool = False,
):
super().__init__(name=name)
self._enabled = enabled
@@ -35,6 +36,8 @@ class FakeDevice(BECDevice):
if signal_class is None:
signal_class = "AsyncSignal" if readout_priority == ReadoutPriority.ASYNC else "Signal"
self._info = {
"read_access": True,
"write_access": write_access,
"signals": {
self.name: {
"kind_str": "hinted",
@@ -42,7 +45,7 @@ class FakeDevice(BECDevice):
"obj_name": self.name,
"signal_class": signal_class,
}
}
},
}
@property
@@ -115,6 +118,8 @@ class FakePositioner(BECPositioner):
"name": self.name,
}
self._info = {
"read_access": None,
"write_access": None, # positioner containers report no write access flag
"signals": {
"readback": {
"kind_str": "hinted",
@@ -131,7 +136,7 @@ class FakePositioner(BECPositioner):
"component_name": "velocity",
"obj_name": f"{self.name}_velocity",
}, # config
}
},
}
self.signals = {
self.name: {"value": self.read_value},
@@ -42,6 +42,8 @@ class DeviceInputConfig(ConnectionConfig):
signal_class_filter: Signal class names used to restrict listed devices.
autocomplete: Whether to use the explicit completer model instead of Qt's default
editable-combobox completer.
include_signals_with_write_access: Whether to additionally list signals that report
write access, in addition to the devices selected by the other filters.
"""
device_filter: list[str] = Field(default_factory=list)
@@ -52,6 +54,7 @@ class DeviceInputConfig(ConnectionConfig):
apply_filter: bool = True
signal_class_filter: list[str] = Field(default_factory=list)
autocomplete: bool = False
include_signals_with_write_access: bool = False
@field_validator("device_filter")
@classmethod
@@ -115,6 +118,8 @@ class DeviceComboBox(BECWidget, QComboBox):
signal_class_filter: Signal class names used to restrict listed devices.
autocomplete: If True, use the explicit line-edit style completer. If False, keep
Qt's default editable-combobox completion behavior.
include_signals_with_write_access: If True, additionally list signals that report
write access on top of the devices selected by the other filters.
**kwargs: Additional keyword arguments passed to ``BECWidget``.
"""
@@ -146,6 +151,7 @@ class DeviceComboBox(BECWidget, QComboBox):
arg_name: str | None = None,
signal_class_filter: list[str] | None = None,
autocomplete: bool | None = None,
include_signals_with_write_access: bool | None = None,
**kwargs,
):
self.config = self._process_config(config)
@@ -190,6 +196,8 @@ class DeviceComboBox(BECWidget, QComboBox):
self.config.autocomplete = autocomplete
if self.config.autocomplete:
self.autocomplete = True
if include_signals_with_write_access is not None:
self.config.include_signals_with_write_access = include_signals_with_write_access
if available_devices is not None:
self.set_available_devices(available_devices)
@@ -261,10 +269,18 @@ class DeviceComboBox(BECWidget, QComboBox):
if not self.apply_filter:
return
devices = self._filter_devices_by_signal_class(self.dev.enabled_devices)
enabled_devices = self.dev.enabled_devices
devices = self._filter_devices_by_signal_class(enabled_devices)
devices = [device for device in devices if self._check_device_filter(device)]
devices = [device for device in devices if self._check_readout_filter(device)]
self.devices = [device.name for device in devices]
device_names = [device.name for device in devices]
if self.include_signals_with_write_access:
seen = set(device_names)
for name in self._signals_with_write_access(enabled_devices):
if name not in seen:
device_names.append(name)
seen.add(name)
self.devices = device_names
@SafeSlot(list)
def set_available_devices(self, devices: list[str]):
@@ -317,6 +333,16 @@ class DeviceComboBox(BECWidget, QComboBox):
self.config.signal_class_filter = value or []
self.update_devices_from_filters()
@SafeProperty(bool)
def include_signals_with_write_access(self) -> bool:
"""Whether signals with write access are listed in addition to the other filters."""
return self.config.include_signals_with_write_access
@include_signals_with_write_access.setter
def include_signals_with_write_access(self, value: bool):
self.config.include_signals_with_write_access = value
self.update_devices_from_filters()
@SafeProperty(bool)
def filter_to_device(self):
"""Include generic Device objects."""
@@ -602,6 +628,18 @@ class DeviceComboBox(BECWidget, QComboBox):
) -> bool:
return device.readout_priority in self.readout_filter
@staticmethod
def _signals_with_write_access(
devices: list[Device | BECSignal | ComputedSignal | Positioner],
) -> list[str]:
"""Return the names of devices that report write access in their device info."""
names = []
for device in devices:
device_info = getattr(device, "_info", None) or {}
if device_info.get("write_access"):
names.append(device.name)
return names
def _update_validity_style(self, is_valid: bool) -> None:
if is_valid or not self.isEnabled():
self.setStyleSheet("")
@@ -663,10 +701,12 @@ if __name__ == "__main__": # pragma: no cover
filter_positioner = QCheckBox("Positioner")
filter_signal = QCheckBox("Signal")
filter_computed = QCheckBox("ComputedSignal")
filter_write_access = QCheckBox("Include write-access signals")
controls.addWidget(filter_device)
controls.addWidget(filter_positioner)
controls.addWidget(filter_signal)
controls.addWidget(filter_computed)
controls.addWidget(filter_write_access)
combo = DeviceComboBox()
combo.set_first_element_as_empty = True
@@ -679,12 +719,14 @@ if __name__ == "__main__": # pragma: no cover
combo.filter_to_positioner = filter_positioner.isChecked()
combo.filter_to_signal = filter_signal.isChecked()
combo.filter_to_computed_signal = filter_computed.isChecked()
combo.include_signals_with_write_access = filter_write_access.isChecked()
class_input.textChanged.connect(_apply_filters)
filter_device.toggled.connect(_apply_filters)
filter_positioner.toggled.connect(_apply_filters)
filter_signal.toggled.connect(_apply_filters)
filter_computed.toggled.connect(_apply_filters)
filter_write_access.toggled.connect(_apply_filters)
_apply_filters()
widget.show()
@@ -287,6 +287,7 @@ class ScanGroupBox(QGroupBox):
arg_name=arg_name,
default=default,
device_filter=BECDeviceFilter.DEVICE,
include_signals_with_write_access=True,
autocomplete=True,
)
else:
@@ -3,6 +3,7 @@ from unittest import mock
import pytest
from bec_lib.device import ReadoutPriority
from bec_widgets.tests.utils import FakeDevice
from bec_widgets.widgets.control.device_input.device_combobox.device_combobox import (
BECDeviceFilter,
DeviceComboBox,
@@ -156,6 +157,48 @@ def test_device_input_combobox_cleanup_clears_callback_before_unregister(qtbot,
remove_mock.assert_called_once_with(callback_id)
def test_device_input_combobox_include_signals_with_write_access(qtbot, mocked_client):
settable_signal = FakeDevice("settable_signal", write_access=True)
mocked_client.device_manager.add_devices([settable_signal])
widget = DeviceComboBox(client=mocked_client, device_filter=BECDeviceFilter.POSITIONER)
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
assert widget.include_signals_with_write_access is False
assert "samx" in widget.devices
assert "settable_signal" not in widget.devices
assert "bpm4i" not in widget.devices
widget.include_signals_with_write_access = True
assert widget.config.include_signals_with_write_access is True
assert "samx" in widget.devices
assert "settable_signal" in widget.devices
assert "bpm4i" not in widget.devices
widget.include_signals_with_write_access = False
assert "settable_signal" not in widget.devices
def test_device_input_combobox_include_signals_with_write_access_via_kwarg(qtbot, mocked_client):
settable_signal = FakeDevice("settable_signal", write_access=True)
mocked_client.device_manager.add_devices([settable_signal])
widget = DeviceComboBox(
client=mocked_client,
device_filter=BECDeviceFilter.POSITIONER,
include_signals_with_write_access=True,
)
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
assert widget.include_signals_with_write_access is True
assert "samx" in widget.devices
assert "settable_signal" in widget.devices
def test_get_device_from_input_combobox_init(device_input_combobox):
device_input_combobox.setCurrentIndex(0)
device_text = device_input_combobox.currentText()
+1
View File
@@ -662,6 +662,7 @@ def test_on_scan_selected(scan_control, scan_name):
if isinstance(widget, DeviceComboBox):
assert widget.currentText() == ""
assert widget.autocomplete is True
assert widget.include_signals_with_write_access is True
assert "samx" in widget.devices
assert (
"async_device" in widget.devices