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

Compare commits

..

5 Commits

Author SHA1 Message Date
wakonig_k 2ae0a40616 ci: update github-script action to v9 2026-04-20 16:50:11 +02:00
wakonig_k 6af1683c23 wip 2026-04-20 16:48:01 +02:00
wakonig_k 5271db1ca6 wip 2026-04-20 16:36:47 +02:00
wakonig_k 92d4519853 ci: move to external action 2026-04-20 16:25:51 +02:00
wakonig_k 42439097e9 ci: add benchmark workflow 2026-04-17 16:06:02 +02:00
100 changed files with 653 additions and 648 deletions
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env bash
set -euo pipefail
bec_core_branch="${BEC_CORE_BRANCH:-main}"
ophyd_devices_branch="${OPHYD_DEVICES_BRANCH:-main}"
plugin_repo_branch="${PLUGIN_REPO_BRANCH:-main}"
python_version="${PYTHON_VERSION:-3.11}"
if command -v conda >/dev/null 2>&1; then
conda_base="$(conda info --base)"
source "$conda_base/etc/profile.d/conda.sh"
fi
echo "Using branch ${bec_core_branch} of BEC CORE"
git clone --branch "$bec_core_branch" https://github.com/bec-project/bec.git
echo "Using branch ${ophyd_devices_branch} of OPHYD_DEVICES"
git clone --branch "$ophyd_devices_branch" https://github.com/bec-project/ophyd_devices.git
echo "Using branch ${plugin_repo_branch} of bec_testing_plugin"
git clone --branch "$plugin_repo_branch" https://github.com/bec-project/bec_testing_plugin.git
conda create -q -n test-environment "python=${python_version}"
conda activate test-environment
cd bec
source ./bin/install_bec_dev.sh
cd ..
python -m pip install -e ./ophyd_devices -e .[dev,pyside6] -e ./bec_testing_plugin
benchmark_tmp_dir="$(mktemp -d)"
export BEC_SERVICE_CONFIG="$benchmark_tmp_dir/services_config.yaml"
# Start Redis
redis-server --daemonize yes --port 6379
# Wait for Redis to be ready
timeout 30 bash -c 'until redis-cli ping > /dev/null 2>&1; do sleep 0.1; done' || {
echo "Redis failed to start" >&2
exit 1
}
# Start BEC server
bec-server start --config "$BEC_SERVICE_CONFIG"
# Wait for BEC server to be ready
sleep 5
# Export BEC client configuration
export BEC_CONFIG='{"redis": {"host": "localhost", "port": 6379}}'
+33
View File
@@ -0,0 +1,33 @@
#!/usr/bin/env python3
"""Start BEC services for benchmark workflows and keep them alive."""
from __future__ import annotations
import argparse
from pathlib import Path
from bec_ipython_client import BECIPythonClient
from bec_lib.redis_connector import RedisConnector
from bec_lib.service_config import ServiceConfig
def _load_demo_config(services_config: Path) -> None:
bec = BECIPythonClient(ServiceConfig(services_config), RedisConnector, forced=True)
bec.start()
try:
bec.config.load_demo_config()
finally:
bec.shutdown()
bec._client._reset_singleton()
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--services-config", required=True, type=Path)
args = parser.parse_args()
_load_demo_config(args.services_config)
if __name__ == "__main__":
main()
+47
View File
@@ -0,0 +1,47 @@
name: BW Benchmarks
on: [ workflow_call ]
permissions:
contents: read
env:
QTWEBENGINE_DISABLE_SANDBOX: 1
QT_QPA_PLATFORM: "offscreen"
jobs:
benchmark:
runs-on: ubuntu-latest
permissions:
contents: write
issues: write
pull-requests: write
defaults:
run:
shell: bash -el {0}
steps:
- name: Checkout BEC Widgets
uses: actions/checkout@v4
with:
repository: bec-project/bec_widgets
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Set up Conda
uses: conda-incubator/setup-miniconda@v3
with:
auto-update-conda: true
auto-activate-base: true
python-version: "3.11"
- name: Run, compare, and publish benchmarks
uses: bec-project/benchmark_action@main
with:
mode: all
attempts: "3"
system-packages: libgl1 libegl1 x11-utils libxkbcommon-x11-0 libdbus-1-3 xvfb
libnss3 libxdamage1 libasound2t64 libatomic1 libxcursor1 hyperfine
redis-server
setup-scripts: .github/scripts/setup_benchmark_env.sh
benchmark-pytest-dirs: tests/unit_tests/benchmarks
threshold-percent: "10"
+2 -2
View File
@@ -12,7 +12,7 @@ jobs:
outputs:
branch-pr: ${{ steps.script.outputs.result }}
steps:
- uses: actions/github-script@v7
- uses: actions/github-script@v9
id: script
if: github.event_name == 'push' && github.event.ref_type != 'tag'
with:
@@ -25,4 +25,4 @@ jobs:
if (prs.data.length) {
console.log(`::notice ::Skipping CI on branch push as it is already run in PR #${prs.data[0]["number"]}`)
return prs.data[0]["number"]
}
}
+17 -7
View File
@@ -1,19 +1,19 @@
name: Full CI
on:
on:
push:
pull_request:
workflow_dispatch:
inputs:
BEC_WIDGETS_BRANCH:
description: 'Branch of BEC Widgets to install'
description: "Branch of BEC Widgets to install"
required: false
type: string
BEC_CORE_BRANCH:
description: 'Branch of BEC Core to install'
description: "Branch of BEC Core to install"
required: false
type: string
OPHYD_DEVICES_BRANCH:
description: 'Branch of Ophyd Devices to install'
description: "Branch of Ophyd Devices to install"
required: false
type: string
@@ -23,6 +23,7 @@ concurrency:
permissions:
pull-requests: write
contents: read
jobs:
check_pr_status:
@@ -33,6 +34,15 @@ jobs:
if: needs.check_pr_status.outputs.branch-pr == ''
uses: ./.github/workflows/formatter.yml
benchmark:
needs: [check_pr_status]
if: needs.check_pr_status.outputs.branch-pr == ''
permissions:
contents: write
issues: write
pull-requests: write
uses: ./.github/workflows/benchmark.yml
unit-test:
needs: [check_pr_status, formatter]
if: needs.check_pr_status.outputs.branch-pr == ''
@@ -69,9 +79,9 @@ jobs:
uses: ./.github/workflows/child_repos.yml
with:
BEC_CORE_BRANCH: ${{ inputs.BEC_CORE_BRANCH || 'main' }}
OPHYD_DEVICES_BRANCH: ${{ inputs.OPHYD_DEVICES_BRANCH || 'main'}}
OPHYD_DEVICES_BRANCH: ${{ inputs.OPHYD_DEVICES_BRANCH || 'main'}}
BEC_WIDGETS_BRANCH: ${{ inputs.BEC_WIDGETS_BRANCH || github.head_ref || github.sha }}
plugin_repos:
needs: [check_pr_status, formatter]
if: needs.check_pr_status.outputs.branch-pr == ''
@@ -81,4 +91,4 @@ jobs:
BEC_WIDGETS_BRANCH: ${{ inputs.BEC_WIDGETS_BRANCH || github.head_ref || github.sha }}
secrets:
GH_READ_TOKEN: ${{ secrets.GH_READ_TOKEN }}
GH_READ_TOKEN: ${{ secrets.GH_READ_TOKEN }}
+12 -13
View File
@@ -1,25 +1,25 @@
name: Run Pytest with different Python versions
on:
on:
workflow_call:
inputs:
pr_number:
description: 'Pull request number'
description: "Pull request number"
required: false
type: number
BEC_CORE_BRANCH:
description: 'Branch of BEC Core to install'
description: "Branch of BEC Core to install"
required: false
default: 'main'
default: "main"
type: string
OPHYD_DEVICES_BRANCH:
description: 'Branch of Ophyd Devices to install'
description: "Branch of Ophyd Devices to install"
required: false
default: 'main'
default: "main"
type: string
BEC_WIDGETS_BRANCH:
description: 'Branch of BEC Widgets to install'
description: "Branch of BEC Widgets to install"
required: false
default: 'main'
default: "main"
type: string
jobs:
@@ -30,15 +30,14 @@ jobs:
python-version: ["3.11", "3.12", "3.13"]
env:
BEC_WIDGETS_BRANCH: main # Set the branch you want for bec_widgets
BEC_CORE_BRANCH: main # Set the branch you want for bec
OPHYD_DEVICES_BRANCH: main # Set the branch you want for ophyd_devices
BEC_WIDGETS_BRANCH: main # Set the branch you want for bec_widgets
BEC_CORE_BRANCH: main # Set the branch you want for bec
OPHYD_DEVICES_BRANCH: main # Set the branch you want for ophyd_devices
PROJECT_PATH: ${{ github.repository }}
QTWEBENGINE_DISABLE_SANDBOX: 1
QT_QPA_PLATFORM: "offscreen"
steps:
- name: Checkout BEC Widgets
uses: actions/checkout@v4
with:
@@ -56,4 +55,4 @@ jobs:
- name: Run Pytest
run: |
pip install pytest pytest-random-order
pytest -v --junitxml=report.xml --random-order ./tests/unit_tests
pytest -v --junitxml=report.xml --random-order --ignore=tests/unit_tests/benchmarks ./tests/unit_tests
+10 -12
View File
@@ -1,32 +1,30 @@
name: Run Pytest with Coverage
on:
on:
workflow_call:
inputs:
pr_number:
description: 'Pull request number'
description: "Pull request number"
required: false
type: number
BEC_CORE_BRANCH:
description: 'Branch of BEC Core to install'
description: "Branch of BEC Core to install"
required: false
default: 'main'
default: "main"
type: string
OPHYD_DEVICES_BRANCH:
description: 'Branch of Ophyd Devices to install'
description: "Branch of Ophyd Devices to install"
required: false
default: 'main'
default: "main"
type: string
BEC_WIDGETS_BRANCH:
description: 'Branch of BEC Widgets to install'
description: "Branch of BEC Widgets to install"
required: false
default: 'main'
default: "main"
type: string
secrets:
CODECOV_TOKEN:
required: true
permissions:
pull-requests: write
@@ -55,7 +53,7 @@ jobs:
- name: Run Pytest with Coverage
id: coverage
run: pytest --random-order --cov=bec_widgets --cov-config=pyproject.toml --cov-branch --cov-report=xml --no-cov-on-fail tests/unit_tests/
run: pytest --random-order --cov=bec_widgets --cov-config=pyproject.toml --cov-branch --cov-report=xml --no-cov-on-fail --ignore=tests/unit_tests/benchmarks tests/unit_tests/
- name: Upload test artifacts
uses: actions/upload-artifact@v4
@@ -69,4 +67,4 @@ jobs:
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
slug: bec-project/bec_widgets
slug: bec-project/bec_widgets
+10 -12
View File
@@ -1,21 +1,19 @@
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}")
@@ -0,0 +1,2 @@
from .config_choice_dialog import ConfigChoiceDialog
from .device_form_dialog import DeviceFormDialog
@@ -8,16 +8,14 @@ from ophyd_devices.interfaces.device_config_templates.ophyd_templates import OPH
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.ophyd_validation import (
OphydValidation,
)
from bec_widgets.widgets.control.device_manager.components.ophyd_validation.ophyd_validation_utils import (
from bec_widgets.widgets.control.device_manager.components.ophyd_validation import (
ConfigStatus,
ConnectionStatus,
format_error_to_md,
@@ -12,7 +12,7 @@ from qtpy import QtCore, QtGui, QtWidgets
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.ophyd_validation_utils import (
from bec_widgets.widgets.control.device_manager.components.ophyd_validation import (
ConfigStatus,
ConnectionStatus,
get_validation_icons,
@@ -13,7 +13,6 @@ from bec_lib.file_utils import DeviceConfigWriter
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
@@ -27,18 +26,26 @@ from qtpy.QtWidgets import (
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._util import SharedSelectionSignal
from bec_widgets.widgets.control.device_manager.components.device_table.device_table import (
from bec_widgets.widgets.control.device_manager.components import (
DeviceTable,
DMConfigView,
DocstringView,
OphydValidation,
)
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._util import SharedSelectionSignal
from bec_widgets.widgets.control.device_manager.components.ophyd_validation.ophyd_validation_utils import (
ConfigStatus,
ConnectionStatus,
@@ -54,29 +61,8 @@ from bec_widgets.widgets.utility.spinner.spinner import SpinnerWidget
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,
@@ -1,17 +1,14 @@
"""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):
"""
@@ -6,18 +6,15 @@ import os
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
+184
View File
@@ -3172,6 +3172,190 @@ class LogPanel(RPCBase):
class Minesweeper(RPCBase): ...
class MonacoDock(RPCBase):
"""MonacoDock is a dock widget that contains Monaco editor instances."""
@rpc_call
def new(
self,
widget: "QWidget | str",
*,
closable: "bool" = True,
floatable: "bool" = True,
movable: "bool" = True,
start_floating: "bool" = False,
floating_state: "Mapping[str, object] | None" = None,
where: "Literal['left', 'right', 'top', 'bottom'] | None" = None,
on_close: "Callable[[CDockWidget, QWidget], None] | None" = None,
tab_with: "CDockWidget | QWidget | str | None" = None,
relative_to: "CDockWidget | QWidget | str | None" = None,
return_dock: "bool" = False,
show_title_bar: "bool | None" = None,
title_buttons: "Mapping[str, bool] | Sequence[str] | str | None" = None,
show_settings_action: "bool | None" = False,
promote_central: "bool" = False,
dock_icon: "QIcon | None" = None,
apply_widget_icon: "bool" = True,
object_name: "str | None" = None,
**widget_kwargs,
) -> "QWidget | CDockWidget | BECWidget":
"""
Create a new widget (or reuse an instance) and add it as a dock.
Args:
widget(QWidget | str): Instance or registered widget type string.
closable(bool): Whether the dock is closable.
floatable(bool): Whether the dock is floatable.
movable(bool): Whether the dock is movable.
start_floating(bool): Whether to start the dock floating.
floating_state(Mapping | None): Optional floating geometry metadata to apply when floating.
where(Literal["left", "right", "top", "bottom"] | None): Dock placement hint relative to the dock area (ignored when
``relative_to`` is provided without an explicit value).
on_close(Callable[[CDockWidget, QWidget], None] | None): Optional custom close handler accepting (dock, widget).
tab_with(CDockWidget | QWidget | str | None): Existing dock (or widget/name) to tab the new dock alongside.
relative_to(CDockWidget | QWidget | str | None): Existing dock (or widget/name) used as the positional anchor.
When supplied and ``where`` is ``None``, the new dock inherits the
anchor's current dock area.
return_dock(bool): When True, return the created dock instead of the widget.
show_title_bar(bool | None): Explicitly show or hide the dock area's title bar.
title_buttons(Mapping[str, bool] | Sequence[str] | str | None): Mapping or iterable describing which title bar buttons should
remain visible. Provide a mapping of button names (``"float"``,
``"close"``, ``"menu"``, ``"auto_hide"``, ``"minimize"``) to booleans,
or a sequence of button names to hide.
show_settings_action(bool | None): Control whether a dock settings/property action should
be installed. Defaults to ``False`` for the basic dock area; subclasses
such as `BECDockArea` override the default to ``True``.
promote_central(bool): When True, promote the created dock to be the dock manager's
central widget (useful for editor stacks or other root content).
dock_icon(QIcon | None): Optional icon applied to the dock via ``CDockWidget.setIcon``.
Provide a `QIcon` (e.g. from ``material_icon``). When ``None`` (default),
the widget's ``ICON_NAME`` attribute is used when available.
apply_widget_icon(bool): When False, skip automatically resolving the icon from
the widget's ``ICON_NAME`` (useful for callers who want no icon and do not pass one explicitly).
object_name(str | None): Optional object name to assign to the created widget.
**widget_kwargs: Additional keyword arguments passed to the widget constructor
when creating by type name.
Returns:
The widget instance by default, or the created `CDockWidget` when `return_dock` is True.
"""
@rpc_call
def dock_map(self) -> "dict[str, CDockWidget]":
"""
Return the dock widgets map as dictionary with names as keys.
"""
@rpc_call
def dock_list(self) -> "list[CDockWidget]":
"""
Return the list of dock widgets.
"""
@rpc_call
def widget_map(self, bec_widgets_only: "bool" = True) -> "dict[str, QWidget]":
"""
Return a dictionary mapping widget names to their corresponding widgets.
Args:
bec_widgets_only(bool): If True, only include widgets that are BECConnector instances.
"""
@rpc_call
def widget_list(self, bec_widgets_only: "bool" = True) -> "list[QWidget]":
"""
Return a list of widgets contained in the dock area.
Args:
bec_widgets_only(bool): If True, only include widgets that are BECConnector instances.
"""
@rpc_call
def attach_all(self):
"""
Re-attach floating docks back into the dock manager.
"""
@rpc_call
def delete_all(self):
"""
Delete all docks and their associated widgets.
"""
@rpc_call
def delete(self, object_name: "str") -> "bool":
"""
Remove a widget from the dock area by its object name.
Args:
object_name: The object name of the widget to remove.
Returns:
bool: True if the widget was found and removed, False otherwise.
Raises:
ValueError: If no widget with the given object name is found.
Example:
>>> dock_area.delete("my_widget")
True
"""
@rpc_call
def set_layout_ratios(
self,
*,
horizontal: "Sequence[float] | Mapping[int | str, float] | None" = None,
vertical: "Sequence[float] | Mapping[int | str, float] | None" = None,
splitter_overrides: "Mapping[int | str | Sequence[int], Sequence[float] | Mapping[int | str, float]] | None" = None,
) -> "None":
"""
Adjust splitter ratios in the dock layout.
Args:
horizontal: Weights applied to every horizontal splitter encountered.
vertical: Weights applied to every vertical splitter encountered.
splitter_overrides: Optional overrides targeting specific splitters identified
by their index path (e.g. ``{0: [1, 2], (1, 0): [3, 5]}``). Paths are zero-based
indices following the splitter hierarchy, starting from the root splitter.
Example:
To build three columns with custom per-column ratios::
area.set_layout_ratios(
horizontal=[1, 2, 1], # column widths
splitter_overrides={
0: [1, 2], # column 0 (two rows)
1: [3, 2, 1], # column 1 (three rows)
2: [1], # column 2 (single row)
},
)
"""
@rpc_call
def describe_layout(self) -> "list[dict[str, Any]]":
"""
Return metadata describing splitter paths, orientations, and contained docks.
Useful for determining the keys to use in `set_layout_ratios(splitter_overrides=...)`.
"""
@rpc_call
def print_layout_structure(self) -> "None":
"""
Pretty-print the current splitter paths to stdout.
"""
@rpc_call
def set_central_dock(self, dock: "CDockWidget | QWidget | str") -> "None":
"""
Promote an existing dock to be the dock manager's central widget.
Args:
dock(CDockWidget | QWidget | str): Dock reference to promote.
"""
class MonacoWidget(RPCBase):
"""A simple Monaco editor widget"""
+10 -32
View File
@@ -1,19 +1,9 @@
from __future__ import annotations
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
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
class RPCWidgetHandler:
@@ -23,7 +13,7 @@ class RPCWidgetHandler:
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.
@@ -41,24 +31,12 @@ class RPCWidgetHandler:
Returns:
None
"""
ignored = set(IGNORE_WIDGETS)
widget_classes = {
reference.name: lazy_import_from(reference.module, (reference.name,))
for reference in get_all_plugin_widget_references(use_cache=False)
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"), use_cache=False
)
if reference.name not in ignored
}
)
self._widget_classes = widget_classes
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":
def create_widget(self, widget_type, **kwargs) -> BECWidget:
"""
Create a widget from an RPC message.
+13
View File
@@ -0,0 +1,13 @@
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
+3 -59
View File
@@ -1,9 +1,7 @@
from __future__ import annotations
import ast
import importlib.metadata
import inspect
import logging
import pkgutil
import traceback
from importlib import util as importlib_util
@@ -11,65 +9,11 @@ from importlib.machinery import FileFinder, ModuleSpec, SourceFileLoader
from types import ModuleType
from typing import Generator
from bec_widgets.utils.plugin_utils import (
BECClassContainer,
BECClassInfo,
BECClassReference,
_ast_node_name,
_class_has_rpc_markers,
_discover_class_references_from_roots,
_find_package_roots,
)
from bec_lib.logger import bec_logger
logger = logging.getLogger(__name__)
from bec_widgets.utils.plugin_utils import BECClassContainer, BECClassInfo
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)
_PLUGIN_WIDGET_REFERENCE_CACHE: dict[tuple[tuple[str, str], ...], tuple[BECClassReference, ...]] = (
{}
)
def _plugin_entry_point_snapshot() -> tuple[tuple[str, str], ...]:
return tuple(
sorted(
(entry_point.name, entry_point.module)
for entry_point in importlib.metadata.entry_points(group="bec.widgets.user_widgets") # type: ignore
)
)
def _build_plugin_widget_references() -> tuple[BECClassReference, ...]:
references: list[BECClassReference] = []
seen_names: set[str] = set()
for entry_point in importlib.metadata.entry_points(group="bec.widgets.user_widgets"): # type: ignore
try:
package_roots = _find_package_roots(entry_point.module)
except ModuleNotFoundError:
continue
for reference in _discover_class_references_from_roots(
entry_point.module,
package_roots,
file_name_filter=lambda file_name: file_name.endswith(".py")
and not file_name.startswith("__"),
candidate_filter=_plugin_class_is_candidate,
):
if reference.name in seen_names:
continue
references.append(reference)
seen_names.add(reference.name)
return tuple(references)
def get_all_plugin_widget_references(*, use_cache: bool = True) -> list[BECClassReference]:
snapshot = _plugin_entry_point_snapshot()
if not use_cache or snapshot not in _PLUGIN_WIDGET_REFERENCE_CACHE:
_PLUGIN_WIDGET_REFERENCE_CACHE[snapshot] = _build_plugin_widget_references()
return list(_PLUGIN_WIDGET_REFERENCE_CACHE[snapshot])
logger = bec_logger.logger
def _submodule_specs(module: ModuleType) -> tuple[ModuleSpec | None, ...]:
+1
View File
@@ -20,6 +20,7 @@ from bec_widgets.widgets.utility.spinner.spinner import SpinnerWidget
if TYPE_CHECKING: # pragma: no cover
from bec_widgets.utils.busy_loader import BusyLoaderOverlay
from bec_widgets.widgets.containers.dock import BECDock
logger = bec_logger.logger
+47 -288
View File
@@ -1,23 +1,22 @@
from __future__ import annotations
import ast
import importlib
import inspect
import os
from dataclasses import dataclass
from functools import lru_cache
from importlib import util as importlib_util
from typing import TYPE_CHECKING, Callable, 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 import BECConnector
from bec_widgets.utils.bec_widget import BECWidget
if TYPE_CHECKING: # pragma: no cover
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
_DISCOVERY_BASE_NAMES = frozenset({"BECConnector", "BECWidget", "ViewBase"})
def get_plugin_widgets() -> dict[str, "BECConnector"]:
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
@@ -36,10 +35,9 @@ def get_plugin_widgets() -> dict[str, "BECConnector"]:
Returns:
dict[str, BECConnector]: A dictionary of widget names and their respective classes.
"""
from bec_lib.plugin_helper import _get_available_plugins
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:
@@ -50,8 +48,6 @@ def get_plugin_widgets() -> dict[str, "BECConnector"]:
def _filter_plugins(obj):
from bec_widgets.utils.bec_connector import BECConnector
return inspect.isclass(obj) and issubclass(obj, BECConnector)
@@ -70,8 +66,6 @@ def get_plugin_auto_updates() -> dict[str, type[AutoUpdates]]:
Returns:
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")
loaded_plugins = {}
for module in modules:
@@ -96,20 +90,14 @@ class BECClassInfo:
name: str
module: str
file: str
obj: type["BECWidget"]
obj: type[BECWidget]
is_connector: bool = False
is_widget: bool = False
is_plugin: bool = False
@dataclass(frozen=True)
class BECClassReference:
name: str
module: str
class BECClassContainer:
def __init__(self, initial: Iterable[BECClassInfo] = ()):
def __init__(self, initial: Iterable[BECClassInfo] = []):
self._collection: list[BECClassInfo] = list(initial)
def __repr__(self):
@@ -121,13 +109,12 @@ class BECClassContainer:
def __add__(self, other: BECClassContainer):
return BECClassContainer((*self, *(c for c in other if c.name not in self.names)))
def as_dict(self, ignores: list[str] | None = None) -> dict[str, type["BECWidget"]]:
def as_dict(self, ignores: list[str] = []) -> dict[str, type[BECWidget]]:
"""get a dict of {name: Type} for all the entries in the collection.
Args:
ignores(list[str]): a list of class names to exclude from the dictionary."""
ignore_set = set(ignores or ())
return {c.name: c.obj for c in self if c.name not in ignore_set}
return {c.name: c.obj for c in self if c.name not in ignores}
def add_class(self, class_info: BECClassInfo):
"""
@@ -179,276 +166,48 @@ class BECClassContainer:
return [info.obj for info in self.collection]
def _ast_node_name(node: ast.expr) -> str | None:
if isinstance(node, ast.Name):
return node.id
if isinstance(node, ast.Attribute):
return node.attr
return None
def _class_has_rpc_markers(node: ast.ClassDef) -> bool:
for stmt in node.body:
if isinstance(stmt, ast.Assign):
target_names = {target.id for target in stmt.targets if isinstance(target, ast.Name)}
if (
"PLUGIN" in target_names
and isinstance(stmt.value, ast.Constant)
and stmt.value.value
):
return True
if {"RPC_CONTENT_CLASS", "USER_ACCESS"} & target_names:
return True
if isinstance(stmt, ast.AnnAssign) and isinstance(stmt.target, ast.Name):
if (
stmt.target.id == "PLUGIN"
and isinstance(stmt.value, ast.Constant)
and stmt.value.value
):
return True
if stmt.target.id in {"RPC_CONTENT_CLASS", "USER_ACCESS"}:
return True
return False
def _class_is_candidate(node: ast.ClassDef) -> bool:
base_names = {_ast_node_name(base) for base in node.bases}
return bool(_DISCOVERY_BASE_NAMES & base_names) or _class_has_rpc_markers(node)
def _candidate_top_level_class_names(path: str) -> list[str]:
with open(path, encoding="utf-8") as file_handle:
module = ast.parse(file_handle.read(), filename=path)
return [
node.name
for node in module.body
if isinstance(node, ast.ClassDef) and _class_is_candidate(node)
]
@lru_cache(maxsize=64)
def _find_package_roots(module_name: str) -> tuple[str, ...]:
spec = importlib_util.find_spec(module_name)
if spec is None:
raise ModuleNotFoundError(module_name)
package_roots = tuple(spec.submodule_search_locations or ())
if package_roots:
return package_roots
if spec.origin:
return (os.path.dirname(spec.origin),)
raise ModuleNotFoundError(module_name)
def _discover_class_references_from_roots(
module_prefix: str,
package_roots: Iterable[str],
*,
file_name_filter: Callable[[str], bool],
candidate_filter: Callable[[ast.ClassDef], bool],
) -> tuple[BECClassReference, ...]:
references: list[BECClassReference] = []
seen_names: set[str] = set()
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_filter(file_name):
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)
rel_path = os.path.relpath(path, package_root).removesuffix(".py")
module_name = ".".join([module_prefix, *rel_path.split(os.sep)])
for node in module.body:
if not isinstance(node, ast.ClassDef) or not candidate_filter(node):
continue
if node.name in seen_names:
continue
references.append(BECClassReference(name=node.name, module=module_name))
seen_names.add(node.name)
return tuple(references)
def _iter_candidate_modules(repo_name: str, package: str) -> Iterable[tuple[str, str, list[str]]]:
try:
package_roots = _find_package_roots(f"{repo_name}.{package}")
except ModuleNotFoundError:
return ()
modules: list[tuple[str, str, list[str]]] = []
for directory in package_roots:
for root, _, files in sorted(os.walk(directory)):
for file_name in sorted(files):
if (
not file_name.endswith(".py")
or file_name.startswith("__")
or file_name.startswith("register_")
or file_name.endswith("_plugin.py")
):
continue
path = os.path.join(root, file_name)
rel_dir = os.path.dirname(os.path.relpath(path, directory))
module_name = (
file_name.removesuffix(".py")
if rel_dir in ("", ".")
else ".".join(rel_dir.split(os.sep) + [file_name.removesuffix(".py")])
)
class_names = _candidate_top_level_class_names(path)
if class_names:
modules.append((f"{repo_name}.{package}.{module_name}", path, class_names))
return tuple(modules)
def _collect_classes_from_package(repo_name: str, package: str) -> BECClassContainer:
"""Collect classes from a package subtree (for example ``widgets`` or ``applications``)."""
collection = BECClassContainer()
for module_name, path, _ in _iter_candidate_modules(repo_name, package):
from qtpy.QtWidgets import QWidget
try:
anchor_module = importlib.import_module(f"{repo_name}.{package}")
except ModuleNotFoundError as exc:
# Some plugin repositories expose only one subtree. Skip gracefully if it does not exist.
if exc.name == f"{repo_name}.{package}":
return collection
raise
from bec_widgets.utils.bec_connector import BECConnector
from bec_widgets.utils.bec_widget import BECWidget
module = importlib.import_module(module_name)
for name, obj in inspect.getmembers(module, inspect.isclass):
if obj.__module__ != module.__name__:
directory = os.path.dirname(anchor_module.__file__)
for root, _, files in sorted(os.walk(directory)):
for file in files:
if not file.endswith(".py") or file.startswith("__"):
continue
class_info = BECClassInfo(name=name, module=module.__name__, file=path, obj=obj)
if issubclass(obj, BECConnector):
class_info.is_connector = True
if issubclass(obj, QWidget) or issubclass(obj, BECWidget):
class_info.is_widget = True
if hasattr(obj, "PLUGIN") and obj.PLUGIN:
class_info.is_plugin = True
collection.add_class(class_info)
path = os.path.join(root, file)
rel_dir = os.path.dirname(os.path.relpath(path, directory))
if rel_dir in ("", "."):
module_name = file.split(".")[0]
else:
module_name = ".".join(rel_dir.split(os.sep) + [file.split(".")[0]])
module = importlib.import_module(f"{repo_name}.{package}.{module_name}")
for name in dir(module):
obj = getattr(module, name)
if not hasattr(obj, "__module__") or obj.__module__ != module.__name__:
continue
if isinstance(obj, type):
class_info = BECClassInfo(name=name, module=module.__name__, file=path, obj=obj)
if issubclass(obj, BECConnector):
class_info.is_connector = True
if issubclass(obj, QWidget) or issubclass(obj, BECWidget):
class_info.is_widget = True
if hasattr(obj, "PLUGIN") and obj.PLUGIN:
class_info.is_plugin = True
collection.add_class(class_info)
return collection
def _build_ast_inheritance_map(
repo_name: str, packages: tuple[str, ...]
) -> dict[str, tuple[str, set[str]]]:
"""
Walk all candidate modules in the given packages and return a map of:
class_name -> (module_name, {direct_base_names})
This is used for the transitive-closure widget discovery so that subclasses
of discovered widget bases are themselves discoverable without needing to
repeat ``PLUGIN = True`` on every intermediate or leaf class.
"""
mapping: dict[str, tuple[str, set[str]]] = {}
for package in packages:
try:
package_roots = _find_package_roots(f"{repo_name}.{package}")
except ModuleNotFoundError:
continue
for directory in package_roots:
for root, _, files in sorted(os.walk(directory)):
for file_name in sorted(files):
if (
not file_name.endswith(".py")
or file_name.startswith("__")
or file_name.startswith("register_")
or file_name.endswith("_plugin.py")
):
continue
path = os.path.join(root, file_name)
rel_dir = os.path.dirname(os.path.relpath(path, directory))
module_name = (
file_name.removesuffix(".py")
if rel_dir in ("", ".")
else ".".join(rel_dir.split(os.sep) + [file_name.removesuffix(".py")])
)
full_module = f"{repo_name}.{package}.{module_name}"
with open(path, encoding="utf-8") as fh:
tree = ast.parse(fh.read(), filename=path)
for node in tree.body:
if not isinstance(node, ast.ClassDef):
continue
base_names = {_ast_node_name(b) for b in node.bases} - {None}
mapping[node.name] = (full_module, base_names) # type: ignore[arg-type]
return mapping
@lru_cache(maxsize=32)
def _cached_custom_class_references(
repo_name: str, packages: tuple[str, ...]
) -> tuple[BECClassReference, ...]:
"""Discover widget/connector class references using a transitive-closure AST scan.
The first pass identifies classes that directly inherit from
``_DISCOVERY_BASE_NAMES`` or carry explicit RPC markers (``PLUGIN``,
``USER_ACCESS``, ``RPC_CONTENT_CLASS``). Subsequent passes treat every
newly found class name as an additional base name, so subclasses of
subclasses are discovered automatically — without requiring each
intermediate class to repeat ``PLUGIN = True``.
"""
inheritance_map = _build_ast_inheritance_map(repo_name, packages)
# Seed with _class_has_rpc_markers — we need the AST nodes for that check.
# Re-parse only to identify initial RPC-marker classes; inheritance_map
# already has everything else we need.
rpc_marker_names: set[str] = set()
for package in packages:
try:
package_roots = _find_package_roots(f"{repo_name}.{package}")
except ModuleNotFoundError:
continue
for directory in package_roots:
for root, _, files in sorted(os.walk(directory)):
for file_name in sorted(files):
if (
not file_name.endswith(".py")
or file_name.startswith("__")
or file_name.startswith("register_")
or file_name.endswith("_plugin.py")
):
continue
path = os.path.join(root, file_name)
with open(path, encoding="utf-8") as fh:
tree = ast.parse(fh.read(), filename=path)
for node in tree.body:
if isinstance(node, ast.ClassDef) and _class_has_rpc_markers(node):
rpc_marker_names.add(node.name)
# Transitive closure: start with known base names + RPC-marker classes,
# then repeatedly add classes whose direct bases are already known.
known: set[str] = set(_DISCOVERY_BASE_NAMES) | rpc_marker_names
changed = True
while changed:
changed = False
for class_name, (_, bases) in inheritance_map.items():
if class_name not in known and bases & known:
known.add(class_name)
changed = True
# Build the final list of references, preserving first-seen order and
# keeping only classes that are in the inheritance map (i.e. have a module).
references: list[BECClassReference] = []
seen_names: set[str] = set()
for class_name, (module_name, bases) in inheritance_map.items():
if class_name not in known:
continue
if class_name in seen_names:
continue
# Only emit if the class is actually a candidate (not just a raw base)
if class_name in _DISCOVERY_BASE_NAMES:
continue
references.append(BECClassReference(name=class_name, module=module_name))
seen_names.add(class_name)
return tuple(references)
def get_custom_class_references(
repo_name: str, packages: tuple[str, ...] | None = None, *, use_cache: bool = True
) -> list[BECClassReference]:
selected_packages = packages or ("widgets",)
if use_cache:
return list(_cached_custom_class_references(repo_name, tuple(selected_packages)))
_cached_custom_class_references.cache_clear()
return list(_cached_custom_class_references(repo_name, tuple(selected_packages)))
def get_custom_classes(
repo_name: str, packages: tuple[str, ...] | None = None
) -> BECClassContainer:
+2 -1
View File
@@ -15,8 +15,8 @@ from qtpy.QtWidgets import QWidget
from redis.exceptions import RedisError
from bec_widgets.cli.rpc.rpc_register import RPCRegister
from bec_widgets.utils import BECDispatcher
from bec_widgets.utils.bec_connector import BECConnector
from bec_widgets.utils.bec_dispatcher import BECDispatcher
from bec_widgets.utils.container_utils import WidgetContainerUtils
from bec_widgets.utils.error_popups import ErrorPopupUtility
from bec_widgets.utils.screen_utils import apply_window_geometry
@@ -25,6 +25,7 @@ from bec_widgets.widgets.containers.main_window.main_window import BECMainWindow
if TYPE_CHECKING: # pragma: no cover
from bec_lib import messages
from qtpy.QtCore import QObject
else:
messages = lazy_import("bec_lib.messages")
logger = bec_logger.logger
+6 -6
View File
@@ -26,7 +26,7 @@ from qtpy.QtWidgets import (
from bec_widgets.widgets.utility.toggle.toggle import ToggleSwitch
if TYPE_CHECKING: # pragma: no cover
from bec_widgets.utils.bec_connector import BECConnector
from bec_widgets.utils import BECConnector
logger = bec_logger.logger
@@ -418,7 +418,7 @@ class WidgetHierarchy:
only_bec_widgets(bool, optional): Whether to print only widgets that are instances of BECWidget.
show_parent(bool, optional): Whether to display which BECWidget is the parent of each discovered BECWidget.
"""
from bec_widgets.utils.bec_connector import BECConnector
from bec_widgets.utils import BECConnector
from bec_widgets.widgets.plots.waveform.waveform import Waveform
for node in WidgetHierarchy.iter_widget_tree(
@@ -468,7 +468,7 @@ class WidgetHierarchy:
from qtpy.QtWidgets import QApplication
from bec_widgets.utils.bec_connector import BECConnector
from bec_widgets.utils import BECConnector
from bec_widgets.widgets.plots.plot_base import PlotBase
# 1) Gather ALL QWidget-based BECConnector objects
@@ -534,7 +534,7 @@ class WidgetHierarchy:
Returns:
The nearest ancestor that is a BECConnector, or None if not found.
"""
from bec_widgets.utils.bec_connector import BECConnector
from bec_widgets.utils import BECConnector
# Guard against deleted/invalid Qt wrappers
if not shb.isValid(widget):
@@ -636,7 +636,7 @@ class WidgetHierarchy:
Return all BECConnector instances whose closest BECConnector ancestor is the given widget,
including the widget itself if it is a BECConnector.
"""
from bec_widgets.utils.bec_connector import BECConnector
from bec_widgets.utils import BECConnector
connectors: list[BECConnector] = []
if isinstance(widget, BECConnector):
@@ -664,7 +664,7 @@ class WidgetHierarchy:
return None
try:
from bec_widgets.utils.bec_connector import BECConnector # local import to avoid cycles
from bec_widgets.utils import BECConnector # local import to avoid cycles
is_bec_target = False
if isinstance(ancestor_class, str):
@@ -21,7 +21,7 @@ import bec_widgets.widgets.containers.qt_ads as QtAds
from bec_widgets import BECWidget, SafeProperty, SafeSlot
from bec_widgets.applications.views.view import ViewTourSteps
from bec_widgets.cli.rpc.rpc_widget_handler import widget_handler
from bec_widgets.utils.bec_dispatcher import BECDispatcher
from bec_widgets.utils import BECDispatcher
from bec_widgets.utils.colors import apply_theme
from bec_widgets.utils.rpc_decorator import rpc_timeout
from bec_widgets.utils.toolbars.actions import (
@@ -68,7 +68,7 @@ from bec_widgets.widgets.containers.dock_area.toolbar_components.workspace_actio
from bec_widgets.widgets.containers.main_window.main_window import BECMainWindowNoRPC
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.scan_control import ScanControl
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
@@ -28,7 +28,7 @@ from qtpy.QtCore import QObject, QTimer
from qtpy.QtWidgets import QApplication, QFrame, QMainWindow, QScrollArea, QWidget
from bec_widgets import SafeProperty, SafeSlot
from bec_widgets.utils.bec_connector import BECConnector
from bec_widgets.utils import BECConnector
from bec_widgets.utils.colors import apply_theme
from bec_widgets.utils.widget_io import WidgetIO
@@ -18,10 +18,10 @@ from qtpy.QtWidgets import (
)
import bec_widgets
from bec_widgets.utils import UILoader
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.colors import apply_theme
from bec_widgets.utils.error_popups import SafeSlot
from bec_widgets.utils.ui_loader import UILoader
from bec_widgets.widgets.containers.main_window.addons.hover_widget import HoverWidget
from bec_widgets.widgets.containers.main_window.addons.notification_center.notification_banner import (
BECNotificationBroker,
@@ -1,4 +1 @@
from PySide6QtAds import *
CDockManager.setConfigFlag(CDockManager.eConfigFlag.FocusHighlighting, True)
CDockManager.setConfigFlag(CDockManager.eConfigFlag.RetainTabSizeWhenCloseButtonHidden, True)
@@ -11,9 +11,9 @@ from qtpy.QtCore import Qt, Signal
from qtpy.QtGui import QDoubleValidator
from qtpy.QtWidgets import QDoubleSpinBox
from bec_widgets.utils import UILoader
from bec_widgets.utils.colors import apply_theme, get_accent_colors
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
from bec_widgets.utils.ui_loader import UILoader
from bec_widgets.widgets.control.device_control.positioner_box.positioner_box_base import (
DeviceUpdateUIComponents,
PositionerBoxBase,
@@ -12,9 +12,9 @@ from qtpy.QtCore import Signal
from qtpy.QtGui import QDoubleValidator
from qtpy.QtWidgets import QDoubleSpinBox
from bec_widgets.utils import UILoader
from bec_widgets.utils.colors import apply_theme
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
from bec_widgets.utils.ui_loader import UILoader
from bec_widgets.widgets.control.device_control.positioner_box.positioner_box_base import (
DeviceUpdateUIComponents,
PositionerBoxBase,
@@ -7,7 +7,7 @@ from bec_lib.device import Signal as BECSignal
from bec_lib.logger import bec_logger
from pydantic import field_validator
from bec_widgets.utils.bec_connector import ConnectionConfig
from bec_widgets.utils import ConnectionConfig
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
from bec_widgets.utils.filter_io import FilterIO
@@ -3,7 +3,7 @@ from bec_lib.device import Signal
from bec_lib.logger import bec_logger
from qtpy.QtCore import Property
from bec_widgets.utils.bec_connector import ConnectionConfig
from bec_widgets.utils import ConnectionConfig
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.error_popups import SafeSlot
from bec_widgets.utils.filter_io import FilterIO
@@ -0,0 +1 @@
from .components import DeviceTable, DMConfigView, DocstringView, OphydValidation
@@ -0,0 +1,5 @@
# from .device_table_view import DeviceTableView
from .device_table.device_table import DeviceTable
from .dm_config_view import DMConfigView
from .dm_docstring_view import DocstringView, docstring_to_markdown
from .ophyd_validation.ophyd_validation import OphydValidation
@@ -0,0 +1,3 @@
from .available_device_resources import AvailableDeviceResources
__all__ = ["AvailableDeviceResources"]
@@ -22,7 +22,7 @@ from bec_widgets.utils.fuzzy_search import is_match
from bec_widgets.widgets.control.device_manager.components.device_table.device_table_row import (
DeviceTableRow,
)
from bec_widgets.widgets.control.device_manager.components.ophyd_validation.ophyd_validation_utils import (
from bec_widgets.widgets.control.device_manager.components.ophyd_validation import (
ConfigStatus,
ConnectionStatus,
get_validation_icons,
@@ -2,7 +2,7 @@
from bec_lib.atlas_models import Device as DeviceModel
from bec_widgets.widgets.control.device_manager.components.ophyd_validation.ophyd_validation_utils import (
from bec_widgets.widgets.control.device_manager.components.ophyd_validation import (
ConfigStatus,
ConnectionStatus,
)
@@ -0,0 +1,8 @@
from .ophyd_validation_utils import (
ConfigStatus,
ConnectionStatus,
DeviceTestModel,
format_error_to_md,
get_validation_icons,
)
from .validation_list_item import ValidationButton, ValidationListItem
@@ -22,16 +22,14 @@ from bec_widgets.utils.bec_list import BECList
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.colors import get_accent_colors
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
from bec_widgets.widgets.control.device_manager.components.ophyd_validation.ophyd_validation_utils import (
from bec_widgets.widgets.control.device_manager.components.ophyd_validation import (
ConfigStatus,
ConnectionStatus,
DeviceTestModel,
format_error_to_md,
get_validation_icons,
)
from bec_widgets.widgets.control.device_manager.components.ophyd_validation.validation_list_item import (
ValidationButton,
ValidationListItem,
format_error_to_md,
get_validation_icons,
)
READY_TO_TEST = False
@@ -7,7 +7,7 @@ from qtpy import QtCore, QtGui, QtWidgets
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.ophyd_validation_utils import (
from bec_widgets.widgets.control.device_manager.components.ophyd_validation import (
ConfigStatus,
ConnectionStatus,
DeviceTestModel,
@@ -0,0 +1 @@
from .scan_control import ScanControl
@@ -19,7 +19,7 @@ from qtpy.QtWidgets import (
QWidget,
)
from bec_widgets.utils.bec_connector import ConnectionConfig
from bec_widgets.utils import ConnectionConfig
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.colors import apply_theme, get_accent_colors
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
@@ -5,10 +5,10 @@ from bec_lib.logger import bec_logger
from qtpy.QtCore import Signal
from qtpy.QtWidgets import QPushButton, QSizePolicy, QTreeWidgetItem, QVBoxLayout, QWidget
from bec_widgets.utils import UILoader
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.colors import get_accent_colors
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
from bec_widgets.utils.ui_loader import UILoader
logger = bec_logger.logger
@@ -1,12 +1,10 @@
from bec_lib.utils.import_utils import lazy_import_from
from bec_ipython_client.main import BECIPythonClient
from qtconsole.inprocess import QtInProcessKernelManager
from qtconsole.manager import QtKernelManager
from qtconsole.rich_jupyter_widget import RichJupyterWidget
from qtpy.QtCore import Qt
from qtpy.QtWidgets import QApplication, QMainWindow
BECIPythonClient = lazy_import_from("bec_ipython_client.main", ("BECIPythonClient",))
class BECJupyterConsole(RichJupyterWidget): # pragma: no cover:
def __init__(self, inprocess: bool = False):
@@ -11,7 +11,7 @@ from bec_lib.logger import bec_logger
from qtpy.QtCore import QSize, Qt
from qtpy.QtWidgets import QDialog, QDialogButtonBox, QPushButton, QVBoxLayout
from bec_widgets.widgets.control.scan_control.scan_control import ScanControl
from bec_widgets.widgets.control.scan_control import ScanControl
logger = bec_logger.logger
+1 -1
View File
@@ -19,8 +19,8 @@ from scipy.interpolate import (
from scipy.spatial import cKDTree
from toolz import partition
from bec_widgets.utils import Colors
from bec_widgets.utils.bec_connector import ConnectionConfig
from bec_widgets.utils.colors import Colors
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
from bec_widgets.utils.settings_dialog import SettingsDialog
from bec_widgets.utils.toolbars.actions import MaterialIconAction
@@ -4,9 +4,9 @@ import os
from qtpy.QtWidgets import QFrame, QScrollArea, QVBoxLayout
from bec_widgets.utils import UILoader
from bec_widgets.utils.error_popups import SafeSlot
from bec_widgets.utils.settings_dialog import SettingWidget
from bec_widgets.utils.ui_loader import UILoader
class HeatmapSettings(SettingWidget):
+1 -1
View File
@@ -10,7 +10,7 @@ from pydantic import BaseModel, Field, field_validator
from qtpy.QtCore import QTimer
from qtpy.QtWidgets import QWidget
from bec_widgets.utils.bec_connector import ConnectionConfig
from bec_widgets.utils import ConnectionConfig
from bec_widgets.utils.colors import Colors, apply_theme
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
from bec_widgets.widgets.plots.image.image_base import ImageBase
@@ -9,7 +9,7 @@ from pydantic import BaseModel, ConfigDict, Field, ValidationError
from qtpy.QtCore import QPointF, Signal, SignalInstance
from qtpy.QtWidgets import QDialog, QVBoxLayout
from bec_widgets.utils.colors import Colors
from bec_widgets.utils import Colors
from bec_widgets.utils.container_utils import WidgetContainerUtils
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
from bec_widgets.utils.side_panel import SidePanel
@@ -9,8 +9,7 @@ from pydantic import Field, ValidationError, field_validator
from qtpy.QtCore import Signal
from qtpy.QtGui import QTransform
from bec_widgets.utils.bec_connector import BECConnector, ConnectionConfig
from bec_widgets.utils.colors import Colors
from bec_widgets.utils import BECConnector, Colors, ConnectionConfig
from bec_widgets.widgets.plots.image.image_processor import (
ImageProcessor,
ImageStats,
@@ -20,8 +20,7 @@ from qtpy.QtWidgets import (
)
from bec_widgets import BECWidget
from bec_widgets.utils.bec_connector import ConnectionConfig
from bec_widgets.utils.bec_dispatcher import BECDispatcher
from bec_widgets.utils import BECDispatcher, ConnectionConfig
from bec_widgets.utils.toolbars.actions import WidgetAction
from bec_widgets.utils.toolbars.bundles import ToolbarBundle
from bec_widgets.utils.toolbars.toolbar import MaterialIconAction, ModularToolBar
@@ -10,8 +10,8 @@ from qtpy.QtCore import Signal
from qtpy.QtGui import QColor
from qtpy.QtWidgets import QHBoxLayout, QMainWindow, QWidget
from bec_widgets.utils.bec_connector import ConnectionConfig
from bec_widgets.utils.colors import Colors, apply_theme
from bec_widgets.utils import Colors, ConnectionConfig
from bec_widgets.utils.colors import apply_theme
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
from bec_widgets.utils.settings_dialog import SettingsDialog
from bec_widgets.utils.toolbars.toolbar import MaterialIconAction
@@ -2,9 +2,9 @@ import os
from qtpy.QtWidgets import QFrame, QScrollArea, QVBoxLayout, QWidget
from bec_widgets.utils import UILoader
from bec_widgets.utils.error_popups import SafeSlot
from bec_widgets.utils.settings_dialog import SettingWidget
from bec_widgets.utils.ui_loader import UILoader
from bec_widgets.utils.widget_io import WidgetIO
@@ -10,8 +10,7 @@ from pydantic import Field, ValidationError, field_validator
from qtpy.QtCore import Signal
from qtpy.QtWidgets import QWidget
from bec_widgets.utils.bec_connector import ConnectionConfig
from bec_widgets.utils.colors import Colors
from bec_widgets.utils import Colors, ConnectionConfig
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
from bec_widgets.utils.side_panel import SidePanel
from bec_widgets.widgets.control.device_input.device_combobox.device_combobox import DeviceComboBox
@@ -2,9 +2,9 @@ import os
from qtpy.QtWidgets import QVBoxLayout, QWidget
from bec_widgets.utils import UILoader
from bec_widgets.utils.error_popups import SafeSlot
from bec_widgets.utils.settings_dialog import SettingWidget
from bec_widgets.utils.ui_loader import UILoader
from bec_widgets.utils.widget_io import WidgetIO
+1 -3
View File
@@ -8,10 +8,8 @@ from bec_lib import bec_logger
from qtpy.QtCore import QPoint, QPointF, Qt, Signal
from qtpy.QtWidgets import QHBoxLayout, QLabel, QMainWindow, QVBoxLayout, QWidget
from bec_widgets.utils.bec_connector import ConnectionConfig
from bec_widgets.utils import ConnectionConfig, Crosshair, EntryValidator
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.crosshair import Crosshair
from bec_widgets.utils.entry_validator import EntryValidator
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
from bec_widgets.utils.fps_counter import FPSCounter
from bec_widgets.utils.plot_indicator_items import BECArrowItem, BECTickItem
+1 -1
View File
@@ -10,7 +10,7 @@ from qtpy import QtCore
from qtpy.QtCore import QObject, Signal
from bec_widgets import SafeProperty
from bec_widgets.utils.bec_connector import BECConnector, ConnectionConfig
from bec_widgets.utils import BECConnector, ConnectionConfig
from bec_widgets.utils.colors import Colors
if TYPE_CHECKING:
@@ -8,8 +8,7 @@ from bec_lib import bec_logger
from pydantic import BaseModel, Field, ValidationError, field_validator
from qtpy import QtCore
from bec_widgets.utils.bec_connector import BECConnector, ConnectionConfig
from bec_widgets.utils.colors import Colors
from bec_widgets.utils import BECConnector, Colors, ConnectionConfig
if TYPE_CHECKING: # pragma: no cover
from bec_widgets.widgets.plots.scatter_waveform.scatter_waveform import ScatterWaveform
@@ -7,8 +7,7 @@ from pydantic import Field, ValidationError, field_validator
from qtpy.QtCore import QTimer, Signal
from qtpy.QtWidgets import QHBoxLayout, QMainWindow, QWidget
from bec_widgets.utils.bec_connector import ConnectionConfig
from bec_widgets.utils.colors import Colors
from bec_widgets.utils import Colors, ConnectionConfig
from bec_widgets.utils.error_popups import SafeProperty, SafeSlot
from bec_widgets.utils.settings_dialog import SettingsDialog
from bec_widgets.utils.toolbars.toolbar import MaterialIconAction
@@ -2,9 +2,9 @@ import os
from qtpy.QtWidgets import QFrame, QScrollArea, QVBoxLayout
from bec_widgets.utils import UILoader
from bec_widgets.utils.error_popups import SafeSlot
from bec_widgets.utils.settings_dialog import SettingWidget
from bec_widgets.utils.ui_loader import UILoader
class ScatterCurveSettings(SettingWidget):
@@ -2,9 +2,9 @@ import os
from qtpy.QtWidgets import QFrame, QScrollArea, QVBoxLayout, QWidget
from bec_widgets.utils import UILoader
from bec_widgets.utils.error_popups import SafeSlot
from bec_widgets.utils.settings_dialog import SettingWidget
from bec_widgets.utils.ui_loader import UILoader
from bec_widgets.utils.widget_io import WidgetIO
+1 -2
View File
@@ -8,8 +8,7 @@ from bec_lib import bec_logger
from pydantic import BaseModel, Field, field_validator
from qtpy import QtCore
from bec_widgets.utils.bec_connector import BECConnector, ConnectionConfig
from bec_widgets.utils.colors import Colors
from bec_widgets.utils import BECConnector, Colors, ConnectionConfig
if TYPE_CHECKING: # pragma: no cover
from bec_widgets.widgets.plots.waveform.waveform import Waveform
@@ -50,10 +50,9 @@ from qtpy.QtWidgets import (
)
from bec_widgets import SafeSlot
from bec_widgets.utils.bec_connector import ConnectionConfig
from bec_widgets.utils import ConnectionConfig, EntryValidator
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.colors import Colors
from bec_widgets.utils.entry_validator import EntryValidator
from bec_widgets.utils.toolbars.actions import WidgetAction
from bec_widgets.utils.toolbars.bundles import ToolbarBundle
from bec_widgets.utils.toolbars.toolbar import MaterialIconAction, ModularToolBar
@@ -25,7 +25,7 @@ from qtpy.QtWidgets import (
QWidget,
)
from bec_widgets.utils.bec_connector import ConnectionConfig
from bec_widgets.utils import ConnectionConfig
from bec_widgets.utils.bec_signal_proxy import BECSignalProxy
from bec_widgets.utils.colors import Colors, apply_theme
from bec_widgets.utils.container_utils import WidgetContainerUtils
@@ -6,8 +6,8 @@ from bec_lib.logger import bec_logger
from qtpy.QtCore import QPointF, QSize, Qt
from qtpy.QtWidgets import QHBoxLayout, QLabel, QVBoxLayout, QWidget
from bec_widgets.utils import Colors
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.colors import Colors
from bec_widgets.utils.error_popups import SafeProperty
from bec_widgets.utils.settings_dialog import SettingsDialog
from bec_widgets.utils.toolbars.actions import MaterialIconAction
@@ -19,9 +19,9 @@ from qtpy.QtWidgets import (
QWidget,
)
from bec_widgets.utils import UILoader
from bec_widgets.utils.error_popups import SafeSlot
from bec_widgets.utils.settings_dialog import SettingWidget
from bec_widgets.utils.ui_loader import UILoader
from bec_widgets.widgets.progress.ring_progress_bar.ring import Ring
from bec_widgets.widgets.utility.visual.colormap_widget.colormap_widget import BECColorMapWidget
@@ -17,10 +17,10 @@ from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.error_popups import SafeSlot
from bec_widgets.utils.list_of_expandable_frames import ListOfExpandableFrames
from bec_widgets.utils.ui_loader import UILoader
from bec_widgets.widgets.services.device_browser.device_item import DeviceItem
from bec_widgets.widgets.services.device_browser.device_item.device_config_dialog import (
DirectUpdateDeviceConfigDialog,
)
from bec_widgets.widgets.services.device_browser.device_item.device_item import DeviceItem
from bec_widgets.widgets.services.device_browser.util import map_device_type_to_icon
logger = bec_logger.logger
@@ -0,0 +1 @@
from .device_item import DeviceItem
@@ -0,0 +1,5 @@
from .scan_history_device_viewer import ScanHistoryDeviceViewer
from .scan_history_metadata_viewer import ScanHistoryMetadataViewer
from .scan_history_view import ScanHistoryView
__all__ = ["ScanHistoryDeviceViewer", "ScanHistoryMetadataViewer", "ScanHistoryView"]
@@ -330,10 +330,8 @@ class ScanHistoryView(BECWidget, QtWidgets.QTreeWidget):
if __name__ == "__main__": # pragma: no cover
# pylint: disable=import-outside-toplevel
from bec_widgets.widgets.services.scan_history_browser.components.scan_history_device_viewer import (
from bec_widgets.widgets.services.scan_history_browser.components import (
ScanHistoryDeviceViewer,
)
from bec_widgets.widgets.services.scan_history_browser.components.scan_history_metadata_viewer import (
ScanHistoryMetadataViewer,
)
from bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button import DarkModeButton
@@ -1,13 +1,9 @@
from qtpy import QtCore, QtWidgets
from bec_widgets.utils.bec_widget import BECWidget, ConnectionConfig
from bec_widgets.widgets.services.scan_history_browser.components.scan_history_device_viewer import (
from bec_widgets.widgets.services.scan_history_browser.components import (
ScanHistoryDeviceViewer,
)
from bec_widgets.widgets.services.scan_history_browser.components.scan_history_metadata_viewer import (
ScanHistoryMetadataViewer,
)
from bec_widgets.widgets.services.scan_history_browser.components.scan_history_view import (
ScanHistoryView,
)
@@ -3,7 +3,8 @@ from qtpy import QtCore, QtGui
from qtpy.QtCore import Property, Signal, Slot
from qtpy.QtWidgets import QSizePolicy, QVBoxLayout, QWidget
from bec_widgets.utils.colors import Colors
from bec_widgets.utils import Colors
from bec_widgets.utils.bec_widget import BECWidget
class RoundedColorMapButton(ColorMapButton):
@@ -19,7 +19,7 @@ from qtpy.QtWidgets import (
QWidget,
)
from bec_widgets.utils.bec_connector import BECConnector
from bec_widgets.utils import BECConnector
from bec_widgets.utils.widget_highlighter import WidgetHighlighter
from bec_widgets.utils.widget_io import WidgetHierarchy
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
# BENCHMARK_TITLE: Import bec_widgets
set -euo pipefail
python -c 'import bec_widgets; print(bec_widgets.__file__)'
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
# BENCHMARK_TITLE: BEC IPython client with companion app
set -euo pipefail
bec --post-startup-file tests/benchmarks/hyperfine/utils/exit_bec_startup.py
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
# BENCHMARK_TITLE: BEC IPython client without companion app
set -euo pipefail
bec --nogui --post-startup-file tests/benchmarks/hyperfine/utils/exit_bec_startup.py
@@ -0,0 +1,5 @@
import time
_ip = get_ipython()
_ip.confirm_exit = False
_ip.ask_exit()
+1 -1
View File
@@ -5,7 +5,7 @@ import random
import pytest
from bec_widgets.cli.client_utils import BECGuiClient
from bec_widgets.widgets.control.scan_control.scan_control import ScanControl
from bec_widgets.widgets.control.scan_control import ScanControl
# pylint: disable=unused-argument
# pylint: disable=redefined-outer-name
+7
View File
@@ -89,6 +89,13 @@ def test_available_widgets(qtbot, connected_client_gui_obj):
# Skip private attributes
if object_name.startswith("_"):
continue
# Skip BECShell as ttyd is not installed
if object_name == "BECShell":
continue
# Skip BecConsole as ttyd is not installed
if object_name == "BecConsole":
continue
#############################
######### Add widget ########
+1 -1
View File
@@ -10,7 +10,7 @@ except ImportError:
from qtpy.QtWidgets import QGridLayout
from bec_widgets.utils.widget_io import WidgetIO
from bec_widgets.widgets.control.scan_control.scan_control import ScanControl
from bec_widgets.widgets.control.scan_control import ScanControl
@pytest.fixture(scope="function")
@@ -0,0 +1,27 @@
from __future__ import annotations
import pytest
from bec_widgets.widgets.containers.dock_area.dock_area import BECDockArea
from bec_widgets.widgets.plots.waveform.waveform import Waveform
from tests.unit_tests.client_mocks import mocked_client
@pytest.fixture
def dock_area(qtbot, mocked_client):
widget = BECDockArea(client=mocked_client)
qtbot.addWidget(widget)
qtbot.waitExposed(widget)
yield widget
def test_add_waveform_to_dock_area(benchmark, dock_area, qtbot, mocked_client):
"""Benchmark adding a Waveform widget to an existing dock area."""
def add_waveform():
dock_area.new("Waveform")
return dock_area
dock = benchmark(add_waveform)
assert dock is not None
+1 -1
View File
@@ -5,7 +5,7 @@ import pytest
from qtpy.QtCore import QObject
from qtpy.QtWidgets import QApplication, QWidget
from bec_widgets.utils.bec_connector import BECConnector
from bec_widgets.utils import BECConnector
from bec_widgets.utils.error_popups import SafeProperty
from bec_widgets.utils.error_popups import SafeSlot as Slot
+4 -5
View File
@@ -4,13 +4,12 @@ from pydantic import ValidationError
from qtpy.QtGui import QColor
from qtpy.QtWidgets import QVBoxLayout, QWidget
from bec_widgets.utils.bec_connector import ConnectionConfig
from bec_widgets.utils import Colors, ConnectionConfig
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.colors import Colors, apply_theme
from bec_widgets.utils.colors import apply_theme
from bec_widgets.widgets.plots.waveform.curve import CurveConfig
from .client_mocks import mocked_client
from .conftest import create_widget
from tests.unit_tests.client_mocks import mocked_client
from tests.unit_tests.conftest import create_widget
def test_color_validation_CSS():
+3 -3
View File
@@ -4,11 +4,11 @@ import pytest
from qtpy.QtCore import QPointF, Qt
from qtpy.QtGui import QTransform
from bec_widgets.utils.crosshair import Crosshair
from bec_widgets.utils import Crosshair
from bec_widgets.widgets.plots.image.image_item import ImageItem
from bec_widgets.widgets.plots.waveform.waveform import Waveform
from tests.unit_tests.client_mocks import mocked_client
from .client_mocks import mocked_client
from .conftest import create_widget
# pylint: disable = redefined-outer-name
@@ -214,7 +214,7 @@ def test_crosshair_clicked_signal(qtbot, plot_widget_with_crosshair):
pos_in_widget = graphics_view.mapFromScene(pos_in_scene)
# Simulate mouse click
qtbot.mouseClick(graphics_view.viewport(), Qt.MouseButton.LeftButton, pos=pos_in_widget)
qtbot.mouseClick(graphics_view.viewport(), Qt.LeftButton, pos=pos_in_widget)
x, y = emitted_positions[0]
+2 -3
View File
@@ -12,9 +12,8 @@ from bec_widgets.widgets.plots.waveform.settings.curve_settings.curve_tree impor
ScanIndexValidator,
)
from bec_widgets.widgets.plots.waveform.waveform import Waveform
from .client_mocks import dap_plugin_message, mocked_client, mocked_client_with_dap
from .conftest import create_widget
from tests.unit_tests.client_mocks import dap_plugin_message, mocked_client, mocked_client_with_dap
from tests.unit_tests.conftest import create_widget
##################################################
# CurveSetting
+1 -1
View File
@@ -19,7 +19,7 @@ from .client_mocks import mocked_client
if TYPE_CHECKING: # pragma: no cover
from qtpy.QtWidgets import QListWidgetItem
from bec_widgets.widgets.services.device_browser.device_item.device_item import DeviceItem
from bec_widgets.widgets.services.device_browser.device_item import DeviceItem
# pylint: disable=no-member
@@ -16,6 +16,8 @@ from qtpy import QtCore, QtGui, QtWidgets
from bec_widgets.utils.bec_list import BECList
from bec_widgets.utils.colors import get_accent_colors
from bec_widgets.widgets.control.device_manager import DeviceTable, DMConfigView, DocstringView
from bec_widgets.widgets.control.device_manager.components import docstring_to_markdown
from bec_widgets.widgets.control.device_manager.components.constants import HEADERS_HELP_MD
from bec_widgets.widgets.control.device_manager.components.device_config_template.device_config_template import (
DeviceConfigTemplate,
@@ -32,17 +34,9 @@ from bec_widgets.widgets.control.device_manager.components.device_config_templat
ReadoutPriorityComboBox,
_try_literal_eval,
)
from bec_widgets.widgets.control.device_manager.components.device_table.device_table import (
DeviceTable,
)
from bec_widgets.widgets.control.device_manager.components.device_table.device_table_row import (
DeviceTableRow,
)
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,
docstring_to_markdown,
)
from bec_widgets.widgets.control.device_manager.components.ophyd_validation.ophyd_validation import (
DeviceTest,
LegendLabel,
+4 -3
View File
@@ -31,11 +31,12 @@ from bec_widgets.applications.views.device_manager_view.device_manager_view impo
DeviceManagerWidget,
)
from bec_widgets.utils.colors import get_accent_colors
from bec_widgets.widgets.control.device_manager.components.device_table.device_table import (
from bec_widgets.widgets.control.device_manager.components import (
DeviceTable,
DMConfigView,
DocstringView,
OphydValidation,
)
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 import (
ConfigStatus,
ConnectionStatus,
+3 -1
View File
@@ -16,7 +16,9 @@ from bec_widgets.widgets.plots.heatmap.heatmap import (
)
# pytest: disable=unused-import
from .client_mocks import create_dummy_scan_item, mocked_client
from tests.unit_tests.client_mocks import mocked_client
from .client_mocks import create_dummy_scan_item
@pytest.fixture
+5 -10
View File
@@ -7,9 +7,8 @@ from qtpy.QtCore import QPointF, Qt
from bec_widgets.widgets.plots.image.image import Image
from bec_widgets.widgets.plots.image.setting_widgets.image_roi_tree import ROIPropertyTree
from bec_widgets.widgets.plots.roi.image_roi import CircularROI, RectangularROI
from .client_mocks import mocked_client
from .conftest import create_widget
from tests.unit_tests.client_mocks import mocked_client
from tests.unit_tests.conftest import create_widget
@pytest.fixture
@@ -45,7 +44,7 @@ def test_initialization(roi_tree, image_widget):
assert roi_tree.plot == image_widget.plot_item
assert roi_tree.controller == image_widget.roi_controller
assert isinstance(roi_tree.roi_items, dict)
assert len(roi_tree.tree.findItems("", Qt.MatchFlag.MatchContains)) == 0 # Empty tree initially
assert len(roi_tree.tree.findItems("", Qt.MatchContains)) == 0 # Empty tree initially
# Check toolbar actions
assert roi_tree.toolbar.components.get_action("roi_rectangle")
@@ -67,16 +66,12 @@ def test_controller_connection(roi_tree, image_widget):
# Verify that ROI was added to the tree
assert roi in roi_tree.roi_items
assert (
len(roi_tree.tree.findItems("test_roi", Qt.MatchFlag.MatchExactly, roi_tree.COL_ROI)) == 1
)
assert len(roi_tree.tree.findItems("test_roi", Qt.MatchExactly, roi_tree.COL_ROI)) == 1
# Remove ROI via controller and check that it's removed from the tree
image_widget.remove_roi(0)
assert roi not in roi_tree.roi_items
assert (
len(roi_tree.tree.findItems("test_roi", Qt.MatchFlag.MatchExactly, roi_tree.COL_ROI)) == 0
)
assert len(roi_tree.tree.findItems("test_roi", Qt.MatchExactly, roi_tree.COL_ROI)) == 0
def test_expand_collapse_tree(roi_tree, image_widget):
+2 -3
View File
@@ -12,9 +12,8 @@ from bec_widgets.widgets.plots.roi.image_roi import (
RectangularROI,
ROIController,
)
from .client_mocks import mocked_client
from .conftest import create_widget
from tests.unit_tests.client_mocks import mocked_client
from tests.unit_tests.conftest import create_widget
@pytest.fixture(params=["rect", "circle", "ellipse"])
+2 -3
View File
@@ -5,9 +5,8 @@ from bec_lib.endpoints import MessageEndpoints
from qtpy.QtCore import QPointF
from bec_widgets.widgets.plots.image.image import Image
from .client_mocks import mocked_client
from .conftest import create_widget
from tests.unit_tests.client_mocks import mocked_client
from tests.unit_tests.conftest import create_widget
##################################################
# Image widget base functionality tests
+1 -1
View File
@@ -1,8 +1,8 @@
from qtpy.QtTest import QSignalSpy
from bec_widgets.widgets.plots.motor_map.motor_map import MotorMap
from tests.unit_tests.client_mocks import mocked_client
from .client_mocks import mocked_client
from .conftest import create_widget
@@ -1,8 +1,8 @@
import numpy as np
from bec_widgets.widgets.plots.multi_waveform.multi_waveform import MultiWaveform
from tests.unit_tests.client_mocks import mocked_client
from .client_mocks import mocked_client
from .conftest import create_widget
##################################################
+1 -13
View File
@@ -1,6 +1,4 @@
import sys
from bec_widgets.utils.plugin_utils import get_custom_class_references, get_custom_classes
from bec_widgets.utils.plugin_utils import get_custom_classes
def test_client_generator_classes():
@@ -12,13 +10,3 @@ def test_client_generator_classes():
assert "Waveform" in connector_cls_names
assert "MotorMap" in plugins
assert "NonExisting" not in plugins
def test_get_custom_class_references_avoids_importing_widget_modules():
target_module = "bec_widgets.widgets.plots.image.image"
sys.modules.pop(target_module, None)
references = get_custom_class_references("bec_widgets", use_cache=False)
assert "Image" in [reference.name for reference in references]
assert target_module not in sys.modules
+1 -1
View File
@@ -8,7 +8,7 @@ from qtpy.QtCore import QEvent, QPoint, QPointF, Qt
from qtpy.QtGui import QColor, QMouseEvent
from qtpy.QtWidgets import QApplication
from bec_widgets.utils.colors import Colors
from bec_widgets.utils import Colors
from bec_widgets.widgets.progress.ring_progress_bar.ring_progress_bar import (
RingProgressBar,
RingProgressContainerWidget,
@@ -3,8 +3,7 @@ import pytest
from bec_widgets.utils.settings_dialog import SettingsDialog
from bec_widgets.widgets.progress.ring_progress_bar.ring_progress_bar import RingProgressBar
from bec_widgets.widgets.progress.ring_progress_bar.ring_progress_settings_cards import RingSettings
from .client_mocks import mocked_client
from tests.unit_tests.client_mocks import mocked_client
@pytest.fixture
+16 -36
View File
@@ -1,9 +1,8 @@
import sys
from types import ModuleType
from unittest.mock import patch
from bec_widgets.cli.rpc.rpc_widget_handler import RPCWidgetHandler
from bec_widgets.utils.plugin_utils import BECClassReference
from bec_widgets.utils.bec_widget import BECWidget
from bec_widgets.utils.plugin_utils import BECClassContainer, BECClassInfo
def test_rpc_widget_handler():
@@ -13,38 +12,19 @@ def test_rpc_widget_handler():
assert "BECDockArea" in handler.widget_classes
def test_duplicate_plugins_not_allowed(monkeypatch):
builtin_module = ModuleType("builtin_test_widgets")
plugin_module = ModuleType("plugin_test_widgets")
class _TestPluginWidget(BECWidget): ...
SharedWidgetBuiltin = type("SharedWidget", (), {})
SharedWidgetBuiltin.__module__ = builtin_module.__name__
setattr(builtin_module, "SharedWidget", SharedWidgetBuiltin)
SharedWidgetPlugin = type("SharedWidget", (), {})
SharedWidgetPlugin.__module__ = plugin_module.__name__
setattr(plugin_module, "SharedWidget", SharedWidgetPlugin)
NewPluginWidget = type("NewPluginWidget", (), {})
NewPluginWidget.__module__ = plugin_module.__name__
setattr(plugin_module, "NewPluginWidget", NewPluginWidget)
monkeypatch.setitem(sys.modules, builtin_module.__name__, builtin_module)
monkeypatch.setitem(sys.modules, plugin_module.__name__, plugin_module)
with (
patch(
"bec_widgets.cli.rpc.rpc_widget_handler.get_all_plugin_widget_references",
return_value=[
BECClassReference(name="SharedWidget", module=plugin_module.__name__),
BECClassReference(name="NewPluginWidget", module=plugin_module.__name__),
],
),
patch(
"bec_widgets.cli.rpc.rpc_widget_handler.get_custom_class_references",
return_value=[BECClassReference(name="SharedWidget", module=builtin_module.__name__)],
),
):
handler = RPCWidgetHandler()
assert handler.widget_classes["SharedWidget"].__module__ == builtin_module.__name__
assert handler.widget_classes["NewPluginWidget"].__module__ == plugin_module.__name__
@patch(
"bec_widgets.cli.rpc.rpc_widget_handler.get_all_plugin_widgets",
return_value=BECClassContainer(
[
BECClassInfo(name="DeviceComboBox", obj=_TestPluginWidget, module="", file=""),
BECClassInfo(name="NewPluginWidget", obj=_TestPluginWidget, module="", file=""),
]
),
)
def test_duplicate_plugins_not_allowed(_):
handler = RPCWidgetHandler()
assert handler.widget_classes["DeviceComboBox"] is not _TestPluginWidget
assert handler.widget_classes["NewPluginWidget"] is _TestPluginWidget
+1 -1
View File
@@ -9,7 +9,7 @@ from qtpy.QtCore import QModelIndex, Qt
from bec_widgets.utils.forms_from_types.items import StrFormItem
from bec_widgets.utils.widget_io import WidgetIO
from bec_widgets.widgets.control.scan_control.scan_control import ScanControl
from bec_widgets.widgets.control.scan_control import ScanControl
from .client_mocks import mocked_client
@@ -6,13 +6,9 @@ from pytestqt import qtbot
from qtpy import QtCore
from bec_widgets.utils.colors import get_accent_colors
from bec_widgets.widgets.services.scan_history_browser.components.scan_history_device_viewer import (
from bec_widgets.widgets.services.scan_history_browser.components import (
ScanHistoryDeviceViewer,
)
from bec_widgets.widgets.services.scan_history_browser.components.scan_history_metadata_viewer import (
ScanHistoryMetadataViewer,
)
from bec_widgets.widgets.services.scan_history_browser.components.scan_history_view import (
ScanHistoryView,
)
from bec_widgets.widgets.services.scan_history_browser.scan_history_browser import (
+1 -1
View File
@@ -10,8 +10,8 @@ from bec_widgets.widgets.plots.scatter_waveform.scatter_waveform import ScatterW
from bec_widgets.widgets.plots.scatter_waveform.settings.scatter_curve_setting import (
ScatterCurveSettings,
)
from tests.unit_tests.client_mocks import create_dummy_scan_item, mocked_client
from .client_mocks import create_dummy_scan_item, mocked_client
from .conftest import create_widget
+2 -2
View File
@@ -18,8 +18,7 @@ from bec_widgets.widgets.plots.waveform.waveform import Waveform
from bec_widgets.widgets.services.scan_history_browser.scan_history_browser import (
ScanHistoryBrowser,
)
from .client_mocks import (
from tests.unit_tests.client_mocks import (
DummyData,
create_dummy_scan_item,
dap_plugin_message,
@@ -27,6 +26,7 @@ from .client_mocks import (
mocked_client,
mocked_client_with_dap,
)
from .conftest import create_widget
# pylint: disable=unexpected-keyword-arg