Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 12 additions & 10 deletions bec_widgets/__init__.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import os
import sys

import bec_widgets.widgets.containers.qt_ads as QtAds
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot

if sys.platform.startswith("linux"):
qt_platform = os.environ.get("QT_QPA_PLATFORM", "")
if qt_platform != "offscreen":
os.environ["QT_QPA_PLATFORM"] = "xcb"

# Default QtAds configuration
QtAds.CDockManager.setConfigFlag(QtAds.CDockManager.eConfigFlag.FocusHighlighting, True)
QtAds.CDockManager.setConfigFlag(
QtAds.CDockManager.eConfigFlag.RetainTabSizeWhenCloseButtonHidden, True
)

__all__ = ["BECWidget", "SafeSlot", "SafeProperty"]


def __getattr__(name: str):
if name == "BECWidget":
from bec_widgets.utils.bec_widget import BECWidget

return BECWidget
if name in {"SafeProperty", "SafeSlot"}:
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot

return {"SafeProperty": SafeProperty, "SafeSlot": SafeSlot}[name]
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
Original file line number Diff line number Diff line change
@@ -1,2 +0,0 @@
from .config_choice_dialog import ConfigChoiceDialog
from .device_form_dialog import DeviceFormDialog
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,16 @@
from qtpy import QtCore, QtWidgets

