1
0
mirror of https://github.com/bec-project/bec_widgets.git synced 2026-05-10 00:32:10 +02:00

Compare commits

..

4 Commits

14 changed files with 549 additions and 260 deletions
+171
View File
@@ -0,0 +1,171 @@
# This file was automatically generated by generate_cli.py
# type: ignore
from __future__ import annotations
# pylint: skip-file
designer_plugins = {
"AbortButton": ("bec_widgets.widgets.control.buttons.button_abort.button_abort", "AbortButton"),
"BECColorMapWidget": (
"bec_widgets.widgets.utility.visual.colormap_widget.colormap_widget",
"BECColorMapWidget",
),
"BECMainWindow": ("bec_widgets.widgets.containers.main_window.main_window", "BECMainWindow"),
"BECProgressBar": (
"bec_widgets.widgets.progress.bec_progressbar.bec_progressbar",
"BECProgressBar",
),
"BECQueue": ("bec_widgets.widgets.services.bec_queue.bec_queue", "BECQueue"),
"BECShell": ("bec_widgets.widgets.editors.bec_console.bec_console", "BECShell"),
"BECSpinBox": ("bec_widgets.widgets.utility.spinbox.decimal_spinbox", "BECSpinBox"),
"BECStatusBox": ("bec_widgets.widgets.services.bec_status_box.bec_status_box", "BECStatusBox"),
"BecConsole": ("bec_widgets.widgets.editors.bec_console.bec_console", "BecConsole"),
"ColorButton": ("bec_widgets.widgets.utility.visual.color_button.color_button", "ColorButton"),
"ColorButtonNative": (
"bec_widgets.widgets.utility.visual.color_button_native.color_button_native",
"ColorButtonNative",
),
"ColormapSelector": (
"bec_widgets.widgets.utility.visual.colormap_selector.colormap_selector",
"ColormapSelector",
),
"DapComboBox": ("bec_widgets.widgets.dap.dap_combo_box.dap_combo_box", "DapComboBox"),
"DarkModeButton": (
"bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button",
"DarkModeButton",
),
"DeviceBrowser": (
"bec_widgets.widgets.services.device_browser.device_browser",
"DeviceBrowser",
),
"DeviceComboBox": (
"bec_widgets.widgets.control.device_input.device_combobox.device_combobox",
"DeviceComboBox",
),
"DeviceLineEdit": (
"bec_widgets.widgets.control.device_input.device_line_edit.device_line_edit",
"DeviceLineEdit",
),
"Heatmap": ("bec_widgets.widgets.plots.heatmap.heatmap", "Heatmap"),
"IDEExplorer": ("bec_widgets.widgets.utility.ide_explorer.ide_explorer", "IDEExplorer"),
"Image": ("bec_widgets.widgets.plots.image.image", "Image"),
"LMFitDialog": ("bec_widgets.widgets.dap.lmfit_dialog.lmfit_dialog", "LMFitDialog"),
"LogPanel": ("bec_widgets.widgets.utility.logpanel.logpanel", "LogPanel"),
"Minesweeper": ("bec_widgets.widgets.games.minesweeper", "Minesweeper"),
"MonacoWidget": ("bec_widgets.widgets.editors.monaco.monaco_widget", "MonacoWidget"),
"MotorMap": ("bec_widgets.widgets.plots.motor_map.motor_map", "MotorMap"),
"MultiWaveform": ("bec_widgets.widgets.plots.multi_waveform.multi_waveform", "MultiWaveform"),
"PdfViewerWidget": ("bec_widgets.widgets.utility.pdf_viewer.pdf_viewer", "PdfViewerWidget"),
"PositionIndicator": (
"bec_widgets.widgets.control.device_control.position_indicator.position_indicator",
"PositionIndicator",
),
"PositionerBox": (
"bec_widgets.widgets.control.device_control.positioner_box.positioner_box.positioner_box",
"PositionerBox",
),
"PositionerBox2D": (
"bec_widgets.widgets.control.device_control.positioner_box.positioner_box_2d.positioner_box_2d",
"PositionerBox2D",
),
"PositionerControlLine": (
"bec_widgets.widgets.control.device_control.positioner_box.positioner_control_line.positioner_control_line",
"PositionerControlLine",
),
"PositionerGroup": (
"bec_widgets.widgets.control.device_control.positioner_group.positioner_group",
"PositionerGroup",
),
"ResetButton": ("bec_widgets.widgets.control.buttons.button_reset.button_reset", "ResetButton"),
"ResumeButton": (
"bec_widgets.widgets.control.buttons.button_resume.button_resume",
"ResumeButton",
),
"RingProgressBar": (
"bec_widgets.widgets.progress.ring_progress_bar.ring_progress_bar",
"RingProgressBar",
),
"SBBMonitor": ("bec_widgets.widgets.editors.sbb_monitor.sbb_monitor", "SBBMonitor"),
"ScanControl": ("bec_widgets.widgets.control.scan_control.scan_control", "ScanControl"),
"ScanMetadata": ("bec_widgets.widgets.editors.scan_metadata.scan_metadata", "ScanMetadata"),
"ScanProgressBar": (
"bec_widgets.widgets.progress.scan_progressbar.scan_progressbar",
"ScanProgressBar",
),
"ScatterWaveform": (
"bec_widgets.widgets.plots.scatter_waveform.scatter_waveform",
"ScatterWaveform",
),
"SignalComboBox": (
"bec_widgets.widgets.control.device_input.signal_combobox.signal_combobox",
"SignalComboBox",
),
"SignalLabel": ("bec_widgets.widgets.utility.signal_label.signal_label", "SignalLabel"),
"SignalLineEdit": (
"bec_widgets.widgets.control.device_input.signal_line_edit.signal_line_edit",
"SignalLineEdit",
),
"SpinnerWidget": ("bec_widgets.widgets.utility.spinner.spinner", "SpinnerWidget"),
"StopButton": ("bec_widgets.widgets.control.buttons.stop_button.stop_button", "StopButton"),
"TextBox": ("bec_widgets.widgets.editors.text_box.text_box", "TextBox"),
"ToggleSwitch": ("bec_widgets.widgets.utility.toggle.toggle", "ToggleSwitch"),
"Waveform": ("bec_widgets.widgets.plots.waveform.waveform", "Waveform"),
"WebsiteWidget": ("bec_widgets.widgets.editors.website.website", "WebsiteWidget"),
"WidgetFinderComboBox": (
"bec_widgets.widgets.utility.widget_finder.widget_finder",
"WidgetFinderComboBox",
),
}
widget_icons = {
"AbortButton": "cancel",
"BECColorMapWidget": "palette",
"BECMainWindow": "widgets",
"BECProgressBar": "page_control",
"BECQueue": "edit_note",
"BECShell": "hub",
"BECSpinBox": "123",
"BECStatusBox": "widgets",
"BecConsole": "terminal",
"ColorButton": "colors",
"ColorButtonNative": "colors",
"ColormapSelector": "palette",
"DapComboBox": "data_exploration",
"DarkModeButton": "dark_mode",
"DeviceBrowser": "lists",
"DeviceComboBox": "list_alt",
"DeviceLineEdit": "edit_note",
"Heatmap": "dataset",
"IDEExplorer": "widgets",
"Image": "image",
"LMFitDialog": "monitoring",
"LogPanel": "browse_activity",
"Minesweeper": "videogame_asset",
"MonacoWidget": "code",
"MotorMap": "my_location",
"MultiWaveform": "ssid_chart",
"PdfViewerWidget": "picture_as_pdf",
"PositionIndicator": "horizontal_distribute",
"PositionerBox": "switch_right",
"PositionerBox2D": "switch_right",
"PositionerControlLine": "switch_left",
"PositionerGroup": "grid_view",
"ResetButton": "restart_alt",
"ResumeButton": "resume",
"RingProgressBar": "track_changes",
"SBBMonitor": "train",
"ScanControl": "tune",
"ScanMetadata": "list_alt",
"ScanProgressBar": "timelapse",
"ScatterWaveform": "scatter_plot",
"SignalComboBox": "list_alt",
"SignalLabel": "scoreboard",
"SignalLineEdit": "vital_signs",
"SpinnerWidget": "progress_activity",
"StopButton": "dangerous",
"TextBox": "chat",
"ToggleSwitch": "toggle_on",
"Waveform": "show_chart",
"WebsiteWidget": "travel_explore",
"WidgetFinderComboBox": "frame_inspect",
}
@@ -206,7 +206,6 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
def _populate_registry_widgets(self): def _populate_registry_widgets(self):
try: try:
widget_handler.update_available_widgets()
items = sorted(widget_handler.widget_classes.keys()) items = sorted(widget_handler.widget_classes.keys())
except Exception as exc: except Exception as exc:
print(f"Failed to load registered widgets: {exc}") print(f"Failed to load registered widgets: {exc}")
@@ -335,20 +334,13 @@ class JupyterConsoleWindow(QWidget): # pragma: no cover:
If kwargs does not contain `object_name`, it will default to the provided shortcut. If kwargs does not contain `object_name`, it will default to the provided shortcut.
""" """
# Ensure registry is loaded
widget_handler.update_available_widgets()
cls = widget_handler.widget_classes.get(widget_type)
if cls is None:
raise ValueError(f"Unknown registered widget type: {widget_type}")
if kwargs is None: if kwargs is None:
kwargs = {"object_name": shortcut} kwargs = {"object_name": shortcut}
else: else:
kwargs = dict(kwargs) kwargs = dict(kwargs)
kwargs.setdefault("object_name", shortcut) kwargs.setdefault("object_name", shortcut)
# Instantiate and add widget = widget_handler.create_widget(widget_type, **kwargs)
widget = cls(**kwargs)
if not isinstance(widget, QWidget): if not isinstance(widget, QWidget):
raise TypeError( raise TypeError(
f"Instantiated object for type '{widget_type}' is not a QWidget: {type(widget)}" f"Instantiated object for type '{widget_type}' is not a QWidget: {type(widget)}"
+56 -4
View File
@@ -4,6 +4,7 @@ import importlib.metadata
import inspect import inspect
import pkgutil import pkgutil
import traceback import traceback
from functools import lru_cache
from importlib import util as importlib_util from importlib import util as importlib_util
from importlib.machinery import FileFinder, ModuleSpec, SourceFileLoader from importlib.machinery import FileFinder, ModuleSpec, SourceFileLoader
from types import ModuleType from types import ModuleType
@@ -11,7 +12,11 @@ from typing import Generator
from bec_lib.logger import bec_logger from bec_lib.logger import bec_logger
from bec_widgets.utils.plugin_utils import BECClassContainer, BECClassInfo from bec_widgets.utils.plugin_utils import (
BECClassContainer,
BECClassInfo,
rpc_widget_registry_from_source,
)
logger = bec_logger.logger logger = bec_logger.logger
@@ -53,6 +58,14 @@ def _submodule_by_name(module: ModuleType, name: str):
return None return None
def _submodule_spec_by_name(module: ModuleType, name: str) -> ModuleSpec | None:
for module_info in pkgutil.iter_modules(module.__path__):
if module_info.name != name or not isinstance(module_info.module_finder, FileFinder):
continue
return module_info.module_finder.find_spec(module_info.name)
return None
def _get_widgets_from_module(module: ModuleType) -> BECClassContainer: def _get_widgets_from_module(module: ModuleType) -> BECClassContainer:
"""Find any BECWidget subclasses in the given module and return them with their info.""" """Find any BECWidget subclasses in the given module and return them with their info."""
from bec_widgets.utils.bec_widget import BECWidget # avoid circular import from bec_widgets.utils.bec_widget import BECWidget # avoid circular import
@@ -90,16 +103,55 @@ def get_plugin_client_module() -> ModuleType | None:
return _submodule_by_name(plugin, "client") if (plugin := user_widget_plugin()) else None return _submodule_by_name(plugin, "client") if (plugin := user_widget_plugin()) else None
def get_plugin_designer_module() -> ModuleType | None:
"""If there is a plugin repository installed, return the designer module."""
return (
_submodule_by_name(plugin, "designer_plugins") if (plugin := user_widget_plugin()) else None
)
@lru_cache
def get_plugin_rpc_widget_registry() -> dict[str, tuple[str, str]]:
"""If there is a plugin repository installed, return the RPC widget registry."""
plugin = user_widget_plugin()
if plugin is None:
return {}
client_spec = _submodule_spec_by_name(plugin, "client")
if client_spec is not None and client_spec.origin:
try:
return rpc_widget_registry_from_source(client_spec.origin)
except (OSError, SyntaxError) as exc:
logger.warning(f"Could not parse plugin RPC widget registry: {exc}")
client_module = get_plugin_client_module()
if client_module is None:
return {}
registry = {}
for plugin_name, plugin_class in inspect.getmembers(client_module, inspect.isclass):
if hasattr(plugin_class, "_IMPORT_MODULE"):
registry[plugin_name] = (plugin_class._IMPORT_MODULE, plugin_class.__name__)
return registry
@lru_cache
def get_plugin_designer_registry() -> dict[str, tuple[str, str]]:
"""If there is a plugin repository installed, return the designer plugin registry."""
designer_module = get_plugin_designer_module()
if designer_module and hasattr(designer_module, "designer_plugins"):
return designer_module.designer_plugins
return {}
def get_all_plugin_widgets() -> BECClassContainer: def get_all_plugin_widgets() -> BECClassContainer:
"""If there is a plugin repository installed, load all widgets from it.""" """If there is a plugin repository installed, load all widgets from it."""
if plugin := user_widget_plugin(): if plugin := user_widget_plugin():
return _all_widgets_from_all_submods(plugin) return _all_widgets_from_all_submods(plugin)
else: return BECClassContainer()
return BECClassContainer()
if __name__ == "__main__": # pragma: no cover if __name__ == "__main__": # pragma: no cover
widgets = get_plugin_rpc_widget_registry()
client = get_plugin_client_module() client = get_plugin_client_module()
print(get_all_plugin_widgets()) print(get_all_plugin_widgets())
... ...
+69 -2
View File
@@ -14,7 +14,11 @@ import isort
from bec_lib.logger import bec_logger from bec_lib.logger import bec_logger
from qtpy.QtCore import Property as QtProperty from qtpy.QtCore import Property as QtProperty
from bec_widgets.utils.generate_designer_plugin import DesignerPluginGenerator, plugin_filenames from bec_widgets.utils.generate_designer_plugin import (
DesignerPluginGenerator,
DesignerPluginInfo,
plugin_filenames,
)
from bec_widgets.utils.plugin_utils import BECClassContainer, get_custom_classes from bec_widgets.utils.plugin_utils import BECClassContainer, get_custom_classes
logger = bec_logger.logger logger = bec_logger.logger
@@ -250,6 +254,58 @@ class {class_name}(RPCBase):\n"""
file.write(formatted_content) file.write(formatted_content)
def write_designer_plugins(plugin_infos: list[DesignerPluginInfo], file_name: str):
"""
Write a registry of Qt widget classes with designer plugins.
Args:
plugin_infos(list[DesignerPluginInfo]): The designer plugin metadata to write.
file_name(str): The name of the file to write to.
"""
plugin_infos = sorted(plugin_infos, key=lambda info: info.plugin_name_pascal)
content = """# This file was automatically generated by generate_cli.py
# type: ignore
from __future__ import annotations
# pylint: skip-file
designer_plugins = {
"""
for info in plugin_infos:
widget_module = info.plugin_class.__module__
widget_class = info.plugin_name_pascal
content += f' "{info.plugin_name_pascal}": ("{widget_module}", "{widget_class}"),\n'
content += """
}
widget_icons = {
"""
for info in plugin_infos:
content += f' "{info.plugin_name_pascal}": "{info.icon_name}",\n'
content += """
}
"""
try:
formatted_content = black.format_str(content, mode=black.Mode(line_length=100))
except black.NothingChanged:
formatted_content = content
config = isort.Config(
profile="black",
line_length=100,
multi_line_output=3,
include_trailing_comma=False,
known_first_party=["bec_widgets"],
)
formatted_content = isort.code(formatted_content, config=config)
with open(file_name, "w", encoding="utf-8") as file:
file.write(formatted_content)
def main(): def main():
""" """
Main entry point for the script, controlled by command line arguments. Main entry point for the script, controlled by command line arguments.
@@ -303,6 +359,8 @@ def main():
else: else:
non_overwrite_classes = [] non_overwrite_classes = []
designer_plugin_infos = []
for cls in rpc_classes.plugins: for cls in rpc_classes.plugins:
logger.info(f"Writing bec-designer plugin files for {cls.__name__}...") logger.info(f"Writing bec-designer plugin files for {cls.__name__}...")
@@ -310,21 +368,30 @@ def main():
logger.error( logger.error(
f"Not writing plugin files for {cls.__name__} because a built-in widget with that name exists" f"Not writing plugin files for {cls.__name__} because a built-in widget with that name exists"
) )
continue
plugin = DesignerPluginGenerator(cls) plugin = DesignerPluginGenerator(cls)
if not hasattr(plugin, "info"): if not hasattr(plugin, "info") or plugin.excluded:
continue continue
def _exists(file: str): def _exists(file: str):
return os.path.exists(os.path.join(plugin.info.base_path, file)) return os.path.exists(os.path.join(plugin.info.base_path, file))
if any(_exists(file) for file in plugin_filenames(plugin.info.plugin_name_snake)): if any(_exists(file) for file in plugin_filenames(plugin.info.plugin_name_snake)):
if _exists(plugin.filenames.plugin):
designer_plugin_infos.append(plugin.info)
logger.debug( logger.debug(
f"Skipping generation of extra plugin files for {plugin.info.plugin_name_snake} - at least one file out of 'plugin.py', 'pyproject', and 'register_{plugin.info.plugin_name_snake}.py' already exists." f"Skipping generation of extra plugin files for {plugin.info.plugin_name_snake} - at least one file out of 'plugin.py', 'pyproject', and 'register_{plugin.info.plugin_name_snake}.py' already exists."
) )
continue continue
plugin.run() plugin.run()
designer_plugin_infos.append(plugin.info)
# Write designer_plugins.py with plugin import metadata for all widgets with designer plugins.
designer_plugins_path = module_dir / client_subdir / "designer_plugins.py"
logger.info(f"Generating designer plugin registry at {designer_plugins_path}")
write_designer_plugins(designer_plugin_infos, str(designer_plugins_path))
if __name__ == "__main__": # pragma: no cover if __name__ == "__main__": # pragma: no cover
@@ -29,6 +29,7 @@ class DesignerPluginInfo:
self.plugin_name_pascal = plugin_class.__name__ self.plugin_name_pascal = plugin_class.__name__
self.plugin_name_snake = pascal_to_snake(self.plugin_name_pascal) self.plugin_name_snake = pascal_to_snake(self.plugin_name_pascal)
self.widget_import = f"from {plugin_class.__module__} import {self.plugin_name_pascal}" self.widget_import = f"from {plugin_class.__module__} import {self.plugin_name_pascal}"
self.icon_name = getattr(plugin_class, "ICON_NAME", "")
plugin_module = ( plugin_module = (
".".join(plugin_class.__module__.split(".")[:-1]) + f".{self.plugin_name_snake}_plugin" ".".join(plugin_class.__module__.split(".")[:-1]) + f".{self.plugin_name_snake}_plugin"
) )
@@ -63,6 +64,10 @@ class DesignerPluginGenerator:
def filenames(self): def filenames(self):
return plugin_filenames(self.info.plugin_name_snake) return plugin_filenames(self.info.plugin_name_snake)
@property
def excluded(self):
return self._excluded
def run(self, validate=True): def run(self, validate=True):
if self._excluded: if self._excluded:
print(f"Plugin {self.widget.__name__} is excluded from generation.") print(f"Plugin {self.widget.__name__} is excluded from generation.")
+110 -50
View File
@@ -1,56 +1,22 @@
from __future__ import annotations from __future__ import annotations
import ast
import importlib import importlib
import inspect import inspect
import os import os
from dataclasses import dataclass from dataclasses import dataclass
from functools import lru_cache
from pathlib import Path
from typing import TYPE_CHECKING, Iterable from typing import TYPE_CHECKING, Iterable
from bec_lib.plugin_helper import _get_available_plugins
from qtpy.QtWidgets import QWidget
from bec_widgets.utils.bec_connector import BECConnector
from bec_widgets.utils.bec_widget import BECWidget
if TYPE_CHECKING: # pragma: no cover if TYPE_CHECKING: # pragma: no cover
from qtpy.QtWidgets import QWidget
from bec_widgets.utils.bec_connector import BECConnector
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.widgets.containers.auto_update.auto_updates import AutoUpdates from bec_widgets.widgets.containers.auto_update.auto_updates import AutoUpdates
def get_plugin_widgets() -> dict[str, BECConnector]:
"""
Get all available widgets from the plugin directory. Widgets are classes that inherit from BECConnector.
The plugins are provided through python plugins and specified in the respective pyproject.toml file using
the following key:
[project.entry-points."bec.widgets.user_widgets"]
plugin_widgets = "path.to.plugin.module"
e.g.
[project.entry-points."bec.widgets.user_widgets"]
plugin_widgets = "pxiii_bec.bec_widgets.widgets"
assuming that the widgets module for the package pxiii_bec is located at pxiii_bec/bec_widgets/widgets and
contains the widgets to be loaded within the pxiii_bec/bec_widgets/widgets/__init__.py file.
Returns:
dict[str, BECConnector]: A dictionary of widget names and their respective classes.
"""
modules = _get_available_plugins("bec.widgets.user_widgets")
loaded_plugins = {}
print(modules)
for module in modules:
mods = inspect.getmembers(module, predicate=_filter_plugins)
for name, mod_cls in mods:
if name in loaded_plugins:
print(f"Duplicated widgets plugin {name}.")
loaded_plugins[name] = mod_cls
return loaded_plugins
def _filter_plugins(obj):
return inspect.isclass(obj) and issubclass(obj, BECConnector)
def get_plugin_auto_updates() -> dict[str, type[AutoUpdates]]: def get_plugin_auto_updates() -> dict[str, type[AutoUpdates]]:
""" """
Get all available auto update classes from the plugin directory. AutoUpdates must inherit from AutoUpdate and be Get all available auto update classes from the plugin directory. AutoUpdates must inherit from AutoUpdate and be
@@ -66,6 +32,8 @@ def get_plugin_auto_updates() -> dict[str, type[AutoUpdates]]:
Returns: Returns:
dict[str, AutoUpdates]: A dictionary of widget names and their respective classes. dict[str, AutoUpdates]: A dictionary of widget names and their respective classes.
""" """
from bec_lib.plugin_helper import _get_available_plugins
modules = _get_available_plugins("bec.widgets.auto_updates") modules = _get_available_plugins("bec.widgets.auto_updates")
loaded_plugins = {} loaded_plugins = {}
for module in modules: for module in modules:
@@ -168,6 +136,11 @@ class BECClassContainer:
def _collect_classes_from_package(repo_name: str, package: str) -> BECClassContainer: def _collect_classes_from_package(repo_name: str, package: str) -> BECClassContainer:
"""Collect classes from a package subtree (for example ``widgets`` or ``applications``).""" """Collect classes from a package subtree (for example ``widgets`` or ``applications``)."""
from qtpy.QtWidgets import QWidget
from bec_widgets.utils.bec_connector import BECConnector
from bec_widgets.utils.bec_widget import BECWidget
collection = BECClassContainer() collection = BECClassContainer()
try: try:
anchor_module = importlib.import_module(f"{repo_name}.{package}") anchor_module = importlib.import_module(f"{repo_name}.{package}")
@@ -194,17 +167,18 @@ def _collect_classes_from_package(repo_name: str, package: str) -> BECClassConta
for name in dir(module): for name in dir(module):
obj = getattr(module, name) obj = getattr(module, name)
if not isinstance(obj, type):
continue
if not hasattr(obj, "__module__") or obj.__module__ != module.__name__: if not hasattr(obj, "__module__") or obj.__module__ != module.__name__:
continue continue
if isinstance(obj, type): class_info = BECClassInfo(name=name, module=module.__name__, file=path, obj=obj)
class_info = BECClassInfo(name=name, module=module.__name__, file=path, obj=obj) if issubclass(obj, BECConnector):
if issubclass(obj, BECConnector): class_info.is_connector = True
class_info.is_connector = True if issubclass(obj, QWidget) or issubclass(obj, BECWidget):
if issubclass(obj, QWidget) or issubclass(obj, BECWidget): class_info.is_widget = True
class_info.is_widget = True if hasattr(obj, "PLUGIN") and obj.PLUGIN:
if hasattr(obj, "PLUGIN") and obj.PLUGIN: class_info.is_plugin = True
class_info.is_plugin = True collection.add_class(class_info)
collection.add_class(class_info)
return collection return collection
@@ -229,3 +203,89 @@ def get_custom_classes(
for package in selected_packages: for package in selected_packages:
collection += _collect_classes_from_package(repo_name, package) collection += _collect_classes_from_package(repo_name, package)
return collection return collection
def _get_designer_registry() -> dict[str, tuple[str, str]]:
from bec_widgets.cli.designer_plugins import designer_plugins
return designer_plugins
def _resolve_widget_from_registry(import_path: str, widget_name: str) -> type[QWidget]:
widget = importlib.import_module(import_path)
return getattr(widget, widget_name)
def designer_plugin_exists(name: str) -> bool:
from bec_widgets.utils.bec_plugin_helper import get_plugin_designer_registry
internal_registry = _get_designer_registry()
external_registry = get_plugin_designer_registry()
return name in internal_registry or name in external_registry
def get_designer_plugin(name: str, raise_on_missing: bool = True) -> type[QWidget] | None:
from bec_widgets.utils.bec_plugin_helper import get_plugin_designer_registry
internal_registry = _get_designer_registry()
external_registry = get_plugin_designer_registry()
if name in external_registry:
import_path, widget_name = external_registry[name]
return _resolve_widget_from_registry(import_path, widget_name)
if name in internal_registry:
import_path, widget_name = internal_registry[name]
return _resolve_widget_from_registry(import_path, widget_name)
if raise_on_missing:
raise ValueError(
f"Designer plugin {name} not found in either internal or external registry."
)
return None
def rpc_widget_registry_from_source(path: str | Path) -> dict[str, tuple[str, str]]:
"""Parse a generated RPC client module and return its widget registry."""
source_path = Path(path)
module_node = ast.parse(source_path.read_text(encoding="utf-8"), filename=str(source_path))
registry = {}
for node in module_node.body:
if not isinstance(node, ast.ClassDef):
continue
for item in node.body:
if not isinstance(item, ast.Assign):
continue
if not any(
isinstance(target, ast.Name) and target.id == "_IMPORT_MODULE"
for target in item.targets
):
continue
if isinstance(item.value, ast.Constant) and isinstance(item.value.value, str):
registry[node.name] = (item.value.value, node.name)
break
return registry
@lru_cache
def get_rpc_widget_registry() -> dict[str, tuple[str, str]]:
client_path = Path(__file__).resolve().parents[1] / "cli" / "client.py"
return rpc_widget_registry_from_source(client_path)
@lru_cache
def rpc_widget_registry() -> dict[str, tuple[str, str]]:
from bec_widgets.utils.bec_plugin_helper import get_plugin_rpc_widget_registry
internal_registry = get_rpc_widget_registry()
external_registry = get_plugin_rpc_widget_registry()
return {**external_registry, **internal_registry}
def get_rpc_widget(name: str, raise_on_missing: bool = True) -> type[QWidget] | None:
registry = rpc_widget_registry()
if name in registry:
import_path, widget_name = registry[name]
return _resolve_widget_from_registry(import_path, widget_name)
if raise_on_missing:
raise ValueError(f"RPC widget {name} not found in registry.")
return None
+17 -25
View File
@@ -1,42 +1,34 @@
from __future__ import annotations from __future__ import annotations
from bec_widgets.cli.client_utils import IGNORE_WIDGETS from typing import TYPE_CHECKING
from bec_widgets.utils.bec_plugin_helper import get_all_plugin_widgets
from bec_widgets.utils.bec_widget import BECWidget from bec_widgets.utils.plugin_utils import get_rpc_widget, rpc_widget_registry
from bec_widgets.utils.plugin_utils import get_custom_classes
if TYPE_CHECKING: # pragma: no cover
from bec_widgets.utils.bec_widget import BECWidget
class RPCWidgetHandler: class RPCWidgetHandler:
"""Handler class for creating widgets from RPC messages.""" """Handler class for creating widgets from RPC messages."""
def __init__(self): def __init__(self):
self._widget_classes = None self._widget_registry = None
@property @property
def widget_classes(self) -> dict[str, type[BECWidget]]: def widget_classes(self) -> dict[str, tuple[str, str]]:
""" """
Get the available widget classes. Get the available widget classes.
Returns: Returns:
dict: The available widget classes. dict: The available widget classes.
""" """
if self._widget_classes is None: registry = rpc_widget_registry()
self.update_available_widgets() if not registry:
return self._widget_classes # type: ignore return {}
return registry
def update_available_widgets(self): @staticmethod
""" def create_widget(widget_type, **kwargs) -> BECWidget:
Update the available widgets.
Returns:
None
"""
self._widget_classes = (
get_custom_classes("bec_widgets", packages=("widgets", "applications"))
+ get_all_plugin_widgets()
).as_dict(IGNORE_WIDGETS)
def create_widget(self, widget_type, **kwargs) -> BECWidget:
""" """
Create a widget from an RPC message. Create a widget from an RPC message.
@@ -48,9 +40,9 @@ class RPCWidgetHandler:
Returns: Returns:
widget(BECWidget): The created widget. widget(BECWidget): The created widget.
""" """
widget_class = self.widget_classes.get(widget_type) # type: ignore widget = get_rpc_widget(widget_type, raise_on_missing=False)
if widget_class: if widget:
return widget_class(**kwargs) return widget(**kwargs)
raise ValueError(f"Unknown widget type: {widget_type}") raise ValueError(f"Unknown widget type: {widget_type}")
+9 -85
View File
@@ -1,10 +1,8 @@
from bec_lib.logger import bec_logger from bec_lib.logger import bec_logger
from qtpy import PYQT6, PYSIDE6 from qtpy import PYSIDE6
from qtpy.QtCore import QFile, QIODevice from qtpy.QtCore import QFile, QIODevice
from bec_widgets.utils.bec_plugin_helper import get_all_plugin_widgets from bec_widgets.utils.plugin_utils import get_designer_plugin
from bec_widgets.utils.generate_designer_plugin import DesignerPluginInfo
from bec_widgets.utils.plugin_utils import get_custom_classes
logger = bec_logger.logger logger = bec_logger.logger
@@ -12,16 +10,14 @@ if PYSIDE6:
from qtpy.QtUiTools import QUiLoader from qtpy.QtUiTools import QUiLoader
class CustomUiLoader(QUiLoader): class CustomUiLoader(QUiLoader):
def __init__(self, baseinstance, custom_widgets: dict | None = None): def __init__(self, baseinstance):
super().__init__(baseinstance) super().__init__(baseinstance)
self.custom_widgets = custom_widgets or {}
self.baseinstance = baseinstance self.baseinstance = baseinstance
def createWidget(self, class_name, parent=None, name=""): def createWidget(self, class_name, parent=None, name=""):
if class_name in self.custom_widgets: widget = get_designer_plugin(class_name, raise_on_missing=False)
widget = self.custom_widgets[class_name](self.baseinstance) if widget is not None:
return widget return widget(self.baseinstance)
return super().createWidget(class_name, self.baseinstance, name) return super().createWidget(class_name, self.baseinstance, name)
@@ -31,16 +27,9 @@ class UILoader:
def __init__(self, parent=None): def __init__(self, parent=None):
self.parent = parent self.parent = parent
self.custom_widgets = ( if not PYSIDE6:
get_custom_classes("bec_widgets") + get_all_plugin_widgets()
).as_dict()
if PYSIDE6:
self.loader = self.load_ui_pyside6
elif PYQT6:
self.loader = self.load_ui_pyqt6
else:
raise ImportError("No compatible Qt bindings found.") raise ImportError("No compatible Qt bindings found.")
self.loader = self.load_ui_pyside6
def load_ui_pyside6(self, ui_file, parent=None): def load_ui_pyside6(self, ui_file, parent=None):
""" """
@@ -53,7 +42,7 @@ class UILoader:
QWidget: The loaded widget. QWidget: The loaded widget.
""" """
parent = parent or self.parent parent = parent or self.parent
loader = CustomUiLoader(parent, self.custom_widgets) loader = CustomUiLoader(parent)
file = QFile(ui_file) file = QFile(ui_file)
if not file.open(QIODevice.ReadOnly): if not file.open(QIODevice.ReadOnly):
raise IOError(f"Cannot open file: {ui_file}") raise IOError(f"Cannot open file: {ui_file}")
@@ -61,71 +50,6 @@ class UILoader:
file.close() file.close()
return widget return widget
def load_ui_pyqt6(self, ui_file, parent=None):
"""
Specific loader for PyQt6 using loadUi.
Args:
ui_file(str): Path to the .ui file.
parent(QWidget): Parent widget.
Returns:
QWidget: The loaded widget.
"""
from PyQt6.uic.Loader.loader import DynamicUILoader
class CustomDynamicUILoader(DynamicUILoader):
def __init__(self, package, custom_widgets: dict = None):
super().__init__(package)
self.custom_widgets = custom_widgets or {}
def _handle_custom_widgets(self, el):
"""Handle the <customwidgets> element."""
def header2module(header):
"""header2module(header) -> string
Convert paths to C++ header files to according Python modules
>>> header2module("foo/bar/baz.h")
'foo.bar.baz'
"""
if header.endswith(".h"):
header = header[:-2]
mpath = []
for part in header.split("/"):
# Ignore any empty parts or those that refer to the current
# directory.
if part not in ("", "."):
if part == "..":
# We should allow this for Python3.
raise SyntaxError(
"custom widget header file name may not contain '..'."
)
mpath.append(part)
return ".".join(mpath)
for custom_widget in el:
classname = custom_widget.findtext("class")
header = custom_widget.findtext("header")
if header:
header = self._translate_bec_widgets_header(header)
self.factory.addCustomWidget(
classname,
custom_widget.findtext("extends") or "QWidget",
header2module(header),
)
def _translate_bec_widgets_header(self, header):
for name, value in self.custom_widgets.items():
if header == DesignerPluginInfo.pascal_to_snake(name):
return value.__module__
return header
return CustomDynamicUILoader("", self.custom_widgets).loadUi(ui_file, parent)
def load_ui(self, ui_file, parent=None): def load_ui(self, ui_file, parent=None):
""" """
Universal UI loader method. Universal UI loader method.
@@ -19,8 +19,7 @@ from qtpy.QtWidgets import (
import bec_widgets.widgets.containers.qt_ads as QtAds import bec_widgets.widgets.containers.qt_ads as QtAds
from bec_widgets import BECWidget, SafeProperty, SafeSlot from bec_widgets import BECWidget, SafeProperty, SafeSlot
from bec_widgets.applications.views.view import ViewTourSteps from bec_widgets.cli.designer_plugins import widget_icons
from bec_widgets.utils.bec_dispatcher import BECDispatcher
from bec_widgets.utils.colors import apply_theme from bec_widgets.utils.colors import apply_theme
from bec_widgets.utils.rpc_decorator import rpc_timeout from bec_widgets.utils.rpc_decorator import rpc_timeout
from bec_widgets.utils.rpc_widget_handler import widget_handler from bec_widgets.utils.rpc_widget_handler import widget_handler
@@ -65,22 +64,7 @@ from bec_widgets.widgets.containers.dock_area.toolbar_components.workspace_actio
WorkspaceConnection, WorkspaceConnection,
workspace_bundle, workspace_bundle,
) )
from bec_widgets.widgets.containers.main_window.main_window import BECMainWindowNoRPC
from bec_widgets.widgets.containers.qt_ads import CDockWidget from bec_widgets.widgets.containers.qt_ads import CDockWidget
from bec_widgets.widgets.control.device_control.positioner_box import PositionerBox, PositionerBox2D
from bec_widgets.widgets.control.scan_control import ScanControl
from bec_widgets.widgets.editors.bec_console.bec_console import BecConsole, BECShell
from bec_widgets.widgets.plots.heatmap.heatmap import Heatmap
from bec_widgets.widgets.plots.image.image import Image
from bec_widgets.widgets.plots.motor_map.motor_map import MotorMap
from bec_widgets.widgets.plots.multi_waveform.multi_waveform import MultiWaveform
from bec_widgets.widgets.plots.scatter_waveform.scatter_waveform import ScatterWaveform
from bec_widgets.widgets.plots.waveform.waveform import Waveform
from bec_widgets.widgets.progress.ring_progress_bar.ring_progress_bar import RingProgressBar
from bec_widgets.widgets.services.bec_queue.bec_queue import BECQueue
from bec_widgets.widgets.services.bec_status_box.bec_status_box import BECStatusBox
from bec_widgets.widgets.utility.logpanel.logpanel import LogPanel
from bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button import DarkModeButton
logger = bec_logger.logger logger = bec_logger.logger
@@ -144,6 +128,10 @@ class BECDockArea(DockAreaWidget):
self._mode = mode self._mode = mode
# Toolbar # Toolbar
from bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button import (
DarkModeButton,
)
self.dark_mode_button = DarkModeButton(parent=self, toolbar=True) self.dark_mode_button = DarkModeButton(parent=self, toolbar=True)
self.dark_mode_button.setVisible(enable_profile_management) self.dark_mode_button.setVisible(enable_profile_management)
self._setup_toolbar() self._setup_toolbar()
@@ -342,39 +330,42 @@ class BECDockArea(DockAreaWidget):
self.toolbar = ModularToolBar(parent=self) self.toolbar = ModularToolBar(parent=self)
plot_actions = { plot_actions = {
"waveform": (Waveform.ICON_NAME, "Add Waveform", "Waveform"), "waveform": (widget_icons["Waveform"], "Add Waveform", "Waveform"),
"scatter_waveform": ( "scatter_waveform": (
ScatterWaveform.ICON_NAME, widget_icons["ScatterWaveform"],
"Add Scatter Waveform", "Add Scatter Waveform",
"ScatterWaveform", "ScatterWaveform",
), ),
"multi_waveform": (MultiWaveform.ICON_NAME, "Add Multi Waveform", "MultiWaveform"), "multi_waveform": (
"image": (Image.ICON_NAME, "Add Image", "Image"), widget_icons["MultiWaveform"],
"motor_map": (MotorMap.ICON_NAME, "Add Motor Map", "MotorMap"), "Add Multi Waveform",
"heatmap": (Heatmap.ICON_NAME, "Add Heatmap", "Heatmap"), "MultiWaveform",
),
"image": (widget_icons["Image"], "Add Image", "Image"),
"motor_map": (widget_icons["MotorMap"], "Add Motor Map", "MotorMap"),
"heatmap": (widget_icons["Heatmap"], "Add Heatmap", "Heatmap"),
} }
device_actions = { device_actions = {
"scan_control": (ScanControl.ICON_NAME, "Add Scan Control", "ScanControl"), "scan_control": (widget_icons["ScanControl"], "Add Scan Control", "ScanControl"),
"positioner_box": (PositionerBox.ICON_NAME, "Add Device Box", "PositionerBox"), "positioner_box": (widget_icons["PositionerBox"], "Add Device Box", "PositionerBox"),
"positioner_box_2D": ( "positioner_box_2D": (
PositionerBox2D.ICON_NAME, widget_icons["PositionerBox2D"],
"Add Device 2D Box", "Add Device 2D Box",
"PositionerBox2D", "PositionerBox2D",
), ),
} }
util_actions = { util_actions = {
"queue": (BECQueue.ICON_NAME, "Add Scan Queue", "BECQueue"), "queue": (widget_icons["BECQueue"], "Add Scan Queue", "BECQueue"),
"status": (BECStatusBox.ICON_NAME, "Add BEC Status Box", "BECStatusBox"), "status": (widget_icons["BECStatusBox"], "Add BEC Status Box", "BECStatusBox"),
"progress_bar": ( "progress_bar": (
RingProgressBar.ICON_NAME, widget_icons["RingProgressBar"],
"Add Circular ProgressBar", "Add Circular ProgressBar",
"RingProgressBar", "RingProgressBar",
), ),
"terminal": (BecConsole.ICON_NAME, "Add Terminal", "BecConsole"), "terminal": (widget_icons["BecConsole"], "Add Terminal", "BecConsole"),
"bec_shell": (BECShell.ICON_NAME, "Add BEC Shell", "BECShell"), "bec_shell": (widget_icons["BECShell"], "Add BEC Shell", "BECShell"),
"log_panel": (LogPanel.ICON_NAME, "Add LogPanel - Disabled", "LogPanel"), "sbb_monitor": (widget_icons["SBBMonitor"], "Add SBB Monitor", "SBBMonitor"),
"sbb_monitor": ("train", "Add SBB Monitor", "SBBMonitor"), "log_panel": (widget_icons["LogPanel"], "Add LogPanel", "LogPanel"),
"log_panel": (LogPanel.ICON_NAME, "Add LogPanel", "LogPanel"),
} }
# Create expandable menu actions (original behavior) # Create expandable menu actions (original behavior)
@@ -1198,6 +1189,8 @@ class BECDockArea(DockAreaWidget):
) )
step_ids.append(step_id) step_ids.append(step_id)
from bec_widgets.applications.views.view import ViewTourSteps
return ViewTourSteps(view_title="Dock Area Workspace", step_ids=step_ids) return ViewTourSteps(view_title="Dock Area Workspace", step_ids=step_ids)
def cleanup(self): def cleanup(self):
@@ -1218,6 +1211,9 @@ class BECDockArea(DockAreaWidget):
if __name__ == "__main__": # pragma: no cover if __name__ == "__main__": # pragma: no cover
import sys import sys
from bec_widgets.utils.bec_dispatcher import BECDispatcher
from bec_widgets.widgets.containers.main_window.main_window import BECMainWindowNoRPC
app = QApplication(sys.argv) app = QApplication(sys.argv)
apply_theme("dark") apply_theme("dark")
dispatcher = BECDispatcher(gui_id="ads") dispatcher = BECDispatcher(gui_id="ads")
+18 -7
View File
@@ -2,21 +2,16 @@ from __future__ import annotations
import json import json
from dataclasses import dataclass from dataclasses import dataclass
from typing import Literal from typing import TYPE_CHECKING, Literal
import numpy as np import numpy as np
import pyqtgraph as pg import pyqtgraph as pg
from bec_lib import bec_logger, messages from bec_lib import bec_logger, messages
from bec_lib.endpoints import MessageEndpoints from bec_lib.endpoints import MessageEndpoints
from bec_lib.utils.import_utils import lazy_import, lazy_import_from
from pydantic import BaseModel, Field, field_validator from pydantic import BaseModel, Field, field_validator
from qtpy.QtCore import QObject, Qt, QThread, QTimer, Signal from qtpy.QtCore import QObject, Qt, QThread, QTimer, Signal
from qtpy.QtGui import QTransform from qtpy.QtGui import QTransform
from scipy.interpolate import (
CloughTocher2DInterpolator,
LinearNDInterpolator,
NearestNDInterpolator,
)
from scipy.spatial import cKDTree
from toolz import partition from toolz import partition
from bec_widgets.utils.bec_connector import ConnectionConfig from bec_widgets.utils.bec_connector import ConnectionConfig
@@ -32,6 +27,22 @@ from bec_widgets.widgets.plots.plot_base import PlotBase
logger = bec_logger.logger logger = bec_logger.logger
if TYPE_CHECKING:
from scipy.interpolate import (
CloughTocher2DInterpolator,
LinearNDInterpolator,
NearestNDInterpolator,
)
from scipy.spatial import cKDTree
else:
CloughTocher2DInterpolator, LinearNDInterpolator, NearestNDInterpolator = lazy_import_from(
"scipy.interpolate",
["CloughTocher2DInterpolator", "LinearNDInterpolator", "NearestNDInterpolator"],
)
cKDTree = lazy_import_from("scipy.spatial", ["cKDTree"])
class HeatmapDeviceSignal(BaseModel): class HeatmapDeviceSignal(BaseModel):
"""The configuration of a signal in the scatter waveform widget.""" """The configuration of a signal in the scatter waveform widget."""
@@ -10,6 +10,7 @@ from bec_lib.device import Positioner
from bec_lib.endpoints import MessageEndpoints from bec_lib.endpoints import MessageEndpoints
from bec_lib.lmfit_serializer import serialize_lmfit_params, serialize_param_object from bec_lib.lmfit_serializer import serialize_lmfit_params, serialize_param_object
from bec_lib.scan_data_container import ScanDataContainer from bec_lib.scan_data_container import ScanDataContainer
from bec_lib.utils.import_utils import lazy_import
from pydantic import Field, ValidationError, field_validator from pydantic import Field, ValidationError, field_validator
from qtpy.QtCore import Qt, QTimer, Signal from qtpy.QtCore import Qt, QTimer, Signal
from qtpy.QtWidgets import ( from qtpy.QtWidgets import (
@@ -54,13 +55,7 @@ _DAP_PARAM = object()
if TYPE_CHECKING: # pragma: no cover if TYPE_CHECKING: # pragma: no cover
import lmfit # type: ignore import lmfit # type: ignore
else: else:
try: lmfit = lazy_import("lmfit")
import lmfit # type: ignore
except Exception as e: # pragma: no cover
logger.warning(
f"lmfit could not be imported: {e}. Custom DAP functionality will be unavailable."
)
lmfit = None
# noinspection PyDataclass # noinspection PyDataclass
+21 -21
View File
@@ -239,7 +239,7 @@ class TestBasicDockArea:
assert basic_dock_area.widget_map(bec_widgets_only=False)["panel_bec"] is panel_bec assert basic_dock_area.widget_map(bec_widgets_only=False)["panel_bec"] is panel_bec
def test_new_widget_string_creates_widget(self, basic_dock_area, qtbot): def test_new_widget_string_creates_widget(self, basic_dock_area, qtbot):
basic_dock_area.new("DarkModeButton") basic_dock_area.new("RingProgressBar")
qtbot.waitUntil(lambda: len(basic_dock_area.dock_list()) > 0, timeout=1000) qtbot.waitUntil(lambda: len(basic_dock_area.dock_list()) > 0, timeout=1000)
assert basic_dock_area.widget_list() assert basic_dock_area.widget_list()
@@ -623,7 +623,7 @@ class TestDockManagement:
initial_count = len(advanced_dock_area.dock_list()) initial_count = len(advanced_dock_area.dock_list())
# Create a widget by string name # Create a widget by string name
widget = advanced_dock_area.new("DarkModeButton") widget = advanced_dock_area.new("RingProgressBar")
# Wait for the dock to be created (since it's async) # Wait for the dock to be created (since it's async)
qtbot.wait(200) qtbot.wait(200)
@@ -691,7 +691,7 @@ class TestDockManagement:
initial_count = len(widget_map) initial_count = len(widget_map)
# Create a widget # Create a widget
advanced_dock_area.new("DarkModeButton") advanced_dock_area.new("RingProgressBar")
qtbot.wait(200) qtbot.wait(200)
# Check widget map updated # Check widget map updated
@@ -705,7 +705,7 @@ class TestDockManagement:
initial_count = len(widget_list) initial_count = len(widget_list)
# Create a widget # Create a widget
advanced_dock_area.new("DarkModeButton") advanced_dock_area.new("RingProgressBar")
qtbot.wait(200) qtbot.wait(200)
# Check widget list updated # Check widget list updated
@@ -715,8 +715,8 @@ class TestDockManagement:
def test_delete_all(self, advanced_dock_area, qtbot): def test_delete_all(self, advanced_dock_area, qtbot):
"""Test delete_all functionality.""" """Test delete_all functionality."""
# Create multiple widgets # Create multiple widgets
advanced_dock_area.new("DarkModeButton") advanced_dock_area.new("RingProgressBar")
advanced_dock_area.new("DarkModeButton") advanced_dock_area.new("RingProgressBar")
# Wait for docks to be created # Wait for docks to be created
qtbot.wait(200) qtbot.wait(200)
@@ -772,7 +772,7 @@ class TestWorkspaceLocking:
def test_lock_workspace_property_setter(self, advanced_dock_area, qtbot): def test_lock_workspace_property_setter(self, advanced_dock_area, qtbot):
"""Test workspace_is_locked property setter.""" """Test workspace_is_locked property setter."""
# Create a dock first # Create a dock first
advanced_dock_area.new("DarkModeButton") advanced_dock_area.new("RingProgressBar")
qtbot.wait(200) qtbot.wait(200)
# Initially unlocked # Initially unlocked
@@ -887,8 +887,8 @@ class TestToolbarFunctionality:
def test_attach_all_action(self, advanced_dock_area, qtbot): def test_attach_all_action(self, advanced_dock_area, qtbot):
"""Test attach_all toolbar action.""" """Test attach_all toolbar action."""
# Create floating docks # Create floating docks
advanced_dock_area.new("DarkModeButton", start_floating=True) advanced_dock_area.new("RingProgressBar", start_floating=True)
advanced_dock_area.new("DarkModeButton", start_floating=True) advanced_dock_area.new("RingProgressBar", start_floating=True)
qtbot.wait(200) qtbot.wait(200)
@@ -916,7 +916,7 @@ class TestToolbarFunctionality:
# Floating entry # Floating entry
settings.setArrayIndex(0) settings.setArrayIndex(0)
settings.setValue("object_name", "FloatingWaveform") settings.setValue("object_name", "FloatingWaveform")
settings.setValue("widget_class", "DarkModeButton") settings.setValue("widget_class", "RingProgressBar")
settings.setValue("closable", True) settings.setValue("closable", True)
settings.setValue("floatable", True) settings.setValue("floatable", True)
settings.setValue("movable", True) settings.setValue("movable", True)
@@ -934,7 +934,7 @@ class TestToolbarFunctionality:
# Anchored entry # Anchored entry
settings.setArrayIndex(1) settings.setArrayIndex(1)
settings.setValue("object_name", "EmbeddedWaveform") settings.setValue("object_name", "EmbeddedWaveform")
settings.setValue("widget_class", "DarkModeButton") settings.setValue("widget_class", "RingProgressBar")
settings.setValue("closable", True) settings.setValue("closable", True)
settings.setValue("floatable", True) settings.setValue("floatable", True)
settings.setValue("movable", True) settings.setValue("movable", True)
@@ -1760,9 +1760,9 @@ class TestProfileManagement:
settings = open_runtime_settings("test_manifest") settings = open_runtime_settings("test_manifest")
# Create real docks # Create real docks
advanced_dock_area.new("DarkModeButton") advanced_dock_area.new("RingProgressBar")
advanced_dock_area.new("DarkModeButton") advanced_dock_area.new("RingProgressBar")
advanced_dock_area.new("DarkModeButton") advanced_dock_area.new("RingProgressBar")
# Wait for docks to be created # Wait for docks to be created
qtbot.wait(1000) qtbot.wait(1000)
@@ -1870,7 +1870,7 @@ class TestWorkspaceProfileOperations:
settings.beginWriteArray("manifest/widgets", 1) settings.beginWriteArray("manifest/widgets", 1)
settings.setArrayIndex(0) settings.setArrayIndex(0)
settings.setValue("object_name", "test_widget") settings.setValue("object_name", "test_widget")
settings.setValue("widget_class", "DarkModeButton") settings.setValue("widget_class", "RingProgressBar")
settings.setValue("closable", True) settings.setValue("closable", True)
settings.setValue("floatable", True) settings.setValue("floatable", True)
settings.setValue("movable", True) settings.setValue("movable", True)
@@ -1976,7 +1976,7 @@ class TestWorkspaceProfileOperations:
settings.beginWriteArray("manifest/widgets", 1) settings.beginWriteArray("manifest/widgets", 1)
settings.setArrayIndex(0) settings.setArrayIndex(0)
settings.setValue("object_name", "source_widget") settings.setValue("object_name", "source_widget")
settings.setValue("widget_class", "DarkModeButton") settings.setValue("widget_class", "RingProgressBar")
settings.setValue("closable", True) settings.setValue("closable", True)
settings.setValue("floatable", True) settings.setValue("floatable", True)
settings.setValue("movable", True) settings.setValue("movable", True)
@@ -1985,7 +1985,7 @@ class TestWorkspaceProfileOperations:
advanced_dock_area.load_profile(source_profile) advanced_dock_area.load_profile(source_profile)
qtbot.wait(500) qtbot.wait(500)
advanced_dock_area.new("DarkModeButton") advanced_dock_area.new("RingProgressBar")
qtbot.wait(500) qtbot.wait(500)
class StubDialog: class StubDialog:
@@ -2029,7 +2029,7 @@ class TestWorkspaceProfileOperations:
settings.beginWriteArray("manifest/widgets", 1) settings.beginWriteArray("manifest/widgets", 1)
settings.setArrayIndex(0) settings.setArrayIndex(0)
settings.setValue("object_name", f"{profile}_widget") settings.setValue("object_name", f"{profile}_widget")
settings.setValue("widget_class", "DarkModeButton") settings.setValue("widget_class", "RingProgressBar")
settings.setValue("closable", True) settings.setValue("closable", True)
settings.setValue("floatable", True) settings.setValue("floatable", True)
settings.setValue("movable", True) settings.setValue("movable", True)
@@ -2038,7 +2038,7 @@ class TestWorkspaceProfileOperations:
advanced_dock_area.load_profile(profile_a) advanced_dock_area.load_profile(profile_a)
qtbot.wait(500) qtbot.wait(500)
advanced_dock_area.new("DarkModeButton") advanced_dock_area.new("RingProgressBar")
qtbot.wait(500) qtbot.wait(500)
advanced_dock_area.load_profile(profile_b) advanced_dock_area.load_profile(profile_b)
@@ -2468,8 +2468,8 @@ class TestModeTransitions:
def test_mode_switching_preserves_existing_docks(self, advanced_dock_area, qtbot): def test_mode_switching_preserves_existing_docks(self, advanced_dock_area, qtbot):
"""Test that mode switching doesn't affect existing docked widgets.""" """Test that mode switching doesn't affect existing docked widgets."""
# Create some widgets # Create some widgets
advanced_dock_area.new("DarkModeButton") advanced_dock_area.new("RingProgressBar")
advanced_dock_area.new("DarkModeButton") advanced_dock_area.new("RingProgressBar")
qtbot.wait(200) qtbot.wait(200)
initial_dock_count = len(advanced_dock_area.dock_list()) initial_dock_count = len(advanced_dock_area.dock_list())
+25 -1
View File
@@ -5,7 +5,8 @@ import black
import isort import isort
import pytest import pytest
from bec_widgets.utils.generate_cli import ClientGenerator from bec_widgets.utils.generate_cli import ClientGenerator, write_designer_plugins
from bec_widgets.utils.generate_designer_plugin import DesignerPluginInfo
from bec_widgets.utils.plugin_utils import BECClassContainer, BECClassInfo from bec_widgets.utils.plugin_utils import BECClassContainer, BECClassInfo
# pylint: disable=missing-function-docstring # pylint: disable=missing-function-docstring
@@ -59,6 +60,14 @@ class MockViewWithContent:
"""Activate view.""" """Activate view."""
class MockDesignerWidgetBase:
ICON_NAME = "mock_icon"
class MockDesignerWidget(MockDesignerWidgetBase):
pass
def test_client_generator_with_black_formatting(): def test_client_generator_with_black_formatting():
generator = ClientGenerator(base=True) generator = ClientGenerator(base=True)
container = BECClassContainer() container = BECClassContainer()
@@ -285,3 +294,18 @@ c = a + b"""
content = file.read() content = file.read()
assert corrected in content assert corrected in content
def test_write_designer_plugins(tmp_path):
file_name = tmp_path / "designer_plugins.py"
write_designer_plugins([DesignerPluginInfo(MockDesignerWidget)], str(file_name))
with open(file_name, "r", encoding="utf-8") as file:
content = file.read()
assert '"MockDesignerWidget":' in content
assert '"tests.unit_tests.test_generate_cli_client"' in content
assert '"MockDesignerWidget"' in content
assert '"MockDesignerWidget": "mock_icon"' in content
assert "MockDesignerWidgetPlugin" not in content
+15 -15
View File
@@ -1,7 +1,6 @@
from unittest.mock import patch from unittest.mock import patch
from bec_widgets.utils.bec_widget import BECWidget from bec_widgets.utils import plugin_utils
from bec_widgets.utils.plugin_utils import BECClassContainer, BECClassInfo
from bec_widgets.utils.rpc_widget_handler import RPCWidgetHandler from bec_widgets.utils.rpc_widget_handler import RPCWidgetHandler
@@ -10,21 +9,22 @@ def test_rpc_widget_handler():
assert "Image" in handler.widget_classes assert "Image" in handler.widget_classes
assert "RingProgressBar" in handler.widget_classes assert "RingProgressBar" in handler.widget_classes
assert "BECDockArea" in handler.widget_classes assert "BECDockArea" in handler.widget_classes
assert isinstance(handler.widget_classes["Image"], tuple)
class _TestPluginWidget(BECWidget): ...
@patch( @patch(
"bec_widgets.utils.rpc_widget_handler.get_all_plugin_widgets", "bec_widgets.utils.bec_plugin_helper.get_plugin_rpc_widget_registry",
return_value=BECClassContainer( return_value={
[ "Image": ("plugin.module", "PluginImage"),
BECClassInfo(name="DeviceComboBox", obj=_TestPluginWidget, module="", file=""), "NewPluginWidget": ("plugin.module", "NewPluginWidget"),
BECClassInfo(name="NewPluginWidget", obj=_TestPluginWidget, module="", file=""), },
]
),
) )
def test_duplicate_plugins_not_allowed(_): def test_duplicate_plugins_not_allowed(_):
handler = RPCWidgetHandler() plugin_utils.rpc_widget_registry.cache_clear()
assert handler.widget_classes["DeviceComboBox"] is not _TestPluginWidget
assert handler.widget_classes["NewPluginWidget"] is _TestPluginWidget try:
handler = RPCWidgetHandler()
assert handler.widget_classes["Image"] != ("plugin.module", "PluginImage")
assert handler.widget_classes["NewPluginWidget"] == ("plugin.module", "NewPluginWidget")
finally:
plugin_utils.rpc_widget_registry.cache_clear()