mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-04-09 18:20:55 +02:00
Compare commits
1 Commits
docs/badge
...
fix/wavefo
| Author | SHA1 | Date | |
|---|---|---|---|
| 15eebd500b |
15
.github/workflows/ci.yml
vendored
15
.github/workflows/ci.yml
vendored
@@ -1,15 +0,0 @@
|
||||
name: Full CI
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
formatter:
|
||||
uses: ./.github/workflows/formatter.yml
|
||||
|
||||
unit-test:
|
||||
uses: ./.github/workflows/pytest.yml
|
||||
|
||||
unit-test-matrix:
|
||||
uses: ./.github/workflows/pytest-matrix.yml
|
||||
|
||||
end2end-test:
|
||||
uses: ./.github/workflows/end2end-conda.yml
|
||||
48
.github/workflows/end2end-conda.yml
vendored
48
.github/workflows/end2end-conda.yml
vendored
@@ -1,48 +0,0 @@
|
||||
name: Run Pytest with Coverage
|
||||
on: [workflow_call]
|
||||
|
||||
jobs:
|
||||
pytest:
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
shell: bash -el {0}
|
||||
|
||||
env:
|
||||
CHILD_PIPELINE_BRANCH: main # Set the branch you want for ophyd_devices
|
||||
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:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Conda
|
||||
uses: conda-incubator/setup-miniconda@v3
|
||||
with:
|
||||
auto-update-conda: true
|
||||
auto-activate-base: true
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgl1 libegl1 x11-utils libxkbcommon-x11-0 libdbus-1-3 xvfb
|
||||
sudo apt-get -y install libnss3 libxdamage1 libasound2t64 libatomic1 libxcursor1
|
||||
|
||||
- name: Conda install and run pytest
|
||||
run: |
|
||||
echo -e "\033[35;1m Using branch $BEC_CORE_BRANCH of BEC CORE \033[0;m";
|
||||
git clone --branch $BEC_CORE_BRANCH https://gitlab.psi.ch/bec/bec.git
|
||||
echo -e "\033[35;1m Using branch $OPHYD_DEVICES_BRANCH of OPHYD_DEVICES \033[0;m";
|
||||
git clone --branch $OPHYD_DEVICES_BRANCH https://gitlab.psi.ch/bec/ophyd_devices.git
|
||||
export OHPYD_DEVICES_PATH=$PWD/ophyd_devices
|
||||
cd ./bec
|
||||
conda create -q -n test-environment python=3.11
|
||||
source ./bin/install_bec_dev.sh -t
|
||||
cd ../
|
||||
pip install -e ./ophyd_devices
|
||||
pip install -e .[dev,pyside6]
|
||||
pytest -v --files-path ./ --start-servers --random-order ./tests/end-2-end
|
||||
61
.github/workflows/formatter.yml
vendored
61
.github/workflows/formatter.yml
vendored
@@ -1,61 +0,0 @@
|
||||
name: Formatter and Pylint jobs
|
||||
on: [workflow_call]
|
||||
jobs:
|
||||
|
||||
Formatter:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.13'
|
||||
|
||||
- name: Run black and isort
|
||||
run: |
|
||||
pip install black isort
|
||||
pip install -e .[dev]
|
||||
black --check --diff --color .
|
||||
isort --check --diff ./
|
||||
Pylint:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.13'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install pylint pylint-exit anybadge
|
||||
|
||||
- name: Run Pylint
|
||||
run: |
|
||||
mkdir -p ./pylint
|
||||
set +e
|
||||
pylint ./${{ github.event.repository.name }} --output-format=text > ./pylint/pylint.log
|
||||
pylint-exit $?
|
||||
set -e
|
||||
|
||||
- name: Extract Pylint Score
|
||||
id: score
|
||||
run: |
|
||||
SCORE=$(sed -n 's/^Your code has been rated at \([-0-9.]*\)\/.*/\1/p' ./pylint/pylint.log)
|
||||
echo "score=$SCORE" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Create Badge
|
||||
run: |
|
||||
anybadge --label=Pylint --file=./pylint/pylint.svg --value="${{ steps.score.outputs.score }}" 2=red 4=orange 8=yellow 10=green
|
||||
|
||||
- name: Upload Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: pylint-artifacts
|
||||
path: |
|
||||
# ./pylint/pylint.log # not sure why this isn't working
|
||||
./pylint/pylint.svg
|
||||
48
.github/workflows/pytest-matrix.yml
vendored
48
.github/workflows/pytest-matrix.yml
vendored
@@ -1,48 +0,0 @@
|
||||
name: Run Pytest with different Python versions
|
||||
on: [workflow_call]
|
||||
|
||||
jobs:
|
||||
pytest-matrix:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.10", "3.11", "3.12"]
|
||||
|
||||
env:
|
||||
CHILD_PIPELINE_BRANCH: main # Set the branch you want for ophyd_devices
|
||||
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:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgl1 libegl1 x11-utils libxkbcommon-x11-0 libdbus-1-3 xvfb
|
||||
sudo apt-get -y install libnss3 libxdamage1 libasound2t64 libatomic1 libxcursor1
|
||||
|
||||
- name: Clone and install dependencies
|
||||
run: |
|
||||
echo -e "\033[35;1m Using branch $BEC_CORE_BRANCH of BEC CORE \033[0;m";
|
||||
git clone --branch $BEC_CORE_BRANCH https://gitlab.psi.ch/bec/bec.git
|
||||
echo -e "\033[35;1m Using branch $OPHYD_DEVICES_BRANCH of OPHYD_DEVICES \033[0;m";
|
||||
git clone --branch $OPHYD_DEVICES_BRANCH https://gitlab.psi.ch/bec/ophyd_devices.git
|
||||
export OHPYD_DEVICES_PATH=$PWD/ophyd_devices
|
||||
pip install -e ./ophyd_devices
|
||||
pip install -e ./bec/bec_lib[dev]
|
||||
pip install -e ./bec/bec_ipython_client
|
||||
pip install -e .[dev,pyside6]
|
||||
|
||||
- name: Run Pytest
|
||||
run: |
|
||||
pip install pytest pytest-random-order
|
||||
pytest -v --maxfail=2 --junitxml=report.xml --random-order ./tests/unit_tests
|
||||
47
.github/workflows/pytest.yml
vendored
47
.github/workflows/pytest.yml
vendored
@@ -1,47 +0,0 @@
|
||||
name: Run Pytest with Coverage
|
||||
on: [workflow_call]
|
||||
|
||||
jobs:
|
||||
pytest:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
env:
|
||||
CHILD_PIPELINE_BRANCH: main # Set the branch you want for ophyd_devices
|
||||
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:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgl1 libegl1 x11-utils libxkbcommon-x11-0 libdbus-1-3 xvfb
|
||||
sudo apt-get -y install libnss3 libxdamage1 libasound2t64 libatomic1 libxcursor1
|
||||
|
||||
- name: Clone and install dependencies
|
||||
run: |
|
||||
echo -e "\033[35;1m Using branch $BEC_CORE_BRANCH of BEC CORE \033[0;m";
|
||||
git clone --branch $BEC_CORE_BRANCH https://gitlab.psi.ch/bec/bec.git
|
||||
echo -e "\033[35;1m Using branch $OPHYD_DEVICES_BRANCH of OPHYD_DEVICES \033[0;m";
|
||||
git clone --branch $OPHYD_DEVICES_BRANCH https://gitlab.psi.ch/bec/ophyd_devices.git
|
||||
export OHPYD_DEVICES_PATH=$PWD/ophyd_devices
|
||||
pip install -e ./ophyd_devices
|
||||
pip install -e ./bec/bec_lib[dev]
|
||||
pip install -e ./bec/bec_ipython_client
|
||||
pip install -e .[dev,pyside6]
|
||||
|
||||
- name: Run Pytest with Coverage
|
||||
run: |
|
||||
pip install coverage pytest pytest-random-order
|
||||
coverage run --source=./bec_widgets -m pytest -v --junitxml=report.xml --maxfail=2 --random-order --full-trace ./tests/unit_tests
|
||||
coverage report
|
||||
coverage xml
|
||||
29
CHANGELOG.md
29
CHANGELOG.md
@@ -1,35 +1,6 @@
|
||||
# CHANGELOG
|
||||
|
||||
|
||||
## v2.3.0 (2025-05-09)
|
||||
|
||||
### Features
|
||||
|
||||
- **bec_connector**: Ability to change object name during runtime
|
||||
([`dc151cd`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/dc151cdfe39f1f0507eeee307a35c1677ae4d8c5))
|
||||
|
||||
|
||||
## v2.2.0 (2025-05-09)
|
||||
|
||||
### Features
|
||||
|
||||
- **launcher**: Add support for launching plugin widget
|
||||
([`1fb680a`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1fb680abb40668e72007c245f32c80112466c46e))
|
||||
|
||||
### Refactoring
|
||||
|
||||
- **launch_window**: Widget tile added
|
||||
([`b9e56c9`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/b9e56c96cbae561beb893cedb7d18e9b6a7bfc76))
|
||||
|
||||
|
||||
## v2.1.3 (2025-05-07)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
- **bec-dispatcher**: Fix reference to boundmethods to avoid duplicated subscriptions
|
||||
([`cf59d31`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/cf59d311132cd1a21f1893c19cc9f2a7e45101d0))
|
||||
|
||||
|
||||
## v2.1.2 (2025-05-06)
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
@@ -1,12 +1,5 @@
|
||||
# BEC Widgets
|
||||
|
||||
|
||||
[](https://github.com/bec-project/bec_widgets/actions/workflows/ci.yml)
|
||||
[](https://pypi.org/project/bec-widgets/)
|
||||
[](./LICENSE)
|
||||
[](https://github.com/psf/black)
|
||||
|
||||
|
||||
**⚠️ Important Notice:**
|
||||
|
||||
🚨 **PyQt6 is no longer supported** due to incompatibilities with Qt Designer. Please use **PySide6** instead. 🚨
|
||||
|
||||
@@ -2,10 +2,10 @@ from __future__ import annotations
|
||||
|
||||
import os
|
||||
import xml.etree.ElementTree as ET
|
||||
from typing import TYPE_CHECKING, Callable
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from bec_lib.logger import bec_logger
|
||||
from qtpy.QtCore import Qt, Signal # type: ignore
|
||||
from qtpy.QtCore import Qt, Signal
|
||||
from qtpy.QtGui import QPainter, QPainterPath, QPixmap
|
||||
from qtpy.QtWidgets import (
|
||||
QApplication,
|
||||
@@ -21,10 +21,8 @@ from qtpy.QtWidgets import (
|
||||
|
||||
import bec_widgets
|
||||
from bec_widgets.cli.rpc.rpc_register import RPCRegister
|
||||
from bec_widgets.utils.bec_plugin_helper import get_all_plugin_widgets
|
||||
from bec_widgets.utils.container_utils import WidgetContainerUtils
|
||||
from bec_widgets.utils.error_popups import SafeSlot
|
||||
from bec_widgets.utils.name_utils import pascal_to_snake
|
||||
from bec_widgets.utils.plugin_utils import get_plugin_auto_updates
|
||||
from bec_widgets.utils.round_frame import RoundedFrame
|
||||
from bec_widgets.utils.toolbar import ModularToolBar
|
||||
@@ -37,8 +35,6 @@ from bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button import
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from qtpy.QtCore import QObject
|
||||
|
||||
from bec_widgets.utils.bec_widget import BECWidget
|
||||
|
||||
logger = bec_logger.logger
|
||||
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
|
||||
|
||||
@@ -145,7 +141,6 @@ class LaunchWindow(BECMainWindow):
|
||||
super().__init__(parent=parent, gui_id=gui_id, window_title=window_title, **kwargs)
|
||||
|
||||
self.app = QApplication.instance()
|
||||
self.tiles: dict[str, LaunchTile] = {}
|
||||
|
||||
# Toolbar
|
||||
self.dark_mode_button = DarkModeButton(parent=self, toolbar=True)
|
||||
@@ -161,105 +156,58 @@ class LaunchWindow(BECMainWindow):
|
||||
self.central_widget.layout = QHBoxLayout(self.central_widget)
|
||||
self.setCentralWidget(self.central_widget)
|
||||
|
||||
self.register_tile(
|
||||
name="dock_area",
|
||||
self.tile_dock_area = LaunchTile(
|
||||
icon_path=os.path.join(MODULE_PATH, "assets", "app_icons", "bec_widgets_icon.png"),
|
||||
top_label="Get started",
|
||||
main_label="BEC Dock Area",
|
||||
description="Highly flexible and customizable dock area application with modular widgets.",
|
||||
action_button=lambda: self.launch("dock_area"),
|
||||
show_selector=False,
|
||||
)
|
||||
self.tile_dock_area.setFixedSize(*self.TILE_SIZE)
|
||||
|
||||
self.available_auto_updates: dict[str, type[AutoUpdates]] = (
|
||||
self._update_available_auto_updates()
|
||||
)
|
||||
self.register_tile(
|
||||
name="auto_update",
|
||||
self.tile_auto_update = LaunchTile(
|
||||
icon_path=os.path.join(MODULE_PATH, "assets", "app_icons", "auto_update.png"),
|
||||
top_label="Get automated",
|
||||
main_label="BEC Auto Update Dock Area",
|
||||
description="Dock area with auto update functionality for BEC widgets plotting.",
|
||||
action_button=self._open_auto_update,
|
||||
show_selector=True,
|
||||
selector_items=list(self.available_auto_updates.keys()) + ["Default"],
|
||||
)
|
||||
self.tile_auto_update.setFixedSize(*self.TILE_SIZE)
|
||||
|
||||
self.register_tile(
|
||||
name="custom_ui_file",
|
||||
self.tile_ui_file = LaunchTile(
|
||||
icon_path=os.path.join(MODULE_PATH, "assets", "app_icons", "ui_loader_tile.png"),
|
||||
top_label="Get customized",
|
||||
main_label="Launch Custom UI File",
|
||||
description="GUI application with custom UI file.",
|
||||
action_button=self._open_custom_ui_file,
|
||||
show_selector=False,
|
||||
)
|
||||
self.tile_ui_file.setFixedSize(*self.TILE_SIZE)
|
||||
|
||||
# plugin widgets
|
||||
self.available_widgets: dict[str, BECWidget] = get_all_plugin_widgets()
|
||||
if self.available_widgets:
|
||||
plugin_repo_name = next(iter(self.available_widgets.values())).__module__.split(".")[0]
|
||||
plugin_repo_name = plugin_repo_name.removesuffix("_bec").upper()
|
||||
self.register_tile(
|
||||
name="widget",
|
||||
icon_path=os.path.join(
|
||||
MODULE_PATH, "assets", "app_icons", "widget_launch_tile.png"
|
||||
),
|
||||
top_label="Get quickly started",
|
||||
main_label=f"Launch a {plugin_repo_name} Widget",
|
||||
description=f"GUI application with one widget from the {plugin_repo_name} repository.",
|
||||
action_button=self._open_widget,
|
||||
show_selector=True,
|
||||
selector_items=list(self.available_widgets.keys()),
|
||||
)
|
||||
# Add tiles to the main layout
|
||||
self.central_widget.layout.addWidget(self.tile_dock_area)
|
||||
self.central_widget.layout.addWidget(self.tile_auto_update)
|
||||
self.central_widget.layout.addWidget(self.tile_ui_file)
|
||||
|
||||
# hacky solution no time to waste
|
||||
self.tiles = [self.tile_dock_area, self.tile_auto_update, self.tile_ui_file]
|
||||
|
||||
# Connect signals
|
||||
self.tile_dock_area.action_button.clicked.connect(lambda: self.launch("dock_area"))
|
||||
self.tile_auto_update.action_button.clicked.connect(self._open_auto_update)
|
||||
self.tile_ui_file.action_button.clicked.connect(self._open_custom_ui_file)
|
||||
self._update_theme()
|
||||
|
||||
# Auto updates
|
||||
self.available_auto_updates: dict[str, type[AutoUpdates]] = (
|
||||
self._update_available_auto_updates()
|
||||
)
|
||||
if self.tile_auto_update.selector is not None:
|
||||
self.tile_auto_update.selector.addItems(
|
||||
list(self.available_auto_updates.keys()) + ["Default"]
|
||||
)
|
||||
|
||||
self.register = RPCRegister()
|
||||
self.register.callbacks.append(self._turn_off_the_lights)
|
||||
self.register.broadcast()
|
||||
|
||||
def register_tile(
|
||||
self,
|
||||
name: str,
|
||||
icon_path: str | None = None,
|
||||
top_label: str | None = None,
|
||||
main_label: str | None = None,
|
||||
description: str | None = None,
|
||||
action_button: Callable | None = None,
|
||||
show_selector: bool = False,
|
||||
selector_items: list[str] | None = None,
|
||||
):
|
||||
"""
|
||||
Register a tile in the launcher window.
|
||||
|
||||
Args:
|
||||
name(str): The name of the tile.
|
||||
icon_path(str): The path to the icon.
|
||||
top_label(str): The top label of the tile.
|
||||
main_label(str): The main label of the tile.
|
||||
description(str): The description of the tile.
|
||||
action_button(callable): The action to be performed when the button is clicked.
|
||||
show_selector(bool): Whether to show a selector or not.
|
||||
selector_items(list[str]): The items to be shown in the selector.
|
||||
"""
|
||||
|
||||
tile = LaunchTile(
|
||||
icon_path=icon_path,
|
||||
top_label=top_label,
|
||||
main_label=main_label,
|
||||
description=description,
|
||||
show_selector=show_selector,
|
||||
)
|
||||
tile.setFixedSize(*self.TILE_SIZE)
|
||||
if action_button:
|
||||
tile.action_button.clicked.connect(action_button)
|
||||
if show_selector and selector_items:
|
||||
tile.selector.addItems(selector_items)
|
||||
self.central_widget.layout.addWidget(tile)
|
||||
|
||||
self.tiles[name] = tile
|
||||
|
||||
def launch(
|
||||
self,
|
||||
launch_script: str,
|
||||
@@ -308,12 +256,6 @@ class LaunchWindow(BECMainWindow):
|
||||
auto_update = kwargs.pop("auto_update", None)
|
||||
return self._launch_auto_update(auto_update)
|
||||
|
||||
if launch_script == "widget":
|
||||
widget = kwargs.pop("widget", None)
|
||||
if widget is None:
|
||||
raise ValueError("Widget name must be provided.")
|
||||
return self._launch_widget(widget)
|
||||
|
||||
launch = getattr(bw_launch, launch_script, None)
|
||||
if launch is None:
|
||||
raise ValueError(f"Launch script {launch_script} not found.")
|
||||
@@ -331,7 +273,6 @@ class LaunchWindow(BECMainWindow):
|
||||
else:
|
||||
window = BECMainWindow()
|
||||
window.setCentralWidget(result_widget)
|
||||
window.setWindowTitle(f"BEC - {result_widget.objectName()}")
|
||||
window.show()
|
||||
return result_widget
|
||||
|
||||
@@ -380,28 +321,11 @@ class LaunchWindow(BECMainWindow):
|
||||
window.show()
|
||||
return window
|
||||
|
||||
def _launch_widget(self, widget: type[BECWidget]) -> QWidget:
|
||||
name = pascal_to_snake(widget.__name__)
|
||||
|
||||
WidgetContainerUtils.raise_for_invalid_name(name)
|
||||
|
||||
window = BECMainWindow()
|
||||
|
||||
widget_instance = widget(root_widget=True, object_name=name)
|
||||
assert isinstance(widget_instance, QWidget)
|
||||
QApplication.processEvents()
|
||||
|
||||
window.setCentralWidget(widget_instance)
|
||||
window.resize(window.minimumSizeHint())
|
||||
window.setWindowTitle(f"BEC - {widget_instance.objectName()}")
|
||||
window.show()
|
||||
return window
|
||||
|
||||
def apply_theme(self, theme: str):
|
||||
"""
|
||||
Change the theme of the application.
|
||||
"""
|
||||
for tile in self.tiles.values():
|
||||
for tile in self.tiles:
|
||||
tile.apply_theme(theme)
|
||||
|
||||
super().apply_theme(theme)
|
||||
@@ -410,25 +334,14 @@ class LaunchWindow(BECMainWindow):
|
||||
"""
|
||||
Open the auto update window.
|
||||
"""
|
||||
if self.tiles["auto_update"].selector is None:
|
||||
if self.tile_auto_update.selector is None:
|
||||
auto_update = None
|
||||
else:
|
||||
auto_update = self.tiles["auto_update"].selector.currentText()
|
||||
auto_update = self.tile_auto_update.selector.currentText()
|
||||
if auto_update == "Default":
|
||||
auto_update = None
|
||||
return self.launch("auto_update", auto_update=auto_update)
|
||||
|
||||
def _open_widget(self):
|
||||
"""
|
||||
Open a widget from the available widgets.
|
||||
"""
|
||||
if self.tiles["widget"].selector is None:
|
||||
return
|
||||
widget = self.tiles["widget"].selector.currentText()
|
||||
if widget not in self.available_widgets:
|
||||
raise ValueError(f"Widget {widget} not found in available widgets.")
|
||||
return self.launch("widget", widget=self.available_widgets[widget])
|
||||
|
||||
@SafeSlot(popup_error=True)
|
||||
def _open_custom_ui_file(self):
|
||||
"""
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.1 MiB |
@@ -205,17 +205,6 @@ class BECConnector:
|
||||
f"This is not necessarily an error as the parent may be deleted before the child and includes already a cleanup. The following exception was raised:\n{content}"
|
||||
)
|
||||
|
||||
def change_object_name(self, name: str) -> None:
|
||||
"""
|
||||
Change the object name of the widget. Unregister old name and register the new one.
|
||||
|
||||
Args:
|
||||
name (str): The new object name.
|
||||
"""
|
||||
self.rpc_register.remove_rpc(self)
|
||||
self.setObjectName(name.replace("-", "_").replace(" ", "_"))
|
||||
QTimer.singleShot(0, self._update_object_name)
|
||||
|
||||
def _update_object_name(self) -> None:
|
||||
"""
|
||||
Enforce a unique object name among siblings and register the object for RPC.
|
||||
|
||||
@@ -4,9 +4,8 @@ import collections
|
||||
import random
|
||||
import string
|
||||
from collections.abc import Callable
|
||||
from typing import TYPE_CHECKING, DefaultDict, Hashable, Union
|
||||
from typing import TYPE_CHECKING, Union
|
||||
|
||||
import louie
|
||||
import redis
|
||||
from bec_lib.client import BECClient
|
||||
from bec_lib.logger import bec_logger
|
||||
@@ -42,25 +41,15 @@ class QtThreadSafeCallback(QObject):
|
||||
self.cb_info = cb_info
|
||||
|
||||
self.cb = cb
|
||||
self.cb_ref = louie.saferef.safe_ref(cb)
|
||||
self.cb_signal.connect(self.cb)
|
||||
self.topics = set()
|
||||
|
||||
def __hash__(self):
|
||||
# make 2 differents QtThreadSafeCallback to look
|
||||
# identical when used as dictionary keys, if the
|
||||
# callback is the same
|
||||
return f"{id(self.cb_ref)}{self.cb_info}".__hash__()
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, QtThreadSafeCallback):
|
||||
return False
|
||||
return self.cb_ref == other.cb_ref and self.cb_info == other.cb_info
|
||||
return f"{id(self.cb)}{self.cb_info}".__hash__()
|
||||
|
||||
def __call__(self, msg_content, metadata):
|
||||
if self.cb_ref() is None:
|
||||
# callback has been deleted
|
||||
return
|
||||
self.cb_signal.emit(msg_content, metadata)
|
||||
|
||||
|
||||
@@ -107,7 +96,7 @@ class BECDispatcher:
|
||||
cls,
|
||||
client=None,
|
||||
config: str | ServiceConfig | None = None,
|
||||
gui_id: str | None = None,
|
||||
gui_id: str = None,
|
||||
*args,
|
||||
**kwargs,
|
||||
):
|
||||
@@ -120,9 +109,7 @@ class BECDispatcher:
|
||||
if self._initialized:
|
||||
return
|
||||
|
||||
self._registered_slots: DefaultDict[Hashable, QtThreadSafeCallback] = (
|
||||
collections.defaultdict()
|
||||
)
|
||||
self._slots = collections.defaultdict(set)
|
||||
self.client = client
|
||||
|
||||
if self.client is None:
|
||||
@@ -175,13 +162,10 @@ class BECDispatcher:
|
||||
topics (EndpointInfo | str | list): A topic or list of topics that can typically be acquired via bec_lib.MessageEndpoints
|
||||
cb_info (dict | None): A dictionary containing information about the callback. Defaults to None.
|
||||
"""
|
||||
qt_slot = QtThreadSafeCallback(cb=slot, cb_info=cb_info)
|
||||
if qt_slot not in self._registered_slots:
|
||||
self._registered_slots[qt_slot] = qt_slot
|
||||
qt_slot = self._registered_slots[qt_slot]
|
||||
self.client.connector.register(topics, cb=qt_slot, **kwargs)
|
||||
slot = QtThreadSafeCallback(cb=slot, cb_info=cb_info)
|
||||
self.client.connector.register(topics, cb=slot, **kwargs)
|
||||
topics_str, _ = self.client.connector._convert_endpointinfo(topics)
|
||||
qt_slot.topics.update(set(topics_str))
|
||||
self._slots[slot].update(set(topics_str))
|
||||
|
||||
def disconnect_slot(self, slot: Callable, topics: Union[str, list]):
|
||||
"""
|
||||
@@ -194,16 +178,16 @@ class BECDispatcher:
|
||||
# find the right slot to disconnect from ;
|
||||
# slot callbacks are wrapped in QtThreadSafeCallback objects,
|
||||
# but the slot we receive here is the original callable
|
||||
for connected_slot in self._registered_slots.values():
|
||||
for connected_slot in self._slots:
|
||||
if connected_slot.cb == slot:
|
||||
break
|
||||
else:
|
||||
return
|
||||
self.client.connector.unregister(topics, cb=connected_slot)
|
||||
topics_str, _ = self.client.connector._convert_endpointinfo(topics)
|
||||
self._registered_slots[connected_slot].topics.difference_update(set(topics_str))
|
||||
if not self._registered_slots[connected_slot].topics:
|
||||
del self._registered_slots[connected_slot]
|
||||
self._slots[connected_slot].difference_update(set(topics_str))
|
||||
if not self._slots[connected_slot]:
|
||||
del self._slots[connected_slot]
|
||||
|
||||
def disconnect_topics(self, topics: Union[str, list]):
|
||||
"""
|
||||
@@ -214,16 +198,11 @@ class BECDispatcher:
|
||||
"""
|
||||
self.client.connector.unregister(topics)
|
||||
topics_str, _ = self.client.connector._convert_endpointinfo(topics)
|
||||
|
||||
remove_slots = []
|
||||
for connected_slot in self._registered_slots.values():
|
||||
connected_slot.topics.difference_update(set(topics_str))
|
||||
|
||||
if not connected_slot.topics:
|
||||
remove_slots.append(connected_slot)
|
||||
|
||||
for connected_slot in remove_slots:
|
||||
self._registered_slots.pop(connected_slot, None)
|
||||
for slot in list(self._slots.keys()):
|
||||
slot_topics = self._slots[slot]
|
||||
slot_topics.difference_update(set(topics_str))
|
||||
if not slot_topics:
|
||||
del self._slots[slot]
|
||||
|
||||
def disconnect_all(self, *args, **kwargs):
|
||||
"""
|
||||
|
||||
@@ -137,6 +137,7 @@ class Waveform(PlotBase):
|
||||
# Curve data
|
||||
self._sync_curves = []
|
||||
self._async_curves = []
|
||||
self._async_connected_devices: set[str] = set()
|
||||
self._slice_index = None
|
||||
self._dap_curves = []
|
||||
self._mode: Literal["none", "sync", "async", "mixed"] = "none"
|
||||
@@ -544,6 +545,7 @@ class Waveform(PlotBase):
|
||||
continue
|
||||
config = CurveConfig(**cfg_dict)
|
||||
self._add_curve(config=config)
|
||||
self.update_with_scan_history(-1)
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"Failed to decode JSON: {e}")
|
||||
|
||||
@@ -1012,6 +1014,7 @@ class Waveform(PlotBase):
|
||||
return
|
||||
|
||||
if current_scan_id != self.scan_id:
|
||||
self._async_connected_devices.clear()
|
||||
self.reset()
|
||||
self.new_scan.emit()
|
||||
self.new_scan_id.emit(current_scan_id)
|
||||
@@ -1178,18 +1181,22 @@ class Waveform(PlotBase):
|
||||
except KeyError:
|
||||
logger.warning(f"Curve {name} not found in plot item.")
|
||||
pass
|
||||
self.bec_dispatcher.connect_slot(
|
||||
self.on_async_readback,
|
||||
MessageEndpoints.device_async_readback(self.scan_id, name),
|
||||
from_start=True,
|
||||
cb_info={"scan_id": self.scan_id},
|
||||
)
|
||||
logger.info(f"Setup async curve {name}")
|
||||
|
||||
# Connect only once per device signal
|
||||
if name not in self._async_connected_devices:
|
||||
self.bec_dispatcher.connect_slot(
|
||||
self.on_async_readback,
|
||||
MessageEndpoints.device_async_readback(self.scan_id, name),
|
||||
from_start=True,
|
||||
cb_info={"scan_id": self.scan_id},
|
||||
)
|
||||
self._async_connected_devices.add(name)
|
||||
logger.info(f"Async read-back connected for {name}")
|
||||
|
||||
@SafeSlot(dict, dict, verify_sender=True)
|
||||
def on_async_readback(self, msg, metadata):
|
||||
"""
|
||||
Get async data readback. This code needs to be fast, therefor we try
|
||||
Get async data readback. This code needs to be fast; therefore, we try
|
||||
to reduce the number of copies in between cycles. Be careful when refactoring
|
||||
this part as it will affect the performance of the async readback.
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "bec_widgets"
|
||||
version = "2.3.0"
|
||||
version = "2.1.2"
|
||||
description = "BEC Widgets"
|
||||
requires-python = ">=3.10"
|
||||
classifiers = [
|
||||
|
||||
@@ -82,52 +82,3 @@ def test_bec_connector_submit_task(bec_connector):
|
||||
while not completed:
|
||||
QApplication.processEvents()
|
||||
time.sleep(0.1)
|
||||
|
||||
|
||||
def test_bec_connector_change_object_name(bec_connector):
|
||||
# Store the original object name and RPC register state
|
||||
original_name = bec_connector.objectName()
|
||||
original_gui_id = bec_connector.gui_id
|
||||
|
||||
# Call the method with a new name
|
||||
new_name = "new_test_name"
|
||||
bec_connector.change_object_name(new_name)
|
||||
|
||||
# Process events to allow the single shot timer to execute
|
||||
QApplication.processEvents()
|
||||
|
||||
# Verify that the object name was changed correctly
|
||||
assert bec_connector.objectName() == new_name
|
||||
assert bec_connector.object_name == new_name
|
||||
|
||||
# Verify that the object is registered in the RPC register with the new name
|
||||
assert bec_connector.rpc_register.object_is_registered(bec_connector)
|
||||
|
||||
# Verify that the object with the original name is no longer registered
|
||||
# The object should still have the same gui_id
|
||||
assert bec_connector.gui_id == original_gui_id
|
||||
# Check that no object with the original name exists in the RPC register
|
||||
all_objects = bec_connector.rpc_register.list_all_connections().values()
|
||||
assert not any(obj.objectName() == original_name for obj in all_objects)
|
||||
|
||||
# Store the current name for the next test
|
||||
previous_name = bec_connector.objectName()
|
||||
|
||||
# Test with spaces and hyphens
|
||||
name_with_spaces_and_hyphens = "test name-with-hyphens"
|
||||
expected_name = "test_name_with_hyphens"
|
||||
bec_connector.change_object_name(name_with_spaces_and_hyphens)
|
||||
|
||||
# Process events to allow the single shot timer to execute
|
||||
QApplication.processEvents()
|
||||
|
||||
# Verify that the object name was changed correctly with replacements
|
||||
assert bec_connector.objectName() == expected_name
|
||||
assert bec_connector.object_name == expected_name
|
||||
|
||||
# Verify that the object is still registered in the RPC register after the second name change
|
||||
assert bec_connector.rpc_register.object_is_registered(bec_connector)
|
||||
|
||||
# Verify that the object with the previous name is no longer registered
|
||||
all_objects = bec_connector.rpc_register.list_all_connections().values()
|
||||
assert not any(obj.objectName() == previous_name for obj in all_objects)
|
||||
|
||||
@@ -7,7 +7,7 @@ import pytest
|
||||
from bec_lib.messages import ScanMessage
|
||||
from bec_lib.serialization import MsgpackSerialization
|
||||
|
||||
from bec_widgets.utils.bec_dispatcher import QtRedisConnector, QtThreadSafeCallback
|
||||
from bec_widgets.utils.bec_dispatcher import QtRedisConnector
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -27,7 +27,6 @@ def bec_dispatcher_w_connector(bec_dispatcher, topics_msg_list, send_msg_event):
|
||||
connector = QtRedisConnector("localhost:1", redis_class_mock)
|
||||
bec_dispatcher.client.connector = connector
|
||||
yield bec_dispatcher
|
||||
connector.shutdown()
|
||||
|
||||
|
||||
dummy_msg = MsgpackSerialization.dumps(ScanMessage(point_id=0, scan_id="0", data={}))
|
||||
@@ -63,6 +62,7 @@ def test_dispatcher_disconnect_all(bec_dispatcher_w_connector, qtbot, send_msg_e
|
||||
|
||||
@pytest.mark.parametrize("topics_msg_list", [(("topic1", dummy_msg), ("topic2", dummy_msg))])
|
||||
def test_dispatcher_disconnect_one(bec_dispatcher_w_connector, qtbot, send_msg_event):
|
||||
# test for BEC issue #276
|
||||
bec_dispatcher = bec_dispatcher_w_connector
|
||||
cb1 = mock.Mock(spec=[])
|
||||
cb2 = mock.Mock(spec=[])
|
||||
@@ -86,21 +86,12 @@ def test_dispatcher_2_cb_same_topic(bec_dispatcher_w_connector, qtbot, send_msg_
|
||||
cb1 = mock.Mock(spec=[])
|
||||
cb2 = mock.Mock(spec=[])
|
||||
|
||||
num_slots = len(bec_dispatcher._registered_slots)
|
||||
|
||||
bec_dispatcher.connect_slot(cb1, "topic1")
|
||||
bec_dispatcher.connect_slot(cb2, "topic1")
|
||||
|
||||
# The redis connector should only subscribe once to the topic
|
||||
assert len(bec_dispatcher.client.connector._topics_cb) == 1
|
||||
|
||||
# The the given topic, two callbacks should be registered
|
||||
assert len(bec_dispatcher.client.connector._topics_cb["topic1"]) == 2
|
||||
|
||||
# The dispatcher should have two slots
|
||||
assert len(bec_dispatcher._registered_slots) == num_slots + 2
|
||||
assert len(bec_dispatcher._slots) == 2
|
||||
bec_dispatcher.disconnect_slot(cb1, "topic1")
|
||||
assert len(bec_dispatcher._registered_slots) == num_slots + 1
|
||||
assert len(bec_dispatcher._slots) == 1
|
||||
|
||||
send_msg_event.set()
|
||||
qtbot.wait(10)
|
||||
@@ -108,31 +99,9 @@ def test_dispatcher_2_cb_same_topic(bec_dispatcher_w_connector, qtbot, send_msg_
|
||||
cb2.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("topics_msg_list", [(("topic1", dummy_msg),)])
|
||||
def test_dispatcher_2_cb_same_topic_same_slot(bec_dispatcher_w_connector, qtbot, send_msg_event):
|
||||
bec_dispatcher = bec_dispatcher_w_connector
|
||||
cb1 = mock.Mock(spec=[])
|
||||
|
||||
bec_dispatcher.connect_slot(cb1, "topic1")
|
||||
bec_dispatcher.connect_slot(cb1, "topic1")
|
||||
assert len(bec_dispatcher.client.connector._topics_cb) == 1
|
||||
assert (
|
||||
len(list(filter(lambda slot: slot.cb == cb1, bec_dispatcher._registered_slots.values())))
|
||||
== 1
|
||||
)
|
||||
|
||||
send_msg_event.set()
|
||||
qtbot.wait(10)
|
||||
assert cb1.call_count == 1
|
||||
bec_dispatcher.disconnect_slot(cb1, "topic1")
|
||||
assert (
|
||||
len(list(filter(lambda slot: slot.cb == cb1, bec_dispatcher._registered_slots.values())))
|
||||
== 0
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("topics_msg_list", [(("topic1", dummy_msg), ("topic2", dummy_msg))])
|
||||
def test_dispatcher_2_topic_same_cb(bec_dispatcher_w_connector, qtbot, send_msg_event):
|
||||
# test for BEC issue #276
|
||||
bec_dispatcher = bec_dispatcher_w_connector
|
||||
cb1 = mock.Mock(spec=[])
|
||||
|
||||
@@ -145,36 +114,3 @@ def test_dispatcher_2_topic_same_cb(bec_dispatcher_w_connector, qtbot, send_msg_
|
||||
send_msg_event.set()
|
||||
qtbot.wait(10)
|
||||
cb1.assert_called_once()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("topics_msg_list", [(("topic1", dummy_msg), ("topic2", dummy_msg))])
|
||||
def test_dispatcher_2_topic_same_cb_with_boundmethod(
|
||||
bec_dispatcher_w_connector, qtbot, send_msg_event
|
||||
):
|
||||
bec_dispatcher = bec_dispatcher_w_connector
|
||||
|
||||
class MockObject:
|
||||
def mock_slot(self, msg, metadata):
|
||||
pass
|
||||
|
||||
cb1 = MockObject()
|
||||
|
||||
bec_dispatcher.connect_slot(cb1.mock_slot, "topic1", {"metadata": "test"})
|
||||
bec_dispatcher.connect_slot(cb1.mock_slot, "topic1", {"metadata": "test"})
|
||||
|
||||
def _get_slots():
|
||||
return list(
|
||||
filter(
|
||||
lambda slot: slot == QtThreadSafeCallback(cb1.mock_slot, {"metadata": "test"}),
|
||||
bec_dispatcher._registered_slots.values(),
|
||||
)
|
||||
)
|
||||
|
||||
assert len(bec_dispatcher.client.connector._topics_cb) == 1
|
||||
assert len(_get_slots()) == 1
|
||||
bec_dispatcher.disconnect_slot(cb1.mock_slot, "topic1")
|
||||
assert len(bec_dispatcher.client.connector._topics_cb) == 0
|
||||
assert len(_get_slots()) == 0
|
||||
|
||||
send_msg_event.set()
|
||||
qtbot.wait(10)
|
||||
|
||||
@@ -64,7 +64,7 @@ def test_launch_window_launch_ui_file_raises_for_qmainwindow(bec_launch_window):
|
||||
|
||||
def test_launch_window_launch_default_auto_update(bec_launch_window):
|
||||
# Mock the auto update selection
|
||||
bec_launch_window.tiles["auto_update"].selector.setCurrentText("Default")
|
||||
bec_launch_window.tile_auto_update.selector.setCurrentText("Default")
|
||||
|
||||
# Call the method to launch the auto update
|
||||
res = bec_launch_window._open_auto_update()
|
||||
@@ -82,11 +82,11 @@ def test_launch_window_launch_plugin_auto_update(bec_launch_window):
|
||||
class PluginAutoUpdate(AutoUpdates): ...
|
||||
|
||||
bec_launch_window.available_auto_updates = {"PluginAutoUpdate": PluginAutoUpdate}
|
||||
bec_launch_window.tiles["auto_update"].selector.clear()
|
||||
bec_launch_window.tiles["auto_update"].selector.addItems(
|
||||
bec_launch_window.tile_auto_update.selector.clear()
|
||||
bec_launch_window.tile_auto_update.selector.addItems(
|
||||
list(bec_launch_window.available_auto_updates.keys()) + ["Default"]
|
||||
)
|
||||
bec_launch_window.tiles["auto_update"].selector.setCurrentText("PluginAutoUpdate")
|
||||
bec_launch_window.tile_auto_update.selector.setCurrentText("PluginAutoUpdate")
|
||||
res = bec_launch_window._open_auto_update()
|
||||
assert isinstance(res, PluginAutoUpdate)
|
||||
assert res.windowTitle() == "BEC - PluginAutoUpdate"
|
||||
|
||||
Reference in New Issue
Block a user