from bec_widgets.utils.error_popups import SafeSlot
from bec_widgets.widgets.control.device_manager.components import OphydValidation
from bec_widgets.widgets.control.device_manager.components.device_config_template.device_config_template import (
DeviceConfigTemplate,
)
from bec_widgets.widgets.control.device_manager.components.device_config_template.template_items import (
validate_name,
)
from bec_widgets.widgets.control.device_manager.components.ophyd_validation import (
from bec_widgets.widgets.control.device_manager.components.ophyd_validation.ophyd_validation import (
OphydValidation,
)
from bec_widgets.widgets.control.device_manager.components.ophyd_validation.ophyd_validation_utils import (
ConfigStatus,
ConnectionStatus,
format_error_to_md,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

from bec_widgets.utils.colors import get_accent_colors
from bec_widgets.utils.error_popups import SafeSlot
from bec_widgets.widgets.control.device_manager.components.ophyd_validation import (
from bec_widgets.widgets.control.device_manager.components.ophyd_validation.ophyd_validation_utils import (
ConfigStatus,
ConnectionStatus,
get_validation_icons,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from bec_lib.logger import bec_logger
from bec_lib.messages import ConfigAction, ScanStatusMessage
from bec_lib.plugin_helper import plugin_package_name, plugin_repo_path
from bec_lib.utils.import_utils import lazy_import_from
from bec_qthemes import apply_theme, material_icon
from qtpy.QtCore import QMetaObject, Qt, QThreadPool, Signal
from qtpy.QtGui import QColor
Expand All @@ -26,26 +27,18 @@
QWidget,
)

from bec_widgets.applications.views.device_manager_view.device_manager_dialogs import (
ConfigChoiceDialog,
DeviceFormDialog,
)
from bec_widgets.applications.views.device_manager_view.device_manager_dialogs.upload_redis_dialog import (
UploadRedisDialog,
)
from bec_widgets.utils.colors import get_accent_colors
from bec_widgets.utils.error_popups import SafeSlot
from bec_widgets.utils.toolbars.actions import MaterialIconAction
from bec_widgets.utils.toolbars.bundles import ToolbarBundle
from bec_widgets.utils.toolbars.toolbar import ModularToolBar
from bec_widgets.widgets.containers.dock_area.basic_dock_area import DockAreaWidget
from bec_widgets.widgets.control.device_manager.components import (
from bec_widgets.widgets.control.device_manager.components._util import SharedSelectionSignal
from bec_widgets.widgets.control.device_manager.components.device_table.device_table import (
DeviceTable,
DMConfigView,
DocstringView,
OphydValidation,
)
from bec_widgets.widgets.control.device_manager.components._util import SharedSelectionSignal
from bec_widgets.widgets.control.device_manager.components.dm_config_view import DMConfigView
from bec_widgets.widgets.control.device_manager.components.dm_docstring_view import DocstringView
from bec_widgets.widgets.control.device_manager.components.ophyd_validation.ophyd_validation_utils import (
ConfigStatus,
ConnectionStatus,
Expand All @@ -61,8 +54,29 @@
if TYPE_CHECKING: # pragma: no cover
from bec_lib.client import BECClient

from bec_widgets.applications.views.device_manager_view.device_manager_dialogs.upload_redis_dialog import (
UploadRedisDialog,
)

logger = bec_logger.logger

ConfigChoiceDialog = lazy_import_from(
"bec_widgets.applications.views.device_manager_view.device_manager_dialogs.config_choice_dialog",
("ConfigChoiceDialog",),
)
DeviceFormDialog = lazy_import_from(
"bec_widgets.applications.views.device_manager_view.device_manager_dialogs.device_form_dialog",
("DeviceFormDialog",),
)
UploadRedisDialog = lazy_import_from(
"bec_widgets.applications.views.device_manager_view.device_manager_dialogs.upload_redis_dialog",
("UploadRedisDialog",),
)
OphydValidation = lazy_import_from(
"bec_widgets.widgets.control.device_manager.components.ophyd_validation.ophyd_validation",
("OphydValidation",),
)

_yes_no_question = partial(
QMessageBox.question,
buttons=QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
"""Module for Device Manager View."""

from bec_lib.utils.import_utils import lazy_import_from
from qtpy.QtCore import QRect
from qtpy.QtWidgets import QWidget

from bec_widgets.applications.views.device_manager_view.device_manager_widget import (
DeviceManagerWidget,
)
from bec_widgets.applications.views.view import ViewBase, ViewTourSteps
from bec_widgets.utils.error_popups import SafeSlot

DeviceManagerWidget = lazy_import_from(
"bec_widgets.applications.views.device_manager_view.device_manager_widget",
("DeviceManagerWidget",),
)


class DeviceManagerView(ViewBase):
"""
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,18 @@

from bec_lib.bec_yaml_loader import yaml_load
from bec_lib.logger import bec_logger
from bec_lib.utils.import_utils import lazy_import_from
from bec_qthemes import material_icon
from qtpy import QtCore, QtWidgets

from bec_widgets.applications.views.device_manager_view.device_manager_display_widget import (
DeviceManagerDisplayWidget,
)
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.error_popups import SafeSlot

DeviceManagerDisplayWidget = lazy_import_from(
"bec_widgets.applications.views.device_manager_view.device_manager_display_widget",
("DeviceManagerDisplayWidget",),
)

logger = bec_logger.logger


Expand Down
42 changes: 32 additions & 10 deletions bec_widgets/cli/rpc/rpc_widget_handler.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,19 @@
from __future__ import annotations

from bec_widgets.cli.client_utils import IGNORE_WIDGETS
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_custom_classes
from typing import TYPE_CHECKING

from bec_lib.utils.import_utils import lazy_import_from

from bec_widgets.utils.bec_plugin_helper import get_all_plugin_widget_references
from bec_widgets.utils.plugin_utils import get_custom_class_references

try:
from bec_widgets.cli.constants import IGNORE_WIDGETS
except ModuleNotFoundError: # pragma: no cover
IGNORE_WIDGETS = ["LaunchWindow"]

if TYPE_CHECKING: # pragma: no cover
from bec_widgets.utils.bec_widget import BECWidget


class RPCWidgetHandler:
Expand All @@ -13,7 +23,7 @@ def __init__(self):
self._widget_classes = None

@property
def widget_classes(self) -> dict[str, type[BECWidget]]:
def widget_classes(self) -> dict[str, type["BECWidget"]]:
"""
Get the available widget classes.

Expand All @@ -31,12 +41,24 @@ def update_available_widgets(self):
Returns:
None
"""
self._widget_classes = (
get_custom_classes("bec_widgets", packages=("widgets", "applications"))
+ get_all_plugin_widgets()
).as_dict(IGNORE_WIDGETS)
ignored = set(IGNORE_WIDGETS)
widget_classes = {
reference.name: lazy_import_from(reference.module, (reference.name,))
for reference in get_all_plugin_widget_references()
if reference.name not in ignored
}
widget_classes.update(
{
reference.name: lazy_import_from(reference.module, (reference.name,))
for reference in get_custom_class_references(
"bec_widgets", packages=("widgets", "applications")
)
if reference.name not in ignored
}
)
self._widget_classes = widget_classes

def create_widget(self, widget_type, **kwargs) -> BECWidget:
def create_widget(self, widget_type, **kwargs) -> "BECWidget":
"""
Create a widget from an RPC message.

Expand Down
13 changes: 0 additions & 13 deletions bec_widgets/utils/__init__.py
Original file line number Diff line number Diff line change
@@ -1,13 +0,0 @@
from qtpy.QtWebEngineWidgets import QWebEngineView

from .bec_connector import BECConnector, ConnectionConfig
from .bec_dispatcher import BECDispatcher
from .bec_table import BECTable
from .colors import Colors
from .container_utils import WidgetContainerUtils
from .crosshair import Crosshair
from .entry_validator import EntryValidator
from .layout_manager import GridLayoutManager
from .rpc_decorator import register_rpc_methods, rpc_public
from .ui_loader import UILoader
from .validator_delegate import DoubleValidationDelegate
54 changes: 53 additions & 1 deletion bec_widgets/utils/bec_plugin_helper.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from __future__ import annotations

import ast
import importlib.metadata
import inspect
import os
import pkgutil
import traceback
from importlib import util as importlib_util
Expand All @@ -11,11 +13,61 @@

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,
BECClassReference,
_ast_node_name,
_class_has_rpc_markers,
)

logger = bec_logger.logger


def _plugin_class_is_candidate(node: ast.ClassDef) -> bool:
base_names = {_ast_node_name(base) for base in node.bases}
return bool({"BECWidget", "BECConnector"} & base_names) or _class_has_rpc_markers(node)


def get_all_plugin_widget_references() -> list[BECClassReference]:
references: list[BECClassReference] = []
seen_names: set[str] = set()
for entry_point in importlib.metadata.entry_points(group="bec.widgets.user_widgets"): # type: ignore
spec = importlib_util.find_spec(entry_point.module)
if spec is None:
continue

package_roots = list(spec.submodule_search_locations or ())
if spec.origin and not package_roots:
package_roots = [os.path.dirname(spec.origin)]

for package_root in package_roots:
for root, _, files in sorted(os.walk(package_root)):
for file_name in sorted(files):
if not file_name.endswith(".py") or file_name.startswith("__"):
continue
path = os.path.join(root, file_name)
with open(path, encoding="utf-8") as file_handle:
module = ast.parse(file_handle.read(), filename=path)
module_name = ".".join(
os.path.relpath(path, package_root).removesuffix(".py").split(os.sep)
)
for node in module.body:
if not isinstance(node, ast.ClassDef) or not _plugin_class_is_candidate(
node
):
continue
if node.name in seen_names:
continue
references.append(
BECClassReference(
name=node.name, module=f"{entry_point.module}.{module_name}"
)
)
seen_names.add(node.name)
return references


def _submodule_specs(module: ModuleType) -> tuple[ModuleSpec | None, ...]:
"""Return specs for all submodules of the given module."""
return tuple(
Expand Down
Loading
Loading