mirror of
https://github.com/bec-project/bec_widgets.git
synced 2026-05-07 15:24:20 +02:00
Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8f05239fcf | |||
| fcac2835dc | |||
| 07b4a98c26 | |||
| 8345dacb26 | |||
| 531d9c621d | |||
| dc151cdfe3 | |||
| e0dfd56a0d | |||
| 1fb680abb4 | |||
| b9e56c96cb | |||
| dd956f18fe | |||
| cf59d31113 | |||
| bc0e277332 | |||
| 75a2780fe0 | |||
| a6c479e42e | |||
| 64a4824054 | |||
| 1619446ec9 | |||
| 37f002427a | |||
| 50cb70dcc6 | |||
| 55f7efc4f5 | |||
| be72c9f270 | |||
| c8cedc0124 | |||
| 3fdbe4031e | |||
| c16b9dce9c | |||
| 9387275851 | |||
| 94463afdba | |||
| 02563b10f3 | |||
| fff4af2489 | |||
| 452124b528 | |||
| 9c84e158ba | |||
| 58a0bc7974 | |||
| 770dbd4b63 | |||
| d22035f897 | |||
| fe21b39b7f | |||
| 1b78840fd8 |
@@ -0,0 +1,15 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
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
|
||||||
+19
-5
@@ -13,7 +13,7 @@ variables:
|
|||||||
value: main
|
value: main
|
||||||
CHILD_PIPELINE_BRANCH: $CI_DEFAULT_BRANCH
|
CHILD_PIPELINE_BRANCH: $CI_DEFAULT_BRANCH
|
||||||
CHECK_PKG_VERSIONS:
|
CHECK_PKG_VERSIONS:
|
||||||
description: Whether to run additional tests against min/max/random selection of dependencies. Set to 1 for running.
|
description: Whether to run additional tests against min/max/random selection of dependencies. Set to 1 for running.
|
||||||
value: 0
|
value: 0
|
||||||
|
|
||||||
workflow:
|
workflow:
|
||||||
@@ -77,7 +77,7 @@ formatter:
|
|||||||
stage: Formatter
|
stage: Formatter
|
||||||
needs: []
|
needs: []
|
||||||
script:
|
script:
|
||||||
- pip install bec_lib[dev]
|
- pip install -e ./[dev]
|
||||||
- isort --check --diff --line-length=100 --profile=black --multi-line=3 --trailing-comma ./
|
- isort --check --diff --line-length=100 --profile=black --multi-line=3 --trailing-comma ./
|
||||||
- black --check --diff --color --line-length=100 --skip-magic-trailing-comma ./
|
- black --check --diff --color --line-length=100 --skip-magic-trailing-comma ./
|
||||||
rules:
|
rules:
|
||||||
@@ -162,6 +162,20 @@ tests:
|
|||||||
- tests/reference_failures/
|
- tests/reference_failures/
|
||||||
when: always
|
when: always
|
||||||
|
|
||||||
|
generate-client-check:
|
||||||
|
stage: test
|
||||||
|
needs: []
|
||||||
|
variables:
|
||||||
|
QT_QPA_PLATFORM: "offscreen"
|
||||||
|
script:
|
||||||
|
- *clone-repos
|
||||||
|
- *install-os-packages
|
||||||
|
- *install-repos
|
||||||
|
- pip install -e .[dev,pyside6]
|
||||||
|
- bw-generate-cli --target bec_widgets
|
||||||
|
# if there are changes in the generated files, fail the job
|
||||||
|
- git diff --exit-code
|
||||||
|
|
||||||
test-matrix:
|
test-matrix:
|
||||||
parallel:
|
parallel:
|
||||||
matrix:
|
matrix:
|
||||||
@@ -189,7 +203,7 @@ test-matrix:
|
|||||||
end-2-end-conda:
|
end-2-end-conda:
|
||||||
stage: End2End
|
stage: End2End
|
||||||
needs: []
|
needs: []
|
||||||
image: continuumio/miniconda3
|
image: continuumio/miniconda3:25.1.1-2
|
||||||
allow_failure: false
|
allow_failure: false
|
||||||
variables:
|
variables:
|
||||||
QT_QPA_PLATFORM: "offscreen"
|
QT_QPA_PLATFORM: "offscreen"
|
||||||
@@ -216,7 +230,7 @@ end-2-end-conda:
|
|||||||
- pip install -e ./ophyd_devices
|
- pip install -e ./ophyd_devices
|
||||||
|
|
||||||
- pip install -e .[dev,pyside6]
|
- pip install -e .[dev,pyside6]
|
||||||
- pytest -v --files-path ./ --start-servers --flush-redis --random-order ./tests/end-2-end
|
- pytest -v --files-path ./ --start-servers --random-order ./tests/end-2-end
|
||||||
|
|
||||||
artifacts:
|
artifacts:
|
||||||
when: on_failure
|
when: on_failure
|
||||||
@@ -231,7 +245,7 @@ end-2-end-conda:
|
|||||||
- if: '$CI_PIPELINE_SOURCE == "parent_pipeline"'
|
- if: '$CI_PIPELINE_SOURCE == "parent_pipeline"'
|
||||||
- if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "main"'
|
- if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "main"'
|
||||||
- if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "production"'
|
- if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME == "production"'
|
||||||
- if: '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME =~ /^pre_release.*$/'
|
- if: "$CI_MERGE_REQUEST_TARGET_BRANCH_NAME =~ /^pre_release.*$/"
|
||||||
|
|
||||||
semver:
|
semver:
|
||||||
stage: Deploy
|
stage: Deploy
|
||||||
|
|||||||
+121
@@ -1,6 +1,127 @@
|
|||||||
# CHANGELOG
|
# 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
|
||||||
|
|
||||||
|
- **waveform**: Ignore callbacks for on_async_readback from QtSender objects that are already
|
||||||
|
destroyed; closes #497
|
||||||
|
([`64a4824`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/64a48240546846fdf4541c2adf3a0a5a0829f948))
|
||||||
|
|
||||||
|
### Build System
|
||||||
|
|
||||||
|
- Remove flush-redis from ci job
|
||||||
|
([`a6c479e`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/a6c479e42ea2a47c45e5a323bb3072bab503ecf1))
|
||||||
|
|
||||||
|
### Refactoring
|
||||||
|
|
||||||
|
- **bec-progressbar**: Add private method for bec_progressbar, udate client file
|
||||||
|
([`37f0024`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/37f002427ad5da01164ae3b0f4983695fe61c243))
|
||||||
|
|
||||||
|
- **bec-status-box**: Add get_server_state user_access method to BECStatusBox
|
||||||
|
([`1619446`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1619446ec9839cfa1c666a3790a0c2abc449c4a8))
|
||||||
|
|
||||||
|
|
||||||
|
## v2.1.1 (2025-05-06)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Import add operator in client
|
||||||
|
([`55f7efc`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/55f7efc4f586128dfb66fc6a8eb5d3a9f32bf61e))
|
||||||
|
|
||||||
|
### Refactoring
|
||||||
|
|
||||||
|
- Supply bec designer filename to function
|
||||||
|
([`be72c9f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/be72c9f2708c93dab24d4383f5622e38cf1dc8a2))
|
||||||
|
|
||||||
|
|
||||||
|
## v2.1.0 (2025-05-05)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- Ensure rpc object do not collide with protected names
|
||||||
|
([`94463af`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/94463afdba11fe2da5958a371ef49572889b8622))
|
||||||
|
|
||||||
|
### Chores
|
||||||
|
|
||||||
|
- **formatter**: Upgrade to black v25
|
||||||
|
([`452124b`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/452124b528c41db14d1e34ab98db95f6f7230ad6))
|
||||||
|
|
||||||
|
### Continuous Integration
|
||||||
|
|
||||||
|
- Install dev dependencies for formatter
|
||||||
|
([`fff4af2`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/fff4af2489bdea0cf4f6f8db68db59fba411c25e))
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- **SafeSlot**: Slot parameters can be overridden with kwarg; add option to raise
|
||||||
|
([`9387275`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/93872758517177503b1f868376a6095670131844))
|
||||||
|
|
||||||
|
### Refactoring
|
||||||
|
|
||||||
|
- **colormap_widget**: Widget is rounded
|
||||||
|
([`02563b1`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/02563b10f3c90bddc069446dfe4137aa5a9727cb))
|
||||||
|
|
||||||
|
### Testing
|
||||||
|
|
||||||
|
- **Dock**: Add validation for new dock creation with invalid name
|
||||||
|
([`c16b9dc`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/c16b9dce9ce629b794d731cd7f3282a59f8b8c59))
|
||||||
|
|
||||||
|
|
||||||
|
## v2.0.3 (2025-05-02)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- **generate_cli**: Apply isort config
|
||||||
|
([`770dbd4`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/770dbd4b63baba588871a4d4ffa77d44872d085b))
|
||||||
|
|
||||||
|
- **image_item**: Wrong user access name for rotation
|
||||||
|
([`58a0bc7`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/58a0bc79742e7e7578988711a9840ed6041d9a69))
|
||||||
|
|
||||||
|
### Continuous Integration
|
||||||
|
|
||||||
|
- Add job to test that the generated client is up to date
|
||||||
|
([`d22035f`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/d22035f8974ac51ae1b6efc0e2b3749ca0a674ff))
|
||||||
|
|
||||||
|
|
||||||
|
## v2.0.2 (2025-05-01)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- **plot_base**: No content margin for plot_widget window
|
||||||
|
([`1b78840`](https://gitlab.psi.ch/bec/bec_widgets/-/commit/1b78840fd87ea0f156c73beeb57c6c06f685f7b1))
|
||||||
|
|
||||||
|
|
||||||
## v2.0.1 (2025-04-30)
|
## v2.0.1 (2025-04-30)
|
||||||
|
|
||||||
### Bug Fixes
|
### Bug Fixes
|
||||||
|
|||||||
@@ -1,5 +1,12 @@
|
|||||||
# BEC Widgets
|
# 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:**
|
**⚠️ Important Notice:**
|
||||||
|
|
||||||
🚨 **PyQt6 is no longer supported** due to incompatibilities with Qt Designer. Please use **PySide6** instead. 🚨
|
🚨 **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 os
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING, Callable
|
||||||
|
|
||||||
from bec_lib.logger import bec_logger
|
from bec_lib.logger import bec_logger
|
||||||
from qtpy.QtCore import Qt, Signal
|
from qtpy.QtCore import Qt, Signal # type: ignore
|
||||||
from qtpy.QtGui import QPainter, QPainterPath, QPixmap
|
from qtpy.QtGui import QPainter, QPainterPath, QPixmap
|
||||||
from qtpy.QtWidgets import (
|
from qtpy.QtWidgets import (
|
||||||
QApplication,
|
QApplication,
|
||||||
@@ -21,8 +21,10 @@ from qtpy.QtWidgets import (
|
|||||||
|
|
||||||
import bec_widgets
|
import bec_widgets
|
||||||
from bec_widgets.cli.rpc.rpc_register import RPCRegister
|
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.container_utils import WidgetContainerUtils
|
||||||
from bec_widgets.utils.error_popups import SafeSlot
|
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.plugin_utils import get_plugin_auto_updates
|
||||||
from bec_widgets.utils.round_frame import RoundedFrame
|
from bec_widgets.utils.round_frame import RoundedFrame
|
||||||
from bec_widgets.utils.toolbar import ModularToolBar
|
from bec_widgets.utils.toolbar import ModularToolBar
|
||||||
@@ -35,6 +37,8 @@ from bec_widgets.widgets.utility.visual.dark_mode_button.dark_mode_button import
|
|||||||
if TYPE_CHECKING: # pragma: no cover
|
if TYPE_CHECKING: # pragma: no cover
|
||||||
from qtpy.QtCore import QObject
|
from qtpy.QtCore import QObject
|
||||||
|
|
||||||
|
from bec_widgets.utils.bec_widget import BECWidget
|
||||||
|
|
||||||
logger = bec_logger.logger
|
logger = bec_logger.logger
|
||||||
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
|
MODULE_PATH = os.path.dirname(bec_widgets.__file__)
|
||||||
|
|
||||||
@@ -141,6 +145,7 @@ class LaunchWindow(BECMainWindow):
|
|||||||
super().__init__(parent=parent, gui_id=gui_id, window_title=window_title, **kwargs)
|
super().__init__(parent=parent, gui_id=gui_id, window_title=window_title, **kwargs)
|
||||||
|
|
||||||
self.app = QApplication.instance()
|
self.app = QApplication.instance()
|
||||||
|
self.tiles: dict[str, LaunchTile] = {}
|
||||||
|
|
||||||
# Toolbar
|
# Toolbar
|
||||||
self.dark_mode_button = DarkModeButton(parent=self, toolbar=True)
|
self.dark_mode_button = DarkModeButton(parent=self, toolbar=True)
|
||||||
@@ -156,58 +161,105 @@ class LaunchWindow(BECMainWindow):
|
|||||||
self.central_widget.layout = QHBoxLayout(self.central_widget)
|
self.central_widget.layout = QHBoxLayout(self.central_widget)
|
||||||
self.setCentralWidget(self.central_widget)
|
self.setCentralWidget(self.central_widget)
|
||||||
|
|
||||||
self.tile_dock_area = LaunchTile(
|
self.register_tile(
|
||||||
|
name="dock_area",
|
||||||
icon_path=os.path.join(MODULE_PATH, "assets", "app_icons", "bec_widgets_icon.png"),
|
icon_path=os.path.join(MODULE_PATH, "assets", "app_icons", "bec_widgets_icon.png"),
|
||||||
top_label="Get started",
|
top_label="Get started",
|
||||||
main_label="BEC Dock Area",
|
main_label="BEC Dock Area",
|
||||||
description="Highly flexible and customizable dock area application with modular widgets.",
|
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.tile_auto_update = LaunchTile(
|
self.available_auto_updates: dict[str, type[AutoUpdates]] = (
|
||||||
|
self._update_available_auto_updates()
|
||||||
|
)
|
||||||
|
self.register_tile(
|
||||||
|
name="auto_update",
|
||||||
icon_path=os.path.join(MODULE_PATH, "assets", "app_icons", "auto_update.png"),
|
icon_path=os.path.join(MODULE_PATH, "assets", "app_icons", "auto_update.png"),
|
||||||
top_label="Get automated",
|
top_label="Get automated",
|
||||||
main_label="BEC Auto Update Dock Area",
|
main_label="BEC Auto Update Dock Area",
|
||||||
description="Dock area with auto update functionality for BEC widgets plotting.",
|
description="Dock area with auto update functionality for BEC widgets plotting.",
|
||||||
|
action_button=self._open_auto_update,
|
||||||
show_selector=True,
|
show_selector=True,
|
||||||
|
selector_items=list(self.available_auto_updates.keys()) + ["Default"],
|
||||||
)
|
)
|
||||||
self.tile_auto_update.setFixedSize(*self.TILE_SIZE)
|
|
||||||
|
|
||||||
self.tile_ui_file = LaunchTile(
|
self.register_tile(
|
||||||
|
name="custom_ui_file",
|
||||||
icon_path=os.path.join(MODULE_PATH, "assets", "app_icons", "ui_loader_tile.png"),
|
icon_path=os.path.join(MODULE_PATH, "assets", "app_icons", "ui_loader_tile.png"),
|
||||||
top_label="Get customized",
|
top_label="Get customized",
|
||||||
main_label="Launch Custom UI File",
|
main_label="Launch Custom UI File",
|
||||||
description="GUI application with 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)
|
|
||||||
|
|
||||||
# Add tiles to the main layout
|
# plugin widgets
|
||||||
self.central_widget.layout.addWidget(self.tile_dock_area)
|
self.available_widgets: dict[str, BECWidget] = get_all_plugin_widgets()
|
||||||
self.central_widget.layout.addWidget(self.tile_auto_update)
|
if self.available_widgets:
|
||||||
self.central_widget.layout.addWidget(self.tile_ui_file)
|
plugin_repo_name = next(iter(self.available_widgets.values())).__module__.split(".")[0]
|
||||||
|
plugin_repo_name = plugin_repo_name.removesuffix("_bec").upper()
|
||||||
# hacky solution no time to waste
|
self.register_tile(
|
||||||
self.tiles = [self.tile_dock_area, self.tile_auto_update, self.tile_ui_file]
|
name="widget",
|
||||||
|
icon_path=os.path.join(
|
||||||
# Connect signals
|
MODULE_PATH, "assets", "app_icons", "widget_launch_tile.png"
|
||||||
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)
|
top_label="Get quickly started",
|
||||||
self.tile_ui_file.action_button.clicked.connect(self._open_custom_ui_file)
|
main_label=f"Launch a {plugin_repo_name} Widget",
|
||||||
self._update_theme()
|
description=f"GUI application with one widget from the {plugin_repo_name} repository.",
|
||||||
|
action_button=self._open_widget,
|
||||||
# Auto updates
|
show_selector=True,
|
||||||
self.available_auto_updates: dict[str, type[AutoUpdates]] = (
|
selector_items=list(self.available_widgets.keys()),
|
||||||
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._update_theme()
|
||||||
|
|
||||||
self.register = RPCRegister()
|
self.register = RPCRegister()
|
||||||
self.register.callbacks.append(self._turn_off_the_lights)
|
self.register.callbacks.append(self._turn_off_the_lights)
|
||||||
self.register.broadcast()
|
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(
|
def launch(
|
||||||
self,
|
self,
|
||||||
launch_script: str,
|
launch_script: str,
|
||||||
@@ -235,10 +287,8 @@ class LaunchWindow(BECMainWindow):
|
|||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Name {name} must be unique for dock areas, but already exists: {existing_dock_areas}."
|
f"Name {name} must be unique for dock areas, but already exists: {existing_dock_areas}."
|
||||||
)
|
)
|
||||||
if not WidgetContainerUtils.has_name_valid_chars(name):
|
WidgetContainerUtils.raise_for_invalid_name(name)
|
||||||
raise ValueError(
|
|
||||||
f"Name {name} contains invalid characters. Only alphanumeric characters, underscores, and dashes are allowed."
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
name = "dock_area"
|
name = "dock_area"
|
||||||
name = WidgetContainerUtils.generate_unique_name(name, existing_dock_areas)
|
name = WidgetContainerUtils.generate_unique_name(name, existing_dock_areas)
|
||||||
@@ -258,6 +308,12 @@ class LaunchWindow(BECMainWindow):
|
|||||||
auto_update = kwargs.pop("auto_update", None)
|
auto_update = kwargs.pop("auto_update", None)
|
||||||
return self._launch_auto_update(auto_update)
|
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)
|
launch = getattr(bw_launch, launch_script, None)
|
||||||
if launch is None:
|
if launch is None:
|
||||||
raise ValueError(f"Launch script {launch_script} not found.")
|
raise ValueError(f"Launch script {launch_script} not found.")
|
||||||
@@ -275,6 +331,7 @@ class LaunchWindow(BECMainWindow):
|
|||||||
else:
|
else:
|
||||||
window = BECMainWindow()
|
window = BECMainWindow()
|
||||||
window.setCentralWidget(result_widget)
|
window.setCentralWidget(result_widget)
|
||||||
|
window.setWindowTitle(f"BEC - {result_widget.objectName()}")
|
||||||
window.show()
|
window.show()
|
||||||
return result_widget
|
return result_widget
|
||||||
|
|
||||||
@@ -284,6 +341,8 @@ class LaunchWindow(BECMainWindow):
|
|||||||
raise ValueError("UI file must be provided for custom UI file launch.")
|
raise ValueError("UI file must be provided for custom UI file launch.")
|
||||||
filename = os.path.basename(ui_file).split(".")[0]
|
filename = os.path.basename(ui_file).split(".")[0]
|
||||||
|
|
||||||
|
WidgetContainerUtils.raise_for_invalid_name(filename)
|
||||||
|
|
||||||
tree = ET.parse(ui_file)
|
tree = ET.parse(ui_file)
|
||||||
root = tree.getroot()
|
root = tree.getroot()
|
||||||
# Check if the top-level widget is a QMainWindow
|
# Check if the top-level widget is a QMainWindow
|
||||||
@@ -321,11 +380,28 @@ class LaunchWindow(BECMainWindow):
|
|||||||
window.show()
|
window.show()
|
||||||
return window
|
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):
|
def apply_theme(self, theme: str):
|
||||||
"""
|
"""
|
||||||
Change the theme of the application.
|
Change the theme of the application.
|
||||||
"""
|
"""
|
||||||
for tile in self.tiles:
|
for tile in self.tiles.values():
|
||||||
tile.apply_theme(theme)
|
tile.apply_theme(theme)
|
||||||
|
|
||||||
super().apply_theme(theme)
|
super().apply_theme(theme)
|
||||||
@@ -334,14 +410,25 @@ class LaunchWindow(BECMainWindow):
|
|||||||
"""
|
"""
|
||||||
Open the auto update window.
|
Open the auto update window.
|
||||||
"""
|
"""
|
||||||
if self.tile_auto_update.selector is None:
|
if self.tiles["auto_update"].selector is None:
|
||||||
auto_update = None
|
auto_update = None
|
||||||
else:
|
else:
|
||||||
auto_update = self.tile_auto_update.selector.currentText()
|
auto_update = self.tiles["auto_update"].selector.currentText()
|
||||||
if auto_update == "Default":
|
if auto_update == "Default":
|
||||||
auto_update = None
|
auto_update = None
|
||||||
return self.launch("auto_update", auto_update=auto_update)
|
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)
|
@SafeSlot(popup_error=True)
|
||||||
def _open_custom_ui_file(self):
|
def _open_custom_ui_file(self):
|
||||||
"""
|
"""
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 2.1 MiB |
@@ -7,6 +7,7 @@ import enum
|
|||||||
import inspect
|
import inspect
|
||||||
import traceback
|
import traceback
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
|
from operator import add
|
||||||
from typing import Literal, Optional
|
from typing import Literal, Optional
|
||||||
|
|
||||||
from bec_lib.logger import bec_logger
|
from bec_lib.logger import bec_logger
|
||||||
@@ -469,6 +470,12 @@ class BECProgressBar(RPCBase):
|
|||||||
>>> progressbar.label_template = "$value / $percentage %"
|
>>> progressbar.label_template = "$value / $percentage %"
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@rpc_call
|
||||||
|
def _get_label(self) -> str:
|
||||||
|
"""
|
||||||
|
Return the label text. mostly used for testing rpc.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class BECQueue(RPCBase):
|
class BECQueue(RPCBase):
|
||||||
"""Widget to display the BEC queue."""
|
"""Widget to display the BEC queue."""
|
||||||
@@ -483,6 +490,12 @@ class BECQueue(RPCBase):
|
|||||||
class BECStatusBox(RPCBase):
|
class BECStatusBox(RPCBase):
|
||||||
"""An autonomous widget to display the status of BEC services."""
|
"""An autonomous widget to display the status of BEC services."""
|
||||||
|
|
||||||
|
@rpc_call
|
||||||
|
def get_server_state(self) -> "str":
|
||||||
|
"""
|
||||||
|
Get the state ("RUNNING", "BUSY", "IDLE", "ERROR") of the BEC server
|
||||||
|
"""
|
||||||
|
|
||||||
@rpc_call
|
@rpc_call
|
||||||
def remove(self):
|
def remove(self):
|
||||||
"""
|
"""
|
||||||
@@ -1317,14 +1330,14 @@ class ImageItem(RPCBase):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
@rpc_call
|
@rpc_call
|
||||||
def rotation(self) -> "Optional[int]":
|
def num_rotation_90(self) -> "Optional[int]":
|
||||||
"""
|
"""
|
||||||
Get or set the number of 90° rotations to apply.
|
Get or set the number of 90° rotations to apply.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@rotation.setter
|
@num_rotation_90.setter
|
||||||
@rpc_call
|
@rpc_call
|
||||||
def rotation(self) -> "Optional[int]":
|
def num_rotation_90(self) -> "Optional[int]":
|
||||||
"""
|
"""
|
||||||
Get or set the number of 90° rotations to apply.
|
Get or set the number of 90° rotations to apply.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ class ClientGenerator:
|
|||||||
import inspect
|
import inspect
|
||||||
import traceback
|
import traceback
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
|
from operator import add
|
||||||
from typing import Literal, Optional
|
from typing import Literal, Optional
|
||||||
"""
|
"""
|
||||||
if self._base
|
if self._base
|
||||||
@@ -222,18 +223,18 @@ class {class_name}(RPCBase):"""
|
|||||||
# Combine header and content, then format with black
|
# Combine header and content, then format with black
|
||||||
full_content = self.header + "\n" + self.content
|
full_content = self.header + "\n" + self.content
|
||||||
try:
|
try:
|
||||||
formatted_content = black.format_str(full_content, mode=black.FileMode(line_length=100))
|
formatted_content = black.format_str(full_content, mode=black.Mode(line_length=100))
|
||||||
except black.NothingChanged:
|
except black.NothingChanged:
|
||||||
formatted_content = full_content
|
formatted_content = full_content
|
||||||
|
|
||||||
isort.Config(
|
config = isort.Config(
|
||||||
profile="black",
|
profile="black",
|
||||||
line_length=100,
|
line_length=100,
|
||||||
multi_line_output=3,
|
multi_line_output=3,
|
||||||
include_trailing_comma=True,
|
include_trailing_comma=False,
|
||||||
known_first_party=["bec_widgets"],
|
known_first_party=["bec_widgets"],
|
||||||
)
|
)
|
||||||
formatted_content = isort.code(formatted_content)
|
formatted_content = isort.code(formatted_content, config=config)
|
||||||
|
|
||||||
with open(file_name, "w", encoding="utf-8") as file:
|
with open(file_name, "w", encoding="utf-8") as file:
|
||||||
file.write(formatted_content)
|
file.write(formatted_content)
|
||||||
@@ -318,5 +319,5 @@ def main():
|
|||||||
if __name__ == "__main__": # pragma: no cover
|
if __name__ == "__main__": # pragma: no cover
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
sys.argv = ["bw-generate-cli", "--target", "csaxs_bec"]
|
sys.argv = ["bw-generate-cli", "--target", "bec_widgets"]
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -205,6 +205,17 @@ 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}"
|
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:
|
def _update_object_name(self) -> None:
|
||||||
"""
|
"""
|
||||||
Enforce a unique object name among siblings and register the object for RPC.
|
Enforce a unique object name among siblings and register the object for RPC.
|
||||||
|
|||||||
@@ -16,9 +16,9 @@ if PYSIDE6:
|
|||||||
from PySide6.scripts.pyside_tool import (
|
from PySide6.scripts.pyside_tool import (
|
||||||
_extend_path_var,
|
_extend_path_var,
|
||||||
init_virtual_env,
|
init_virtual_env,
|
||||||
qt_tool_wrapper,
|
|
||||||
is_pyenv_python,
|
is_pyenv_python,
|
||||||
is_virtual_env,
|
is_virtual_env,
|
||||||
|
qt_tool_wrapper,
|
||||||
ui_tool_binary,
|
ui_tool_binary,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -78,7 +78,7 @@ def list_editable_packages() -> set[str]:
|
|||||||
return editable_packages
|
return editable_packages
|
||||||
|
|
||||||
|
|
||||||
def patch_designer(): # pragma: no cover
|
def patch_designer(cmd_args: list[str] = []): # pragma: no cover
|
||||||
if not PYSIDE6:
|
if not PYSIDE6:
|
||||||
print("PYSIDE6 is not available in the environment. Cannot patch designer.")
|
print("PYSIDE6 is not available in the environment. Cannot patch designer.")
|
||||||
return
|
return
|
||||||
@@ -119,7 +119,7 @@ def patch_designer(): # pragma: no cover
|
|||||||
editable_packages = list_editable_packages()
|
editable_packages = list_editable_packages()
|
||||||
for pckg in editable_packages:
|
for pckg in editable_packages:
|
||||||
_extend_path_var("PYTHONPATH", pckg, True)
|
_extend_path_var("PYTHONPATH", pckg, True)
|
||||||
qt_tool_wrapper(ui_tool_binary("designer"), sys.argv[1:])
|
qt_tool_wrapper(ui_tool_binary("designer"), cmd_args)
|
||||||
|
|
||||||
|
|
||||||
def find_plugin_paths(base_path: Path):
|
def find_plugin_paths(base_path: Path):
|
||||||
@@ -147,7 +147,7 @@ def set_plugin_environment_variable(plugin_paths):
|
|||||||
|
|
||||||
|
|
||||||
# Patch the designer function
|
# Patch the designer function
|
||||||
def main(): # pragma: no cover
|
def open_designer(cmd_args: list[str] = []): # pragma: no cover
|
||||||
if not PYSIDE6:
|
if not PYSIDE6:
|
||||||
print("PYSIDE6 is not available in the environment. Exiting...")
|
print("PYSIDE6 is not available in the environment. Exiting...")
|
||||||
return
|
return
|
||||||
@@ -160,7 +160,11 @@ def main(): # pragma: no cover
|
|||||||
|
|
||||||
set_plugin_environment_variable(plugin_paths)
|
set_plugin_environment_variable(plugin_paths)
|
||||||
|
|
||||||
patch_designer()
|
patch_designer(cmd_args)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
open_designer(sys.argv[1:])
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__": # pragma: no cover
|
if __name__ == "__main__": # pragma: no cover
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ import collections
|
|||||||
import random
|
import random
|
||||||
import string
|
import string
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from typing import TYPE_CHECKING, Union
|
from typing import TYPE_CHECKING, DefaultDict, Hashable, Union
|
||||||
|
|
||||||
|
import louie
|
||||||
import redis
|
import redis
|
||||||
from bec_lib.client import BECClient
|
from bec_lib.client import BECClient
|
||||||
from bec_lib.logger import bec_logger
|
from bec_lib.logger import bec_logger
|
||||||
@@ -25,21 +26,41 @@ if TYPE_CHECKING: # pragma: no cover
|
|||||||
|
|
||||||
|
|
||||||
class QtThreadSafeCallback(QObject):
|
class QtThreadSafeCallback(QObject):
|
||||||
|
"""QtThreadSafeCallback is a wrapper around a callback function to make it thread-safe for Qt."""
|
||||||
|
|
||||||
cb_signal = pyqtSignal(dict, dict)
|
cb_signal = pyqtSignal(dict, dict)
|
||||||
|
|
||||||
def __init__(self, cb):
|
def __init__(self, cb: Callable, cb_info: dict | None = None):
|
||||||
|
"""
|
||||||
|
Initialize the QtThreadSafeCallback.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cb (Callable): The callback function to be wrapped.
|
||||||
|
cb_info (dict, optional): Additional information about the callback. Defaults to None.
|
||||||
|
"""
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
self.cb_info = cb_info
|
||||||
|
|
||||||
self.cb = cb
|
self.cb = cb
|
||||||
|
self.cb_ref = louie.saferef.safe_ref(cb)
|
||||||
self.cb_signal.connect(self.cb)
|
self.cb_signal.connect(self.cb)
|
||||||
|
self.topics = set()
|
||||||
|
|
||||||
def __hash__(self):
|
def __hash__(self):
|
||||||
# make 2 differents QtThreadSafeCallback to look
|
# make 2 differents QtThreadSafeCallback to look
|
||||||
# identical when used as dictionary keys, if the
|
# identical when used as dictionary keys, if the
|
||||||
# callback is the same
|
# callback is the same
|
||||||
return id(self.cb)
|
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
|
||||||
|
|
||||||
def __call__(self, msg_content, metadata):
|
def __call__(self, msg_content, metadata):
|
||||||
|
if self.cb_ref() is None:
|
||||||
|
# callback has been deleted
|
||||||
|
return
|
||||||
self.cb_signal.emit(msg_content, metadata)
|
self.cb_signal.emit(msg_content, metadata)
|
||||||
|
|
||||||
|
|
||||||
@@ -86,7 +107,7 @@ class BECDispatcher:
|
|||||||
cls,
|
cls,
|
||||||
client=None,
|
client=None,
|
||||||
config: str | ServiceConfig | None = None,
|
config: str | ServiceConfig | None = None,
|
||||||
gui_id: str = None,
|
gui_id: str | None = None,
|
||||||
*args,
|
*args,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
):
|
):
|
||||||
@@ -99,7 +120,9 @@ class BECDispatcher:
|
|||||||
if self._initialized:
|
if self._initialized:
|
||||||
return
|
return
|
||||||
|
|
||||||
self._slots = collections.defaultdict(set)
|
self._registered_slots: DefaultDict[Hashable, QtThreadSafeCallback] = (
|
||||||
|
collections.defaultdict()
|
||||||
|
)
|
||||||
self.client = client
|
self.client = client
|
||||||
|
|
||||||
if self.client is None:
|
if self.client is None:
|
||||||
@@ -141,6 +164,7 @@ class BECDispatcher:
|
|||||||
self,
|
self,
|
||||||
slot: Callable,
|
slot: Callable,
|
||||||
topics: Union[EndpointInfo, str, list[Union[EndpointInfo, str]]],
|
topics: Union[EndpointInfo, str, list[Union[EndpointInfo, str]]],
|
||||||
|
cb_info: dict | None = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Connect widget's qt slot, so that it is called on new pub/sub topic message.
|
"""Connect widget's qt slot, so that it is called on new pub/sub topic message.
|
||||||
@@ -149,11 +173,15 @@ class BECDispatcher:
|
|||||||
slot (Callable): A slot method/function that accepts two inputs: content and metadata of
|
slot (Callable): A slot method/function that accepts two inputs: content and metadata of
|
||||||
the corresponding pub/sub message
|
the corresponding pub/sub message
|
||||||
topics (EndpointInfo | str | list): A topic or list of topics that can typically be acquired via bec_lib.MessageEndpoints
|
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.
|
||||||
"""
|
"""
|
||||||
slot = QtThreadSafeCallback(slot)
|
qt_slot = QtThreadSafeCallback(cb=slot, cb_info=cb_info)
|
||||||
self.client.connector.register(topics, cb=slot, **kwargs)
|
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)
|
||||||
topics_str, _ = self.client.connector._convert_endpointinfo(topics)
|
topics_str, _ = self.client.connector._convert_endpointinfo(topics)
|
||||||
self._slots[slot].update(set(topics_str))
|
qt_slot.topics.update(set(topics_str))
|
||||||
|
|
||||||
def disconnect_slot(self, slot: Callable, topics: Union[str, list]):
|
def disconnect_slot(self, slot: Callable, topics: Union[str, list]):
|
||||||
"""
|
"""
|
||||||
@@ -166,16 +194,16 @@ class BECDispatcher:
|
|||||||
# find the right slot to disconnect from ;
|
# find the right slot to disconnect from ;
|
||||||
# slot callbacks are wrapped in QtThreadSafeCallback objects,
|
# slot callbacks are wrapped in QtThreadSafeCallback objects,
|
||||||
# but the slot we receive here is the original callable
|
# but the slot we receive here is the original callable
|
||||||
for connected_slot in self._slots:
|
for connected_slot in self._registered_slots.values():
|
||||||
if connected_slot.cb == slot:
|
if connected_slot.cb == slot:
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
return
|
return
|
||||||
self.client.connector.unregister(topics, cb=connected_slot)
|
self.client.connector.unregister(topics, cb=connected_slot)
|
||||||
topics_str, _ = self.client.connector._convert_endpointinfo(topics)
|
topics_str, _ = self.client.connector._convert_endpointinfo(topics)
|
||||||
self._slots[connected_slot].difference_update(set(topics_str))
|
self._registered_slots[connected_slot].topics.difference_update(set(topics_str))
|
||||||
if not self._slots[connected_slot]:
|
if not self._registered_slots[connected_slot].topics:
|
||||||
del self._slots[connected_slot]
|
del self._registered_slots[connected_slot]
|
||||||
|
|
||||||
def disconnect_topics(self, topics: Union[str, list]):
|
def disconnect_topics(self, topics: Union[str, list]):
|
||||||
"""
|
"""
|
||||||
@@ -186,11 +214,16 @@ class BECDispatcher:
|
|||||||
"""
|
"""
|
||||||
self.client.connector.unregister(topics)
|
self.client.connector.unregister(topics)
|
||||||
topics_str, _ = self.client.connector._convert_endpointinfo(topics)
|
topics_str, _ = self.client.connector._convert_endpointinfo(topics)
|
||||||
for slot in list(self._slots.keys()):
|
|
||||||
slot_topics = self._slots[slot]
|
remove_slots = []
|
||||||
slot_topics.difference_update(set(topics_str))
|
for connected_slot in self._registered_slots.values():
|
||||||
if not slot_topics:
|
connected_slot.topics.difference_update(set(topics_str))
|
||||||
del self._slots[slot]
|
|
||||||
|
if not connected_slot.topics:
|
||||||
|
remove_slots.append(connected_slot)
|
||||||
|
|
||||||
|
for connected_slot in remove_slots:
|
||||||
|
self._registered_slots.pop(connected_slot, None)
|
||||||
|
|
||||||
def disconnect_all(self, *args, **kwargs):
|
def disconnect_all(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ def _submodule_specs(module: ModuleType) -> tuple[ModuleSpec | None, ...]:
|
|||||||
|
|
||||||
|
|
||||||
def _loaded_submodules_from_specs(
|
def _loaded_submodules_from_specs(
|
||||||
submodule_specs: tuple[ModuleSpec | None, ...]
|
submodule_specs: tuple[ModuleSpec | None, ...],
|
||||||
) -> Generator[ModuleType, None, None]:
|
) -> Generator[ModuleType, None, None]:
|
||||||
"""Load all submodules from the given specs."""
|
"""Load all submodules from the given specs."""
|
||||||
for submodule in (
|
for submodule in (
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
""" This custom class is a thin wrapper around the SignalProxy class to allow signal calls to be blocked.
|
"""This custom class is a thin wrapper around the SignalProxy class to allow signal calls to be blocked.
|
||||||
Unblocking the proxy needs to be done through the slot unblock_proxy. The most likely use case for this class is
|
Unblocking the proxy needs to be done through the slot unblock_proxy. The most likely use case for this class is
|
||||||
when the callback function is potentially initiating a slower progress, i.e. requesting a data analysis routine to
|
when the callback function is potentially initiating a slower progress, i.e. requesting a data analysis routine to
|
||||||
analyse data. Requesting a new fit may lead to request piling up and an overall slow done of performance. This proxy
|
analyse data. Requesting a new fit may lead to request piling up and an overall slow done of performance. This proxy
|
||||||
will allow you to decide by yourself when to unblock and execute the callback again."""
|
will allow you to decide by yourself when to unblock and execute the callback again."""
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import itertools
|
from typing import Any, Type
|
||||||
from typing import Literal, Type
|
|
||||||
|
|
||||||
from qtpy.QtWidgets import QWidget
|
from qtpy.QtWidgets import QWidget
|
||||||
|
|
||||||
from bec_widgets.cli.rpc.rpc_register import RPCRegister
|
from bec_widgets.cli.client_utils import BECGuiClient
|
||||||
|
|
||||||
|
|
||||||
class WidgetContainerUtils:
|
class WidgetContainerUtils:
|
||||||
@@ -73,3 +72,36 @@ class WidgetContainerUtils:
|
|||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"No widget of class {widget_class} found.")
|
raise ValueError(f"No widget of class {widget_class} found.")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def name_is_protected(name: str, container: Any = None) -> bool:
|
||||||
|
"""
|
||||||
|
Check if the name is not protected.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name(str): The name to be checked.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True if the name is not protected, False otherwise.
|
||||||
|
"""
|
||||||
|
if container is None:
|
||||||
|
container = BECGuiClient
|
||||||
|
gui_client_methods = set(filter(lambda x: not x.startswith("_"), dir(container)))
|
||||||
|
return name in gui_client_methods
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def raise_for_invalid_name(name: str, container: Any = None) -> None:
|
||||||
|
"""
|
||||||
|
Check if the name is valid. If not, raise a ValueError.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name(str): The name to be checked.
|
||||||
|
Raises:
|
||||||
|
ValueError: If the name is not valid.
|
||||||
|
"""
|
||||||
|
if not WidgetContainerUtils.has_name_valid_chars(name):
|
||||||
|
raise ValueError(
|
||||||
|
f"Name '{name}' contains invalid characters. Only alphanumeric characters, underscores, and dashes are allowed."
|
||||||
|
)
|
||||||
|
if WidgetContainerUtils.name_is_protected(name, container):
|
||||||
|
raise ValueError(f"Name '{name}' is protected. Please choose another name.")
|
||||||
|
|||||||
@@ -99,16 +99,30 @@ def SafeSlot(*slot_args, **slot_kwargs): # pylint: disable=invalid-name
|
|||||||
'verify_sender' keyword argument can be passed with boolean value if the sender should be verified
|
'verify_sender' keyword argument can be passed with boolean value if the sender should be verified
|
||||||
before executing the slot. If True, the slot will only execute if the sender is a QObject. This is
|
before executing the slot. If True, the slot will only execute if the sender is a QObject. This is
|
||||||
useful to prevent function calls from already deleted objects.
|
useful to prevent function calls from already deleted objects.
|
||||||
|
'raise_error' keyword argument can be passed with boolean value if the error should be raised
|
||||||
|
after the error is displayed. This is useful to propagate the error to the caller but should be used
|
||||||
|
with great care to avoid segfaults.
|
||||||
|
|
||||||
|
The keywords above are stored in a container which can be overridden by passing
|
||||||
|
'_override_slot_params' keyword argument with a dictionary containing the keywords to override.
|
||||||
|
This is useful to override the default behavior of the decorator for a specific function call.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
popup_error = bool(slot_kwargs.pop("popup_error", False))
|
_slot_params = {
|
||||||
verify_sender = bool(slot_kwargs.pop("verify_sender", False))
|
"popup_error": bool(slot_kwargs.pop("popup_error", False)),
|
||||||
|
"verify_sender": bool(slot_kwargs.pop("verify_sender", False)),
|
||||||
|
"raise_error": bool(slot_kwargs.pop("raise_error", False)),
|
||||||
|
}
|
||||||
|
|
||||||
def error_managed(method):
|
def error_managed(method):
|
||||||
@Slot(*slot_args, **slot_kwargs)
|
@Slot(*slot_args, **slot_kwargs)
|
||||||
@functools.wraps(method)
|
@functools.wraps(method)
|
||||||
def wrapper(*args, **kwargs):
|
def wrapper(*args, **kwargs):
|
||||||
|
|
||||||
|
_override_slot_params = kwargs.pop("_override_slot_params", {})
|
||||||
|
_slot_params.update(_override_slot_params)
|
||||||
try:
|
try:
|
||||||
if not verify_sender or len(args) == 0:
|
if not _slot_params["verify_sender"] or len(args) == 0:
|
||||||
return method(*args, **kwargs)
|
return method(*args, **kwargs)
|
||||||
|
|
||||||
_instance = args[0]
|
_instance = args[0]
|
||||||
@@ -126,11 +140,11 @@ def SafeSlot(*slot_args, **slot_kwargs): # pylint: disable=invalid-name
|
|||||||
except Exception:
|
except Exception:
|
||||||
slot_name = f"{method.__module__}.{method.__qualname__}"
|
slot_name = f"{method.__module__}.{method.__qualname__}"
|
||||||
error_msg = traceback.format_exc()
|
error_msg = traceback.format_exc()
|
||||||
if popup_error:
|
if _slot_params["popup_error"]:
|
||||||
ErrorPopupUtility().custom_exception_hook(
|
ErrorPopupUtility().custom_exception_hook(*sys.exc_info(), popup_error=True)
|
||||||
*sys.exc_info(), popup_error=popup_error
|
|
||||||
)
|
|
||||||
logger.error(f"SafeSlot error in slot '{slot_name}':\n{error_msg}")
|
logger.error(f"SafeSlot error in slot '{slot_name}':\n{error_msg}")
|
||||||
|
if _slot_params["raise_error"]:
|
||||||
|
raise
|
||||||
|
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
""" Module for a thin wrapper (LinearRegionWrapper) around the LinearRegionItem in pyqtgraph.
|
"""Module for a thin wrapper (LinearRegionWrapper) around the LinearRegionItem in pyqtgraph.
|
||||||
The class is mainly designed for usage with the BECWaveform and 1D plots. """
|
The class is mainly designed for usage with the BECWaveform and 1D plots."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|||||||
@@ -303,11 +303,7 @@ class BECDock(BECWidget, Dock):
|
|||||||
shift(Literal["down", "up", "left", "right"]): The direction to shift the widgets if the position is occupied.
|
shift(Literal["down", "up", "left", "right"]): The direction to shift the widgets if the position is occupied.
|
||||||
"""
|
"""
|
||||||
if name is not None:
|
if name is not None:
|
||||||
if not WidgetContainerUtils.has_name_valid_chars(name):
|
WidgetContainerUtils.raise_for_invalid_name(name, container=self)
|
||||||
raise ValueError(
|
|
||||||
f"Name {name} contains invalid characters. "
|
|
||||||
f"Only alphanumeric characters and underscores are allowed."
|
|
||||||
)
|
|
||||||
|
|
||||||
if row is None:
|
if row is None:
|
||||||
row = self.layout.rowCount()
|
row = self.layout.rowCount()
|
||||||
|
|||||||
@@ -366,11 +366,8 @@ class BECDockArea(BECWidget, QWidget):
|
|||||||
f"Name {name} must be unique for docks, but already exists in DockArea "
|
f"Name {name} must be unique for docks, but already exists in DockArea "
|
||||||
f"with name: {self.object_name} and id {self.gui_id}."
|
f"with name: {self.object_name} and id {self.gui_id}."
|
||||||
)
|
)
|
||||||
if not WidgetContainerUtils.has_name_valid_chars(name):
|
WidgetContainerUtils.raise_for_invalid_name(name, container=self)
|
||||||
raise ValueError(
|
|
||||||
f"Name {name} contains invalid characters. "
|
|
||||||
f"Only alphanumeric characters and underscores are allowed."
|
|
||||||
)
|
|
||||||
else: # Name is not provided
|
else: # Name is not provided
|
||||||
name = WidgetContainerUtils.generate_unique_name(name="dock", list_of_names=dock_names)
|
name = WidgetContainerUtils.generate_unique_name(name="dock", list_of_names=dock_names)
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
""" Module for a PositionerGroup widget to control a positioner device."""
|
"""Module for a PositionerGroup widget to control a positioner device."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
""" Module for DapComboBox widget class to select a DAP model from a combobox. """
|
"""Module for DapComboBox widget class to select a DAP model from a combobox."""
|
||||||
|
|
||||||
from bec_lib.logger import bec_logger
|
from bec_lib.logger import bec_logger
|
||||||
from qtpy.QtCore import Property, Signal, Slot
|
from qtpy.QtCore import Property, Signal, Slot
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
BECConsole is a Qt widget that runs a Bash shell.
|
BECConsole is a Qt widget that runs a Bash shell.
|
||||||
|
|
||||||
BECConsole VT100 emulation is powered by Pyte,
|
BECConsole VT100 emulation is powered by Pyte,
|
||||||
(https://github.com/selectel/pyte).
|
(https://github.com/selectel/pyte).
|
||||||
@@ -56,12 +56,12 @@ control_keys_mapping = {
|
|||||||
QtCore.Qt.Key_G: b"\x07", # Ctrl-G (Bell)
|
QtCore.Qt.Key_G: b"\x07", # Ctrl-G (Bell)
|
||||||
QtCore.Qt.Key_H: b"\x08", # Ctrl-H (Backspace)
|
QtCore.Qt.Key_H: b"\x08", # Ctrl-H (Backspace)
|
||||||
QtCore.Qt.Key_I: b"\x09", # Ctrl-I (Tab)
|
QtCore.Qt.Key_I: b"\x09", # Ctrl-I (Tab)
|
||||||
QtCore.Qt.Key_J: b"\x0A", # Ctrl-J (Line Feed)
|
QtCore.Qt.Key_J: b"\x0a", # Ctrl-J (Line Feed)
|
||||||
QtCore.Qt.Key_K: b"\x0B", # Ctrl-K (Vertical Tab)
|
QtCore.Qt.Key_K: b"\x0b", # Ctrl-K (Vertical Tab)
|
||||||
QtCore.Qt.Key_L: b"\x0C", # Ctrl-L (Form Feed)
|
QtCore.Qt.Key_L: b"\x0c", # Ctrl-L (Form Feed)
|
||||||
QtCore.Qt.Key_M: b"\x0D", # Ctrl-M (Carriage Return)
|
QtCore.Qt.Key_M: b"\x0d", # Ctrl-M (Carriage Return)
|
||||||
QtCore.Qt.Key_N: b"\x0E", # Ctrl-N
|
QtCore.Qt.Key_N: b"\x0e", # Ctrl-N
|
||||||
QtCore.Qt.Key_O: b"\x0F", # Ctrl-O
|
QtCore.Qt.Key_O: b"\x0f", # Ctrl-O
|
||||||
QtCore.Qt.Key_P: b"\x10", # Ctrl-P
|
QtCore.Qt.Key_P: b"\x10", # Ctrl-P
|
||||||
QtCore.Qt.Key_Q: b"\x11", # Ctrl-Q
|
QtCore.Qt.Key_Q: b"\x11", # Ctrl-Q
|
||||||
QtCore.Qt.Key_R: b"\x12", # Ctrl-R
|
QtCore.Qt.Key_R: b"\x12", # Ctrl-R
|
||||||
@@ -72,10 +72,10 @@ control_keys_mapping = {
|
|||||||
QtCore.Qt.Key_W: b"\x17", # Ctrl-W
|
QtCore.Qt.Key_W: b"\x17", # Ctrl-W
|
||||||
QtCore.Qt.Key_X: b"\x18", # Ctrl-X
|
QtCore.Qt.Key_X: b"\x18", # Ctrl-X
|
||||||
QtCore.Qt.Key_Y: b"\x19", # Ctrl-Y
|
QtCore.Qt.Key_Y: b"\x19", # Ctrl-Y
|
||||||
QtCore.Qt.Key_Z: b"\x1A", # Ctrl-Z
|
QtCore.Qt.Key_Z: b"\x1a", # Ctrl-Z
|
||||||
QtCore.Qt.Key_Escape: b"\x1B", # Ctrl-Escape
|
QtCore.Qt.Key_Escape: b"\x1b", # Ctrl-Escape
|
||||||
QtCore.Qt.Key_Backslash: b"\x1C", # Ctrl-\
|
QtCore.Qt.Key_Backslash: b"\x1c", # Ctrl-\
|
||||||
QtCore.Qt.Key_Underscore: b"\x1F", # Ctrl-_
|
QtCore.Qt.Key_Underscore: b"\x1f", # Ctrl-_
|
||||||
}
|
}
|
||||||
|
|
||||||
normal_keys_mapping = {
|
normal_keys_mapping = {
|
||||||
@@ -89,7 +89,7 @@ normal_keys_mapping = {
|
|||||||
QtCore.Qt.Key_Left: b"\x02",
|
QtCore.Qt.Key_Left: b"\x02",
|
||||||
QtCore.Qt.Key_Up: b"\x10",
|
QtCore.Qt.Key_Up: b"\x10",
|
||||||
QtCore.Qt.Key_Right: b"\x06",
|
QtCore.Qt.Key_Right: b"\x06",
|
||||||
QtCore.Qt.Key_Down: b"\x0E",
|
QtCore.Qt.Key_Down: b"\x0e",
|
||||||
QtCore.Qt.Key_PageUp: b"\x49",
|
QtCore.Qt.Key_PageUp: b"\x49",
|
||||||
QtCore.Qt.Key_PageDown: b"\x51",
|
QtCore.Qt.Key_PageDown: b"\x51",
|
||||||
QtCore.Qt.Key_F1: b"\x1b\x31",
|
QtCore.Qt.Key_F1: b"\x1b\x31",
|
||||||
|
|||||||
@@ -61,8 +61,8 @@ class ImageItem(BECConnector, pg.ImageItem):
|
|||||||
"fft.setter",
|
"fft.setter",
|
||||||
"log",
|
"log",
|
||||||
"log.setter",
|
"log.setter",
|
||||||
"rotation",
|
"num_rotation_90",
|
||||||
"rotation.setter",
|
"num_rotation_90.setter",
|
||||||
"transpose",
|
"transpose",
|
||||||
"transpose.setter",
|
"transpose.setter",
|
||||||
"get_data",
|
"get_data",
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ class PlotBase(BECWidget, QWidget):
|
|||||||
self._ui_mode = UIMode.POPUP if popups else UIMode.SIDE
|
self._ui_mode = UIMode.POPUP if popups else UIMode.SIDE
|
||||||
self.axis_settings_dialog = None
|
self.axis_settings_dialog = None
|
||||||
self.plot_widget = pg.GraphicsLayoutWidget(parent=self)
|
self.plot_widget = pg.GraphicsLayoutWidget(parent=self)
|
||||||
|
self.plot_widget.ci.setContentsMargins(0, 0, 0, 0)
|
||||||
self.plot_item = pg.PlotItem(viewBox=BECViewBox(enableMenu=True))
|
self.plot_item = pg.PlotItem(viewBox=BECViewBox(enableMenu=True))
|
||||||
self.plot_widget.addItem(self.plot_item)
|
self.plot_widget.addItem(self.plot_item)
|
||||||
self.side_panel = SidePanel(self, orientation="left", panel_max_width=280)
|
self.side_panel = SidePanel(self, orientation="left", panel_max_width=280)
|
||||||
@@ -795,6 +796,7 @@ class PlotBase(BECWidget, QWidget):
|
|||||||
"""
|
"""
|
||||||
self.plot_item.showAxis("top", value)
|
self.plot_item.showAxis("top", value)
|
||||||
self.plot_item.showAxis("right", value)
|
self.plot_item.showAxis("right", value)
|
||||||
|
|
||||||
self.property_changed.emit("outer_axes", value)
|
self.property_changed.emit("outer_axes", value)
|
||||||
|
|
||||||
@SafeProperty(bool, doc="Show inner axes of the plot widget.")
|
@SafeProperty(bool, doc="Show inner axes of the plot widget.")
|
||||||
@@ -814,6 +816,7 @@ class PlotBase(BECWidget, QWidget):
|
|||||||
"""
|
"""
|
||||||
self.plot_item.showAxis("bottom", value)
|
self.plot_item.showAxis("bottom", value)
|
||||||
self.plot_item.showAxis("left", value)
|
self.plot_item.showAxis("left", value)
|
||||||
|
|
||||||
self._apply_x_label()
|
self._apply_x_label()
|
||||||
self._apply_y_label()
|
self._apply_y_label()
|
||||||
self.property_changed.emit("inner_axes", value)
|
self.property_changed.emit("inner_axes", value)
|
||||||
|
|||||||
@@ -1182,10 +1182,11 @@ class Waveform(PlotBase):
|
|||||||
self.on_async_readback,
|
self.on_async_readback,
|
||||||
MessageEndpoints.device_async_readback(self.scan_id, name),
|
MessageEndpoints.device_async_readback(self.scan_id, name),
|
||||||
from_start=True,
|
from_start=True,
|
||||||
|
cb_info={"scan_id": self.scan_id},
|
||||||
)
|
)
|
||||||
logger.info(f"Setup async curve {name}")
|
logger.info(f"Setup async curve {name}")
|
||||||
|
|
||||||
@SafeSlot(dict, dict)
|
@SafeSlot(dict, dict, verify_sender=True)
|
||||||
def on_async_readback(self, msg, metadata):
|
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, therefor we try
|
||||||
@@ -1204,6 +1205,14 @@ class Waveform(PlotBase):
|
|||||||
msg(dict): Message with the async data.
|
msg(dict): Message with the async data.
|
||||||
metadata(dict): Metadata of the message.
|
metadata(dict): Metadata of the message.
|
||||||
"""
|
"""
|
||||||
|
sender = self.sender()
|
||||||
|
if not hasattr(sender, "cb_info"):
|
||||||
|
logger.info(f"Sender {sender} has no cb_info.")
|
||||||
|
return
|
||||||
|
scan_id = sender.cb_info.get("scan_id", None)
|
||||||
|
if scan_id != self.scan_id:
|
||||||
|
logger.info("Scan ID mismatch, ignoring async readback.")
|
||||||
|
|
||||||
instruction = metadata.get("async_update", {}).get("type")
|
instruction = metadata.get("async_update", {}).get("type")
|
||||||
if instruction not in ["add", "add_slice", "replace"]:
|
if instruction not in ["add", "add_slice", "replace"]:
|
||||||
logger.warning(f"Invalid async update instruction: {instruction}")
|
logger.warning(f"Invalid async update instruction: {instruction}")
|
||||||
@@ -1212,6 +1221,7 @@ class Waveform(PlotBase):
|
|||||||
plot_mode = self.x_axis_mode["name"]
|
plot_mode = self.x_axis_mode["name"]
|
||||||
for curve in self._async_curves:
|
for curve in self._async_curves:
|
||||||
x_data = None # Reset x_data
|
x_data = None # Reset x_data
|
||||||
|
y_data = None # Reset y_data
|
||||||
# Get the curve data
|
# Get the curve data
|
||||||
async_data = msg["signals"].get(curve.config.signal.entry, None)
|
async_data = msg["signals"].get(curve.config.signal.entry, None)
|
||||||
if async_data is None:
|
if async_data is None:
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ class BECProgressBar(BECWidget, QWidget):
|
|||||||
"set_minimum",
|
"set_minimum",
|
||||||
"label_template",
|
"label_template",
|
||||||
"label_template.setter",
|
"label_template.setter",
|
||||||
|
"_get_label",
|
||||||
]
|
]
|
||||||
ICON_NAME = "page_control"
|
ICON_NAME = "page_control"
|
||||||
|
|
||||||
@@ -235,6 +236,10 @@ class BECProgressBar(BECWidget, QWidget):
|
|||||||
(value - self._user_minimum) / (self._user_maximum - self._user_minimum) * self._maximum
|
(value - self._user_minimum) / (self._user_maximum - self._user_minimum) * self._maximum
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def _get_label(self) -> str:
|
||||||
|
"""Return the label text. mostly used for testing rpc."""
|
||||||
|
return self.center_label.text()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__": # pragma: no cover
|
if __name__ == "__main__": # pragma: no cover
|
||||||
app = QApplication(sys.argv)
|
app = QApplication(sys.argv)
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ class BECStatusBox(BECWidget, CompactPopupWidget):
|
|||||||
|
|
||||||
PLUGIN = True
|
PLUGIN = True
|
||||||
CORE_SERVICES = ["DeviceServer", "ScanServer", "SciHub", "ScanBundler", "FileWriterManager"]
|
CORE_SERVICES = ["DeviceServer", "ScanServer", "SciHub", "ScanBundler", "FileWriterManager"]
|
||||||
|
USER_ACCESS = ["get_server_state", "remove"]
|
||||||
|
|
||||||
service_update = Signal(BECServiceInfoContainer)
|
service_update = Signal(BECServiceInfoContainer)
|
||||||
bec_core_state = Signal(str)
|
bec_core_state = Signal(str)
|
||||||
@@ -134,6 +135,10 @@ class BECStatusBox(BECWidget, CompactPopupWidget):
|
|||||||
"QTreeWidget::item:selected {}"
|
"QTreeWidget::item:selected {}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_server_state(self) -> str:
|
||||||
|
"""Get the state ("RUNNING", "BUSY", "IDLE", "ERROR") of the BEC server"""
|
||||||
|
return self.status_container[self.box_name]["info"].status
|
||||||
|
|
||||||
def _create_status_widget(
|
def _create_status_widget(
|
||||||
self, service_name: str, status=BECStatus, info: dict = None, metrics: dict = None
|
self, service_name: str, status=BECStatus, info: dict = None, metrics: dict = None
|
||||||
) -> StatusItem:
|
) -> StatusItem:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
""" Module for a StatusItem widget to display status and metrics for a BEC service.
|
"""Module for a StatusItem widget to display status and metrics for a BEC service.
|
||||||
The widget is bound to be used with the BECStatusBox widget."""
|
The widget is bound to be used with the BECStatusBox widget."""
|
||||||
|
|
||||||
import enum
|
import enum
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
""" Utilities for filtering and formatting in the LogPanel"""
|
"""Utilities for filtering and formatting in the LogPanel"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from pyqtgraph.widgets.ColorMapButton import ColorMapButton
|
from pyqtgraph.widgets.ColorMapButton import ColorMapButton
|
||||||
|
from qtpy import QtCore, QtGui
|
||||||
from qtpy.QtCore import Property, Signal, Slot
|
from qtpy.QtCore import Property, Signal, Slot
|
||||||
from qtpy.QtWidgets import QSizePolicy, QVBoxLayout, QWidget
|
from qtpy.QtWidgets import QSizePolicy, QVBoxLayout, QWidget
|
||||||
|
|
||||||
@@ -6,6 +7,23 @@ from bec_widgets.utils import Colors
|
|||||||
from bec_widgets.utils.bec_widget import BECWidget
|
from bec_widgets.utils.bec_widget import BECWidget
|
||||||
|
|
||||||
|
|
||||||
|
class RoundedColorMapButton(ColorMapButton):
|
||||||
|
"""Thin wrapper around pyqtgraph ColorMapButton to add rounded clipping."""
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.setAttribute(QtCore.Qt.WA_TranslucentBackground)
|
||||||
|
|
||||||
|
def paintEvent(self, evt):
|
||||||
|
painter = QtGui.QPainter(self)
|
||||||
|
painter.setRenderHint(QtGui.QPainter.Antialiasing)
|
||||||
|
path = QtGui.QPainterPath()
|
||||||
|
path.addRoundedRect(self.rect(), 8, 8)
|
||||||
|
painter.setClipPath(path)
|
||||||
|
self.paintColorMap(painter, self.contentsRect())
|
||||||
|
painter.end()
|
||||||
|
|
||||||
|
|
||||||
class BECColorMapWidget(BECWidget, QWidget):
|
class BECColorMapWidget(BECWidget, QWidget):
|
||||||
colormap_changed_signal = Signal(str)
|
colormap_changed_signal = Signal(str)
|
||||||
ICON_NAME = "palette"
|
ICON_NAME = "palette"
|
||||||
@@ -15,7 +33,7 @@ class BECColorMapWidget(BECWidget, QWidget):
|
|||||||
def __init__(self, parent=None, cmap: str = "plasma", **kwargs):
|
def __init__(self, parent=None, cmap: str = "plasma", **kwargs):
|
||||||
super().__init__(parent=parent, **kwargs)
|
super().__init__(parent=parent, **kwargs)
|
||||||
# Create the ColorMapButton
|
# Create the ColorMapButton
|
||||||
self.button = ColorMapButton()
|
self.button = RoundedColorMapButton()
|
||||||
|
|
||||||
# Set the size policy and minimum width
|
# Set the size policy and minimum width
|
||||||
size_policy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
|
size_policy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.Fixed)
|
||||||
|
|||||||
+3
-2
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "bec_widgets"
|
name = "bec_widgets"
|
||||||
version = "2.0.1"
|
version = "2.3.0"
|
||||||
description = "BEC Widgets"
|
description = "BEC Widgets"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
classifiers = [
|
classifiers = [
|
||||||
@@ -16,7 +16,7 @@ dependencies = [
|
|||||||
"bec_ipython_client>=2.21.4, <=4.0", # needed for jupyter console
|
"bec_ipython_client>=2.21.4, <=4.0", # needed for jupyter console
|
||||||
"bec_lib>=3.29, <=4.0",
|
"bec_lib>=3.29, <=4.0",
|
||||||
"bec_qthemes~=0.7, >=0.7",
|
"bec_qthemes~=0.7, >=0.7",
|
||||||
"black~=24.0", # needed for bw-generate-cli
|
"black~=25.0", # needed for bw-generate-cli
|
||||||
"isort~=5.13, >=5.13.2", # needed for bw-generate-cli
|
"isort~=5.13, >=5.13.2", # needed for bw-generate-cli
|
||||||
"pydantic~=2.0",
|
"pydantic~=2.0",
|
||||||
"pyqtgraph~=0.13",
|
"pyqtgraph~=0.13",
|
||||||
@@ -31,6 +31,7 @@ dependencies = [
|
|||||||
dev = [
|
dev = [
|
||||||
"coverage~=7.0",
|
"coverage~=7.0",
|
||||||
"fakeredis~=2.23, >=2.23.2",
|
"fakeredis~=2.23, >=2.23.2",
|
||||||
|
"isort~=5.13, >=5.13.2",
|
||||||
"pytest-bec-e2e>=2.21.4, <=4.0",
|
"pytest-bec-e2e>=2.21.4, <=4.0",
|
||||||
"pytest-qt~=4.4",
|
"pytest-qt~=4.4",
|
||||||
"pytest-random-order~=1.1",
|
"pytest-random-order~=1.1",
|
||||||
|
|||||||
@@ -4,8 +4,7 @@ import random
|
|||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from bec_widgets.cli.client_utils import BECGuiClient, _start_plot_process
|
from bec_widgets.cli.client_utils import BECGuiClient
|
||||||
from bec_widgets.utils import BECDispatcher
|
|
||||||
|
|
||||||
# pylint: disable=unused-argument
|
# pylint: disable=unused-argument
|
||||||
# pylint: disable=redefined-outer-name
|
# pylint: disable=redefined-outer-name
|
||||||
@@ -28,7 +27,7 @@ def gui_id():
|
|||||||
return f"figure_{random.randint(0,100)}" # make a new gui id each time, to ensure no 'gui is alive' zombie key can perturbate
|
return f"figure_{random.randint(0,100)}" # make a new gui id each time, to ensure no 'gui is alive' zombie key can perturbate
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture(scope="function")
|
||||||
def connected_client_gui_obj(qtbot, gui_id, bec_client_lib):
|
def connected_client_gui_obj(qtbot, gui_id, bec_client_lib):
|
||||||
"""
|
"""
|
||||||
Fixture to create a new BECGuiClient object and start a server in the background.
|
Fixture to create a new BECGuiClient object and start a server in the background.
|
||||||
@@ -42,22 +41,3 @@ def connected_client_gui_obj(qtbot, gui_id, bec_client_lib):
|
|||||||
yield gui
|
yield gui
|
||||||
finally:
|
finally:
|
||||||
gui.kill_server()
|
gui.kill_server()
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope="session")
|
|
||||||
def connected_gui_with_scope_session(qtbot, gui_id, bec_client_lib):
|
|
||||||
"""
|
|
||||||
Fixture to create a new BECGuiClient object and start a server in the background.
|
|
||||||
|
|
||||||
This fixture is scoped to the session, meaning it remains alive for all tests in the session.
|
|
||||||
We can use this fixture to create a gui object that is used across multiple tests, and
|
|
||||||
simulate a real-world scenario where the gui is not restarted for each test.
|
|
||||||
"""
|
|
||||||
gui = BECGuiClient(gui_id=gui_id)
|
|
||||||
try:
|
|
||||||
gui.start(wait=True)
|
|
||||||
# After the server started, we need to wait until the bec exists in the namespace
|
|
||||||
qtbot.waitUntil(lambda: hasattr(gui, "bec"), timeout=5000)
|
|
||||||
yield gui
|
|
||||||
finally:
|
|
||||||
gui.kill_server()
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
Test module for the gui object within the BEC IPython client.
|
Test module for the gui object within the BEC IPython client.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from unittest import mock
|
from unittest import mock
|
||||||
|
|||||||
@@ -136,7 +136,7 @@ def test_async_plotting(qtbot, bec_client_lib, connected_client_gui_obj):
|
|||||||
dev.waveform.sim.select_model("GaussianModel")
|
dev.waveform.sim.select_model("GaussianModel")
|
||||||
dev.waveform.sim.params = {"amplitude": 1000, "center": 4000, "sigma": 300}
|
dev.waveform.sim.params = {"amplitude": 1000, "center": 4000, "sigma": 300}
|
||||||
dev.waveform.async_update.set("add").wait()
|
dev.waveform.async_update.set("add").wait()
|
||||||
dev.waveform.waveform_shape.set(1000).wait()
|
dev.waveform.waveform_shape.set(10000).wait()
|
||||||
wf = dock.new("wf_dock").new("Waveform")
|
wf = dock.new("wf_dock").new("Waveform")
|
||||||
curve = wf.plot(y_name="waveform")
|
curve = wf.plot(y_name="waveform")
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ from bec_widgets.cli.rpc.rpc_base import RPCReference
|
|||||||
|
|
||||||
def test_rpc_reference_objects(connected_client_gui_obj):
|
def test_rpc_reference_objects(connected_client_gui_obj):
|
||||||
gui = connected_client_gui_obj
|
gui = connected_client_gui_obj
|
||||||
dock = gui.window_list[0].new("dock")
|
dock = gui.window_list[0].new()
|
||||||
plt = dock.new(name="fig", widget="Waveform")
|
plt = dock.new(name="fig", widget="Waveform")
|
||||||
|
|
||||||
plt.plot(x_name="samx", y_name="bpm4i")
|
plt.plot(x_name="samx", y_name="bpm4i")
|
||||||
|
|||||||
@@ -0,0 +1,82 @@
|
|||||||
|
"""
|
||||||
|
End-2-End test fixtures for module scoped testing. The fixtures overwrite the default versions used
|
||||||
|
for the function scoped tests. The fixtures will only be created once for this entire module, meaning
|
||||||
|
that any test can be used to test user interaction and potential leakage of threads or other resources across
|
||||||
|
different widgets.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import random
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from bec_ipython_client import BECIPythonClient
|
||||||
|
from bec_lib.redis_connector import RedisConnector
|
||||||
|
from bec_lib.service_config import ServiceConfig
|
||||||
|
from bec_lib.tests.utils import wait_for_empty_queue
|
||||||
|
from pytestqt.plugin import QtBot
|
||||||
|
|
||||||
|
from bec_widgets.cli.client_utils import BECGuiClient
|
||||||
|
|
||||||
|
# pylint: disable=unused-argument
|
||||||
|
# pylint: disable=redefined-outer-name
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def gui_id():
|
||||||
|
"""New gui id each time, to ensure no 'gui is alive' zombie key can perturbate"""
|
||||||
|
return f"figure_{random.randint(0,100)}"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def bec_ipython_client_with_demo_config(
|
||||||
|
bec_redis_fixture, bec_services_config_file_path, bec_servers
|
||||||
|
):
|
||||||
|
"""Fixture to create a BECIPythonClient with a demo config."""
|
||||||
|
config = ServiceConfig(bec_services_config_file_path)
|
||||||
|
bec = BECIPythonClient(config, RedisConnector, forced=True)
|
||||||
|
bec.start()
|
||||||
|
bec.config.load_demo_config()
|
||||||
|
try:
|
||||||
|
yield bec
|
||||||
|
finally:
|
||||||
|
bec.shutdown()
|
||||||
|
bec._client._reset_singleton()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def bec_client_lib(bec_ipython_client_with_demo_config):
|
||||||
|
"""Fixture to create a BECIPythonClient with a demo config."""
|
||||||
|
bec = bec_ipython_client_with_demo_config
|
||||||
|
bec.queue.request_queue_reset()
|
||||||
|
bec.queue.request_scan_continuation()
|
||||||
|
wait_for_empty_queue(bec)
|
||||||
|
yield bec
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def qtbot_scope_module(qapp, request):
|
||||||
|
"""
|
||||||
|
Fixture used to create a QtBot instance for using during testing.
|
||||||
|
|
||||||
|
Make sure to call addWidget for each top-level widget you create to ensure
|
||||||
|
that they are properly closed after the test ends.
|
||||||
|
"""
|
||||||
|
result = QtBot(request)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def connected_client_gui_obj(qtbot_scope_module, gui_id, bec_client_lib):
|
||||||
|
"""
|
||||||
|
Fixture to create a new BECGuiClient object and start a server in the background.
|
||||||
|
|
||||||
|
This fixture is scoped to the session, meaning it remains alive for all tests in the session.
|
||||||
|
We can use this fixture to create a gui object that is used across multiple tests, and
|
||||||
|
simulate a real-world scenario where the gui is not restarted for each test.
|
||||||
|
"""
|
||||||
|
gui = BECGuiClient(gui_id=gui_id)
|
||||||
|
try:
|
||||||
|
gui.start(wait=True)
|
||||||
|
qtbot_scope_module.waitUntil(lambda: hasattr(gui, "bec"), timeout=5000)
|
||||||
|
yield gui
|
||||||
|
finally:
|
||||||
|
gui.kill_server()
|
||||||
@@ -0,0 +1,667 @@
|
|||||||
|
"""
|
||||||
|
End-to-end tests single gui instance across the full session.
|
||||||
|
|
||||||
|
Each test will use the same gui instance, simulating a real-world scenario where the gui is not
|
||||||
|
restarted for each test. The interaction is tested through the rpc calls.
|
||||||
|
|
||||||
|
Note: wait_for_namespace_created is a utility method that helps to wait for the namespace to be
|
||||||
|
created in the gui. This is necessary because the rpc calls are asynchronous and the namespace
|
||||||
|
may not be created immediately after the rpc call is made.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import random
|
||||||
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from bec_widgets.cli.client import BECDockArea
|
||||||
|
from bec_widgets.cli.rpc.rpc_base import RPCBase, RPCReference
|
||||||
|
|
||||||
|
PYTEST_TIMEOUT = 50
|
||||||
|
|
||||||
|
if TYPE_CHECKING: # pragma: no cover
|
||||||
|
from bec_widgets.cli import client
|
||||||
|
from bec_widgets.cli.client_utils import BECGuiClient
|
||||||
|
|
||||||
|
# pylint: disable=redefined-outer-name
|
||||||
|
# pylint: disable=too-many-arguments
|
||||||
|
# pylint: disable=protected-access
|
||||||
|
# pylint: disable=unused-variable
|
||||||
|
|
||||||
|
|
||||||
|
def wait_for_namespace_change(
|
||||||
|
qtbot,
|
||||||
|
gui: BECGuiClient,
|
||||||
|
parent_widget: RPCBase | RPCReference,
|
||||||
|
object_name: str,
|
||||||
|
widget_gui_id: str,
|
||||||
|
timeout: float = 10000,
|
||||||
|
exists: bool = True,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Utility method to wait for the namespace to be created in the widget.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
qtbot: The qtbot fixture.
|
||||||
|
gui: The client_utils.BECGuiClient 'gui' object from the CLI.
|
||||||
|
parent_widget: The widget that creates a new widget.
|
||||||
|
object_name: The name of the widget that was created. Must appear as attribute in namespace of parent.
|
||||||
|
widget_gui_id: The gui_id of the created widget.
|
||||||
|
timeout: The timeout in milliseconds for the qtbot to wait for changes to appear.
|
||||||
|
exists: If True, wait for the object to be created. If False, wait for the object to be removed.
|
||||||
|
"""
|
||||||
|
# GUI object is not registered in the registry (yet)
|
||||||
|
if parent_widget is gui:
|
||||||
|
|
||||||
|
def check_reference_registered():
|
||||||
|
# Check server registry
|
||||||
|
obj = gui._server_registry.get(widget_gui_id, None)
|
||||||
|
if obj is None:
|
||||||
|
if not exists:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
# CHeck Ipython registry
|
||||||
|
obj = gui._ipython_registry.get(widget_gui_id, None)
|
||||||
|
if obj is None:
|
||||||
|
if not exists:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
def check_reference_registered():
|
||||||
|
# Check server registry
|
||||||
|
obj = gui._server_registry.get(widget_gui_id, None)
|
||||||
|
if obj is None:
|
||||||
|
if not exists:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
# CHeck Ipython registry
|
||||||
|
obj = gui._ipython_registry.get(widget_gui_id, None)
|
||||||
|
if obj is None:
|
||||||
|
if not exists:
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
# Check reference registry
|
||||||
|
ref = parent_widget._rpc_references.get(widget_gui_id, None)
|
||||||
|
if exists:
|
||||||
|
return ref is not None
|
||||||
|
return ref is None
|
||||||
|
|
||||||
|
try:
|
||||||
|
qtbot.waitUntil(check_reference_registered, timeout=timeout)
|
||||||
|
except Exception as e:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Timeout waiting for {parent_widget.object_name}.{object_name} to be created."
|
||||||
|
) from e
|
||||||
|
|
||||||
|
|
||||||
|
def create_widget(
|
||||||
|
qtbot, gui: BECGuiClient, widget_cls_name: str
|
||||||
|
) -> tuple[RPCReference, RPCReference]:
|
||||||
|
"""Utility method to create a widget and wait for the namespaces to be created."""
|
||||||
|
if hasattr(gui, "dock_area"):
|
||||||
|
dock_area: client.BECDockArea = gui.dock_area
|
||||||
|
else:
|
||||||
|
dock_area: client.BECDockArea = gui.new(name="dock_area")
|
||||||
|
wait_for_namespace_change(qtbot, gui, gui, dock_area.object_name, dock_area._gui_id)
|
||||||
|
dock: client.BECDock = dock_area.new()
|
||||||
|
wait_for_namespace_change(qtbot, gui, dock_area, dock.object_name, dock._gui_id)
|
||||||
|
widget = dock.new(widget=widget_cls_name)
|
||||||
|
wait_for_namespace_change(qtbot, gui, dock, widget.object_name, widget._gui_id)
|
||||||
|
return dock, widget
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="module")
|
||||||
|
def random_generator_from_seed(request):
|
||||||
|
"""Fixture to get a random seed for the following tests."""
|
||||||
|
seed = request.config.getoption("--random-order-seed").split(":")[-1]
|
||||||
|
try:
|
||||||
|
seed = int(seed)
|
||||||
|
except ValueError: # Should not be required...
|
||||||
|
seed = 42
|
||||||
|
rng = random.Random(seed)
|
||||||
|
yield rng
|
||||||
|
|
||||||
|
|
||||||
|
def maybe_remove_dock_area(qtbot, gui: BECGuiClient, random_int_gen: random.Random):
|
||||||
|
"""Utility method to remove all dock_ares from gui object, likelihood 50%."""
|
||||||
|
random_int = random_int_gen.randint(0, 100)
|
||||||
|
if random_int >= 50:
|
||||||
|
# Needed, reference gets deleted in the gui
|
||||||
|
name = gui.dock_area.object_name
|
||||||
|
gui_id = gui.dock_area._gui_id
|
||||||
|
gui.delete("dock_area")
|
||||||
|
wait_for_namespace_change(
|
||||||
|
qtbot, gui=gui, parent_widget=gui, object_name=name, widget_gui_id=gui_id, exists=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.timeout(PYTEST_TIMEOUT)
|
||||||
|
def test_widgets_e2e_abort_button(qtbot, connected_client_gui_obj, random_generator_from_seed):
|
||||||
|
"""Test the AbortButton widget."""
|
||||||
|
gui: BECGuiClient = connected_client_gui_obj
|
||||||
|
bec = gui._client
|
||||||
|
# Create dock_area, dock, widget
|
||||||
|
dock, widget = create_widget(qtbot, gui, gui.available_widgets.AbortButton)
|
||||||
|
dock: client.BECDock
|
||||||
|
widget: client.AbortButton
|
||||||
|
|
||||||
|
# No rpc calls to check so far
|
||||||
|
|
||||||
|
# Try detaching the dock
|
||||||
|
dock.detach()
|
||||||
|
|
||||||
|
# Test removing the widget, or leaving it open for the next test
|
||||||
|
maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.timeout(PYTEST_TIMEOUT)
|
||||||
|
def test_widgets_e2e_bec_progress_bar(qtbot, connected_client_gui_obj, random_generator_from_seed):
|
||||||
|
"""Test the BECProgressBar widget."""
|
||||||
|
gui = connected_client_gui_obj
|
||||||
|
bec = gui._client
|
||||||
|
# Create dock_area, dock, widget
|
||||||
|
dock, widget = create_widget(qtbot, gui, gui.available_widgets.BECProgressBar)
|
||||||
|
dock: client.BECDock
|
||||||
|
widget: client.BECProgressBar
|
||||||
|
|
||||||
|
# Check rpc calls
|
||||||
|
assert widget.label_template == "$value / $maximum - $percentage %"
|
||||||
|
widget.set_maximum(100)
|
||||||
|
widget.set_minimum(50)
|
||||||
|
widget.set_value(75)
|
||||||
|
|
||||||
|
assert widget._get_label() == "75 / 100 - 50 %"
|
||||||
|
|
||||||
|
# Test removing the widget, or leaving it open for the next test
|
||||||
|
maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.timeout(PYTEST_TIMEOUT)
|
||||||
|
def test_widgets_e2e_bec_queue(qtbot, connected_client_gui_obj, random_generator_from_seed):
|
||||||
|
"""Test the BECQueue widget."""
|
||||||
|
gui = connected_client_gui_obj
|
||||||
|
bec = gui._client
|
||||||
|
# Create dock_area, dock, widget
|
||||||
|
dock, widget = create_widget(qtbot, gui, gui.available_widgets.BECQueue)
|
||||||
|
dock: client.BECDock
|
||||||
|
widget: client.BECQueue
|
||||||
|
|
||||||
|
# No rpc calls to test so far
|
||||||
|
# maybe we can add an rpc call to check the queue length
|
||||||
|
|
||||||
|
# Test removing the widget, or leaving it open for the next test
|
||||||
|
maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.timeout(PYTEST_TIMEOUT)
|
||||||
|
def test_widgets_e2e_bec_status_box(qtbot, connected_client_gui_obj, random_generator_from_seed):
|
||||||
|
"""Test the BECStatusBox widget."""
|
||||||
|
gui = connected_client_gui_obj
|
||||||
|
bec = gui._client
|
||||||
|
# Create dock_area, dock, widget
|
||||||
|
dock, widget = create_widget(qtbot, gui, gui.available_widgets.BECStatusBox)
|
||||||
|
|
||||||
|
# Check rpc calls
|
||||||
|
assert widget.get_server_state() in ["RUNNING", "IDLE", "BUSY", "ERROR"]
|
||||||
|
|
||||||
|
# Test removing the widget, or leaving it open for the next test
|
||||||
|
maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.timeout(PYTEST_TIMEOUT)
|
||||||
|
def test_widgets_e2e_dap_combo_box(qtbot, connected_client_gui_obj, random_generator_from_seed):
|
||||||
|
"""Test the DAPComboBox widget."""
|
||||||
|
gui = connected_client_gui_obj
|
||||||
|
bec = gui._client
|
||||||
|
# Create dock_area, dock, widget
|
||||||
|
dock, widget = create_widget(qtbot, gui, gui.available_widgets.DapComboBox)
|
||||||
|
dock: client.BECDock
|
||||||
|
widget: client.DAPComboBox
|
||||||
|
|
||||||
|
# Check rpc calls
|
||||||
|
widget.select_fit_model("PseudoVoigtModel")
|
||||||
|
widget.select_x_axis("samx")
|
||||||
|
widget.select_y_axis("bpm4i")
|
||||||
|
|
||||||
|
# Test removing the widget, or leaving it open for the next test
|
||||||
|
maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.timeout(PYTEST_TIMEOUT)
|
||||||
|
def test_widgets_e2e_device_browser(qtbot, connected_client_gui_obj, random_generator_from_seed):
|
||||||
|
"""Test the DeviceBrowser widget."""
|
||||||
|
gui = connected_client_gui_obj
|
||||||
|
bec = gui._client
|
||||||
|
# Create dock_area, dock, widget
|
||||||
|
dock, widget = create_widget(qtbot, gui, gui.available_widgets.DeviceBrowser)
|
||||||
|
dock: client.BECDock
|
||||||
|
widget: client.DeviceBrowser
|
||||||
|
|
||||||
|
# No rpc calls yet to check
|
||||||
|
|
||||||
|
# Test removing the widget, or leaving it open for the next test
|
||||||
|
maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.timeout(PYTEST_TIMEOUT)
|
||||||
|
def test_widgets_e2e_device_combo_box(qtbot, connected_client_gui_obj, random_generator_from_seed):
|
||||||
|
"""Test the DeviceComboBox widget."""
|
||||||
|
gui = connected_client_gui_obj
|
||||||
|
bec = gui._client
|
||||||
|
# Create dock_area, dock, widget
|
||||||
|
dock, widget = create_widget(qtbot, gui, gui.available_widgets.DeviceComboBox)
|
||||||
|
dock: client.BECDock
|
||||||
|
widget: client.DeviceComboBox
|
||||||
|
|
||||||
|
# No rpc calls to check so far, maybe set_device should be exposed
|
||||||
|
|
||||||
|
# Test removing the widget, or leaving it open for the next test
|
||||||
|
maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.timeout(PYTEST_TIMEOUT)
|
||||||
|
def test_widgets_e2e_device_line_edit(qtbot, connected_client_gui_obj, random_generator_from_seed):
|
||||||
|
"""Test the DeviceLineEdit widget."""
|
||||||
|
gui = connected_client_gui_obj
|
||||||
|
bec = gui._client
|
||||||
|
# Create dock_area, dock, widget
|
||||||
|
dock, widget = create_widget(qtbot, gui, gui.available_widgets.DeviceLineEdit)
|
||||||
|
dock: client.BECDock
|
||||||
|
widget: client.DeviceLineEdit
|
||||||
|
|
||||||
|
# No rpc calls to check so far
|
||||||
|
# Should probably have a set_device method
|
||||||
|
|
||||||
|
# No rpc calls to check so far, maybe set_device should be exposed
|
||||||
|
|
||||||
|
# Test removing the widget, or leaving it open for the next test
|
||||||
|
maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.timeout(PYTEST_TIMEOUT)
|
||||||
|
def test_widgets_e2e_image(qtbot, connected_client_gui_obj, random_generator_from_seed):
|
||||||
|
"""Test the Image widget."""
|
||||||
|
gui = connected_client_gui_obj
|
||||||
|
bec = gui._client
|
||||||
|
# Create dock_area, dock, widget
|
||||||
|
dock, widget = create_widget(qtbot, gui, gui.available_widgets.Image)
|
||||||
|
dock: client.BECDock
|
||||||
|
widget: client.Image
|
||||||
|
|
||||||
|
scans = bec.scans
|
||||||
|
dev = bec.device_manager.devices
|
||||||
|
# Test rpc calls
|
||||||
|
img = widget.image(dev.eiger)
|
||||||
|
assert img.get_data() is None
|
||||||
|
# Run a scan and plot the image
|
||||||
|
s = scans.line_scan(dev.samx, -3, 3, steps=50, exp_time=0.01, relative=False)
|
||||||
|
s.wait()
|
||||||
|
|
||||||
|
def _wait_for_scan_in_history():
|
||||||
|
# Get scan item from history
|
||||||
|
scan_item = bec.history.get_by_scan_id(s.scan.scan_id)
|
||||||
|
return scan_item is not None
|
||||||
|
|
||||||
|
qtbot.waitUntil(_wait_for_scan_in_history, timeout=7000)
|
||||||
|
|
||||||
|
# Check that last image is equivalent to data in Redis
|
||||||
|
last_img = bec.device_monitor.get_data(
|
||||||
|
dev.eiger, count=1
|
||||||
|
) # Get last image from Redis monitor 2D endpoint
|
||||||
|
assert np.allclose(img.get_data(), last_img)
|
||||||
|
|
||||||
|
# Test removing the widget, or leaving it open for the next test
|
||||||
|
maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
|
||||||
|
|
||||||
|
|
||||||
|
# TODO re-enable when issue is resolved #560
|
||||||
|
# @pytest.mark.timeout(PYTEST_TIMEOUT)
|
||||||
|
# def test_widgets_e2e_log_panel(qtbot, connected_client_gui_obj, random_generator_from_seed):
|
||||||
|
# """Test the LogPanel widget."""
|
||||||
|
# gui = connected_client_gui_obj
|
||||||
|
# bec = gui._client
|
||||||
|
# # Create dock_area, dock, widget
|
||||||
|
# dock, widget = create_widget(qtbot, gui, gui.available_widgets.LogPanel)
|
||||||
|
# dock: client.BECDock
|
||||||
|
# widget: client.LogPanel
|
||||||
|
|
||||||
|
# # No rpc calls to check so far
|
||||||
|
|
||||||
|
# # Test removing the widget, or leaving it open for the next test
|
||||||
|
# maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.timeout(PYTEST_TIMEOUT)
|
||||||
|
def test_widgets_e2e_minesweeper(qtbot, connected_client_gui_obj, random_generator_from_seed):
|
||||||
|
"""Test the MineSweeper widget."""
|
||||||
|
gui = connected_client_gui_obj
|
||||||
|
bec = gui._client
|
||||||
|
# Create dock_area, dock, widget
|
||||||
|
dock, widget = create_widget(qtbot, gui, gui.available_widgets.Minesweeper)
|
||||||
|
dock: client.BECDock
|
||||||
|
widget: client.MineSweeper
|
||||||
|
|
||||||
|
# No rpc calls to check so far
|
||||||
|
|
||||||
|
# Test removing the widget, or leaving it open for the next test
|
||||||
|
maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.timeout(PYTEST_TIMEOUT)
|
||||||
|
def test_widgets_e2e_motor_map(qtbot, connected_client_gui_obj, random_generator_from_seed):
|
||||||
|
"""Test the MotorMap widget."""
|
||||||
|
gui = connected_client_gui_obj
|
||||||
|
bec = gui._client
|
||||||
|
# Create dock_area, dock, widget
|
||||||
|
dock, widget = create_widget(qtbot, gui, gui.available_widgets.MotorMap)
|
||||||
|
dock: client.BECDock
|
||||||
|
widget: client.MotorMap
|
||||||
|
|
||||||
|
# Test RPC calls
|
||||||
|
dev = bec.device_manager.devices
|
||||||
|
scans = bec.scans
|
||||||
|
# Set motor map to names
|
||||||
|
widget.map(dev.samx, dev.samy)
|
||||||
|
# Move motor samx to pos
|
||||||
|
pos = dev.samx.limits[1] - 1 # -1 from higher limit
|
||||||
|
scans.mv(dev.samx, pos, relative=False).wait()
|
||||||
|
# Check that data is up to date
|
||||||
|
assert np.isclose(widget.get_data()["x"][-1], pos, dev.samx.precision)
|
||||||
|
# Move motor samy to pos
|
||||||
|
pos = dev.samy.limits[0] + 1 # +1 from lower limit
|
||||||
|
scans.mv(dev.samy, pos, relative=False).wait()
|
||||||
|
# Check that data is up to date
|
||||||
|
assert np.isclose(widget.get_data()["y"][-1], pos, dev.samy.precision)
|
||||||
|
|
||||||
|
# Test removing the widget, or leaving it open for the next test
|
||||||
|
maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.timeout(PYTEST_TIMEOUT)
|
||||||
|
def test_widgets_e2e_multi_waveform(qtbot, connected_client_gui_obj, random_generator_from_seed):
|
||||||
|
"""Test MultiWaveform widget."""
|
||||||
|
gui = connected_client_gui_obj
|
||||||
|
bec = gui._client
|
||||||
|
# Create dock_area, dock, widget
|
||||||
|
dock, widget = create_widget(qtbot, gui, gui.available_widgets.MultiWaveform)
|
||||||
|
dock: client.BECDock
|
||||||
|
widget: client.MultiWaveform
|
||||||
|
|
||||||
|
# Test RPC calls
|
||||||
|
dev = bec.device_manager.devices
|
||||||
|
scans = bec.scans
|
||||||
|
# test plotting
|
||||||
|
cm = "cividis"
|
||||||
|
widget.plot(dev.waveform, color_palette=cm)
|
||||||
|
assert widget.monitor == dev.waveform.name
|
||||||
|
assert widget.color_palette == cm
|
||||||
|
|
||||||
|
# Scan with BEC
|
||||||
|
s = scans.line_scan(dev.samx, -3, 3, steps=5, exp_time=0.01, relative=False)
|
||||||
|
s.wait()
|
||||||
|
|
||||||
|
def _wait_for_scan_in_history():
|
||||||
|
# Get scan item from history
|
||||||
|
scan_item = bec.history.get_by_scan_id(s.scan.scan_id)
|
||||||
|
return scan_item is not None
|
||||||
|
|
||||||
|
qtbot.waitUntil(_wait_for_scan_in_history, timeout=7000)
|
||||||
|
# Wait for data in history (should be plotted?)
|
||||||
|
|
||||||
|
# TODO how can we check that the data was plotted, implement get_data()
|
||||||
|
|
||||||
|
# Test removing the widget, or leaving it open for the next test
|
||||||
|
maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.timeout(PYTEST_TIMEOUT)
|
||||||
|
def test_widgets_e2e_positioner_indicator(
|
||||||
|
qtbot, connected_client_gui_obj, random_generator_from_seed
|
||||||
|
):
|
||||||
|
"""Test the PositionIndicator widget."""
|
||||||
|
gui = connected_client_gui_obj
|
||||||
|
bec = gui._client
|
||||||
|
# Create dock_area, dock, widget
|
||||||
|
dock, widget = create_widget(qtbot, gui, gui.available_widgets.PositionIndicator)
|
||||||
|
dock: client.BECDock
|
||||||
|
widget: client.PositionIndicator
|
||||||
|
|
||||||
|
# TODO check what these rpc calls are supposed to do! Issue created #461
|
||||||
|
widget.set_value(5)
|
||||||
|
|
||||||
|
# Test removing the widget, or leaving it open for the next test
|
||||||
|
maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.timeout(PYTEST_TIMEOUT)
|
||||||
|
def test_widgets_e2e_positioner_box(qtbot, connected_client_gui_obj, random_generator_from_seed):
|
||||||
|
"""Test the PositionerBox widget."""
|
||||||
|
gui = connected_client_gui_obj
|
||||||
|
bec = gui._client
|
||||||
|
# Create dock_area, dock, widget
|
||||||
|
dock, widget = create_widget(qtbot, gui, gui.available_widgets.PositionerBox)
|
||||||
|
dock: client.BECDock
|
||||||
|
widget: client.PositionerBox
|
||||||
|
|
||||||
|
# Test rpc calls
|
||||||
|
dev = bec.device_manager.devices
|
||||||
|
scans = bec.scans
|
||||||
|
# No rpc calls to check so far
|
||||||
|
widget.set_positioner(dev.samx)
|
||||||
|
widget.set_positioner(dev.samy.name)
|
||||||
|
|
||||||
|
scans.mv(dev.samy, -3, relative=False).wait()
|
||||||
|
|
||||||
|
# Test removing the widget, or leaving it open for the next test
|
||||||
|
maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.timeout(PYTEST_TIMEOUT)
|
||||||
|
def test_widgets_e2e_positioner_box_2d(qtbot, connected_client_gui_obj, random_generator_from_seed):
|
||||||
|
"""Test the PositionerBox2D widget."""
|
||||||
|
gui = connected_client_gui_obj
|
||||||
|
bec = gui._client
|
||||||
|
# Create dock_area, dock, widget
|
||||||
|
dock, widget = create_widget(qtbot, gui, gui.available_widgets.PositionerBox2D)
|
||||||
|
dock: client.BECDock
|
||||||
|
widget: client.PositionerBox2D
|
||||||
|
|
||||||
|
# Test rpc calls
|
||||||
|
dev = bec.device_manager.devices
|
||||||
|
scans = bec.scans
|
||||||
|
# No rpc calls to check so far
|
||||||
|
widget.set_positioner_hor(dev.samx)
|
||||||
|
widget.set_positioner_ver(dev.samy)
|
||||||
|
|
||||||
|
# Try moving the motors
|
||||||
|
scans.mv(dev.samx, 3, relative=False).wait()
|
||||||
|
scans.mv(dev.samy, -3, relative=False).wait()
|
||||||
|
|
||||||
|
# Test removing the widget, or leaving it open for the next test
|
||||||
|
maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.timeout(PYTEST_TIMEOUT)
|
||||||
|
def test_widgets_e2e_positioner_control_line(
|
||||||
|
qtbot, connected_client_gui_obj, random_generator_from_seed
|
||||||
|
):
|
||||||
|
"""Test the positioner control line widget"""
|
||||||
|
gui = connected_client_gui_obj
|
||||||
|
bec = gui._client
|
||||||
|
# Create dock_area, dock, widget
|
||||||
|
dock, widget = create_widget(qtbot, gui, gui.available_widgets.PositionerControlLine)
|
||||||
|
dock: client.BECDock
|
||||||
|
widget: client.PositionerControlLine
|
||||||
|
|
||||||
|
# Test rpc calls
|
||||||
|
dev = bec.device_manager.devices
|
||||||
|
scans = bec.scans
|
||||||
|
# Set positioner
|
||||||
|
widget.set_positioner(dev.samx)
|
||||||
|
scans.mv(dev.samx, 3, relative=False).wait()
|
||||||
|
widget.set_positioner(dev.samy.name)
|
||||||
|
scans.mv(dev.samy, -3, relative=False).wait()
|
||||||
|
|
||||||
|
# Test removing the widget, or leaving it open for the next test
|
||||||
|
maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.timeout(PYTEST_TIMEOUT)
|
||||||
|
def test_widgets_e2e_ring_progress_bar(qtbot, connected_client_gui_obj, random_generator_from_seed):
|
||||||
|
"""Test the RingProgressBar widget"""
|
||||||
|
gui = connected_client_gui_obj
|
||||||
|
bec = gui._client
|
||||||
|
# Create dock_area, dock, widget
|
||||||
|
dock, widget = create_widget(qtbot, gui, gui.available_widgets.RingProgressBar)
|
||||||
|
dock: client.BECDock
|
||||||
|
widget: client.RingProgressBar
|
||||||
|
|
||||||
|
# Test rpc calls
|
||||||
|
dev = bec.device_manager.devices
|
||||||
|
scans = bec.scans
|
||||||
|
# Do a scan
|
||||||
|
scans.line_scan(dev.samx, -3, 3, steps=50, exp_time=0.01, relative=False).wait()
|
||||||
|
|
||||||
|
# Test removing the widget, or leaving it open for the next test
|
||||||
|
maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.timeout(PYTEST_TIMEOUT)
|
||||||
|
def test_widgets_e2e_scan_control(qtbot, connected_client_gui_obj, random_generator_from_seed):
|
||||||
|
"""Test the ScanControl widget"""
|
||||||
|
gui = connected_client_gui_obj
|
||||||
|
bec = gui._client
|
||||||
|
# Create dock_area, dock, widget
|
||||||
|
dock, widget = create_widget(qtbot, gui, gui.available_widgets.ScanControl)
|
||||||
|
dock: client.BECDock
|
||||||
|
widget: client.ScanControl
|
||||||
|
|
||||||
|
# No rpc calls to check so far
|
||||||
|
|
||||||
|
# Test removing the widget, or leaving it open for the next test
|
||||||
|
maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.timeout(PYTEST_TIMEOUT)
|
||||||
|
def test_widgets_e2e_scatter_waveform(qtbot, connected_client_gui_obj, random_generator_from_seed):
|
||||||
|
"""Test the ScatterWaveform widget"""
|
||||||
|
gui = connected_client_gui_obj
|
||||||
|
bec = gui._client
|
||||||
|
# Create dock_area, dock, widget
|
||||||
|
dock, widget = create_widget(qtbot, gui, gui.available_widgets.ScatterWaveform)
|
||||||
|
dock: client.BECDock
|
||||||
|
widget: client.ScatterWaveform
|
||||||
|
|
||||||
|
# Test rpc calls
|
||||||
|
dev = bec.device_manager.devices
|
||||||
|
scans = bec.scans
|
||||||
|
widget.plot(dev.samx, dev.samy, dev.bpm4i)
|
||||||
|
scans.grid_scan(dev.samx, -5, 5, 5, dev.samy, -5, 5, 5, exp_time=0.01, relative=False).wait()
|
||||||
|
|
||||||
|
# Test removing the widget, or leaving it open for the next test
|
||||||
|
maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.timeout(PYTEST_TIMEOUT)
|
||||||
|
def test_widgets_e2e_stop_button(qtbot, connected_client_gui_obj, random_generator_from_seed):
|
||||||
|
"""Test the StopButton widget"""
|
||||||
|
gui = connected_client_gui_obj
|
||||||
|
bec = gui._client
|
||||||
|
# Create dock_area, dock, widget
|
||||||
|
dock, widget = create_widget(qtbot, gui, gui.available_widgets.StopButton)
|
||||||
|
dock: client.BECDock
|
||||||
|
widget: client.StopButton
|
||||||
|
|
||||||
|
# No rpc calls to check so far
|
||||||
|
|
||||||
|
# Test removing the widget, or leaving it open for the next test
|
||||||
|
maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.timeout(PYTEST_TIMEOUT)
|
||||||
|
def test_widgets_e2e_resume_button(qtbot, connected_client_gui_obj, random_generator_from_seed):
|
||||||
|
"""Test the StopButton widget"""
|
||||||
|
gui = connected_client_gui_obj
|
||||||
|
bec = gui._client
|
||||||
|
# Create dock_area, dock, widget
|
||||||
|
dock, widget = create_widget(qtbot, gui, gui.available_widgets.ResumeButton)
|
||||||
|
dock: client.BECDock
|
||||||
|
widget: client.ResumeButton
|
||||||
|
|
||||||
|
# No rpc calls to check so far
|
||||||
|
|
||||||
|
# Test removing the widget, or leaving it open for the next test
|
||||||
|
maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.timeout(PYTEST_TIMEOUT)
|
||||||
|
def test_widgets_e2e_reset_button(qtbot, connected_client_gui_obj, random_generator_from_seed):
|
||||||
|
"""Test the StopButton widget"""
|
||||||
|
gui = connected_client_gui_obj
|
||||||
|
bec = gui._client
|
||||||
|
# Create dock_area, dock, widget
|
||||||
|
dock, widget = create_widget(qtbot, gui, gui.available_widgets.ResetButton)
|
||||||
|
dock: client.BECDock
|
||||||
|
widget: client.ResetButton
|
||||||
|
# No rpc calls to check so far
|
||||||
|
|
||||||
|
# Test removing the widget, or leaving it open for the next test
|
||||||
|
maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.timeout(PYTEST_TIMEOUT)
|
||||||
|
def test_widgets_e2e_text_box(qtbot, connected_client_gui_obj, random_generator_from_seed):
|
||||||
|
"""Test the TextBox widget"""
|
||||||
|
gui = connected_client_gui_obj
|
||||||
|
bec = gui._client
|
||||||
|
# Create dock_area, dock, widget
|
||||||
|
dock, widget = create_widget(qtbot, gui, gui.available_widgets.TextBox)
|
||||||
|
dock: client.BECDock
|
||||||
|
widget: client.TextBox
|
||||||
|
|
||||||
|
# RPC calls
|
||||||
|
widget.set_plain_text("Hello World")
|
||||||
|
widget.set_html_text("<b> Hello World HTML </b>")
|
||||||
|
|
||||||
|
# Test removing the widget, or leaving it open for the next test
|
||||||
|
maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.timeout(PYTEST_TIMEOUT)
|
||||||
|
def test_widgets_e2e_waveform(qtbot, connected_client_gui_obj, random_generator_from_seed):
|
||||||
|
"""Test the Waveform widget"""
|
||||||
|
gui = connected_client_gui_obj
|
||||||
|
bec = gui._client
|
||||||
|
# Create dock_area, dock, widget
|
||||||
|
dock, widget = create_widget(qtbot, gui, gui.available_widgets.Waveform)
|
||||||
|
dock: client.BECDock
|
||||||
|
widget: client.Waveform
|
||||||
|
|
||||||
|
# Test rpc calls
|
||||||
|
dev = bec.device_manager.devices
|
||||||
|
scans = bec.scans
|
||||||
|
widget.plot(dev.bpm4i)
|
||||||
|
s = scans.line_scan(dev.samx, -3, 3, steps=50, exp_time=0.01, relative=False)
|
||||||
|
s.wait()
|
||||||
|
|
||||||
|
def _wait_for_scan_in_history():
|
||||||
|
# Get scan item from history
|
||||||
|
scan_item = bec.history.get_by_scan_id(s.scan.scan_id)
|
||||||
|
return scan_item is not None
|
||||||
|
|
||||||
|
qtbot.waitUntil(_wait_for_scan_in_history, timeout=7000)
|
||||||
|
|
||||||
|
scan_item = bec.history.get_by_scan_id(s.scan.scan_id)
|
||||||
|
samx_data = scan_item.devices.samx.samx.read()["value"]
|
||||||
|
bpm4i_data = scan_item.devices.bpm4i.bpm4i.read()["value"]
|
||||||
|
curve = widget.curves[0]
|
||||||
|
assert np.allclose(curve.get_data()[0], samx_data)
|
||||||
|
assert np.allclose(curve.get_data()[1], bpm4i_data)
|
||||||
|
|
||||||
|
# Test removing the widget, or leaving it open for the next test
|
||||||
|
maybe_remove_dock_area(qtbot, gui=gui, random_int_gen=random_generator_from_seed)
|
||||||
@@ -82,3 +82,52 @@ def test_bec_connector_submit_task(bec_connector):
|
|||||||
while not completed:
|
while not completed:
|
||||||
QApplication.processEvents()
|
QApplication.processEvents()
|
||||||
time.sleep(0.1)
|
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.messages import ScanMessage
|
||||||
from bec_lib.serialization import MsgpackSerialization
|
from bec_lib.serialization import MsgpackSerialization
|
||||||
|
|
||||||
from bec_widgets.utils.bec_dispatcher import QtRedisConnector
|
from bec_widgets.utils.bec_dispatcher import QtRedisConnector, QtThreadSafeCallback
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
@@ -27,6 +27,7 @@ def bec_dispatcher_w_connector(bec_dispatcher, topics_msg_list, send_msg_event):
|
|||||||
connector = QtRedisConnector("localhost:1", redis_class_mock)
|
connector = QtRedisConnector("localhost:1", redis_class_mock)
|
||||||
bec_dispatcher.client.connector = connector
|
bec_dispatcher.client.connector = connector
|
||||||
yield bec_dispatcher
|
yield bec_dispatcher
|
||||||
|
connector.shutdown()
|
||||||
|
|
||||||
|
|
||||||
dummy_msg = MsgpackSerialization.dumps(ScanMessage(point_id=0, scan_id="0", data={}))
|
dummy_msg = MsgpackSerialization.dumps(ScanMessage(point_id=0, scan_id="0", data={}))
|
||||||
@@ -62,7 +63,6 @@ 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))])
|
@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):
|
def test_dispatcher_disconnect_one(bec_dispatcher_w_connector, qtbot, send_msg_event):
|
||||||
# test for BEC issue #276
|
|
||||||
bec_dispatcher = bec_dispatcher_w_connector
|
bec_dispatcher = bec_dispatcher_w_connector
|
||||||
cb1 = mock.Mock(spec=[])
|
cb1 = mock.Mock(spec=[])
|
||||||
cb2 = mock.Mock(spec=[])
|
cb2 = mock.Mock(spec=[])
|
||||||
@@ -86,12 +86,21 @@ def test_dispatcher_2_cb_same_topic(bec_dispatcher_w_connector, qtbot, send_msg_
|
|||||||
cb1 = mock.Mock(spec=[])
|
cb1 = mock.Mock(spec=[])
|
||||||
cb2 = mock.Mock(spec=[])
|
cb2 = mock.Mock(spec=[])
|
||||||
|
|
||||||
|
num_slots = len(bec_dispatcher._registered_slots)
|
||||||
|
|
||||||
bec_dispatcher.connect_slot(cb1, "topic1")
|
bec_dispatcher.connect_slot(cb1, "topic1")
|
||||||
bec_dispatcher.connect_slot(cb2, "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
|
assert len(bec_dispatcher.client.connector._topics_cb) == 1
|
||||||
assert len(bec_dispatcher._slots) == 2
|
|
||||||
|
# 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
|
||||||
bec_dispatcher.disconnect_slot(cb1, "topic1")
|
bec_dispatcher.disconnect_slot(cb1, "topic1")
|
||||||
assert len(bec_dispatcher._slots) == 1
|
assert len(bec_dispatcher._registered_slots) == num_slots + 1
|
||||||
|
|
||||||
send_msg_event.set()
|
send_msg_event.set()
|
||||||
qtbot.wait(10)
|
qtbot.wait(10)
|
||||||
@@ -99,9 +108,31 @@ def test_dispatcher_2_cb_same_topic(bec_dispatcher_w_connector, qtbot, send_msg_
|
|||||||
cb2.assert_called_once()
|
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))])
|
@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):
|
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
|
bec_dispatcher = bec_dispatcher_w_connector
|
||||||
cb1 = mock.Mock(spec=[])
|
cb1 = mock.Mock(spec=[])
|
||||||
|
|
||||||
@@ -114,3 +145,36 @@ def test_dispatcher_2_topic_same_cb(bec_dispatcher_w_connector, qtbot, send_msg_
|
|||||||
send_msg_event.set()
|
send_msg_event.set()
|
||||||
qtbot.wait(10)
|
qtbot.wait(10)
|
||||||
cb1.assert_called_once()
|
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)
|
||||||
|
|||||||
@@ -89,6 +89,13 @@ def test_undock_and_dock_docks(bec_dock_area, qtbot):
|
|||||||
assert len(bec_dock_area.dock_area.tempAreas) == 0
|
assert len(bec_dock_area.dock_area.tempAreas) == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_new_dock_raises_for_invalid_name(bec_dock_area):
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
bec_dock_area.new(
|
||||||
|
name="new", _override_slot_params={"popup_error": False, "raise_error": True}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
###################################
|
###################################
|
||||||
# Toolbar Actions
|
# Toolbar Actions
|
||||||
###################################
|
###################################
|
||||||
|
|||||||
@@ -74,6 +74,7 @@ def test_client_generator_with_black_formatting():
|
|||||||
import inspect
|
import inspect
|
||||||
import traceback
|
import traceback
|
||||||
from functools import reduce
|
from functools import reduce
|
||||||
|
from operator import add
|
||||||
from typing import Literal, Optional
|
from typing import Literal, Optional
|
||||||
|
|
||||||
from bec_lib.logger import bec_logger
|
from bec_lib.logger import bec_logger
|
||||||
|
|||||||
@@ -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):
|
def test_launch_window_launch_default_auto_update(bec_launch_window):
|
||||||
# Mock the auto update selection
|
# Mock the auto update selection
|
||||||
bec_launch_window.tile_auto_update.selector.setCurrentText("Default")
|
bec_launch_window.tiles["auto_update"].selector.setCurrentText("Default")
|
||||||
|
|
||||||
# Call the method to launch the auto update
|
# Call the method to launch the auto update
|
||||||
res = bec_launch_window._open_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): ...
|
class PluginAutoUpdate(AutoUpdates): ...
|
||||||
|
|
||||||
bec_launch_window.available_auto_updates = {"PluginAutoUpdate": PluginAutoUpdate}
|
bec_launch_window.available_auto_updates = {"PluginAutoUpdate": PluginAutoUpdate}
|
||||||
bec_launch_window.tile_auto_update.selector.clear()
|
bec_launch_window.tiles["auto_update"].selector.clear()
|
||||||
bec_launch_window.tile_auto_update.selector.addItems(
|
bec_launch_window.tiles["auto_update"].selector.addItems(
|
||||||
list(bec_launch_window.available_auto_updates.keys()) + ["Default"]
|
list(bec_launch_window.available_auto_updates.keys()) + ["Default"]
|
||||||
)
|
)
|
||||||
bec_launch_window.tile_auto_update.selector.setCurrentText("PluginAutoUpdate")
|
bec_launch_window.tiles["auto_update"].selector.setCurrentText("PluginAutoUpdate")
|
||||||
res = bec_launch_window._open_auto_update()
|
res = bec_launch_window._open_auto_update()
|
||||||
assert isinstance(res, PluginAutoUpdate)
|
assert isinstance(res, PluginAutoUpdate)
|
||||||
assert res.windowTitle() == "BEC - PluginAutoUpdate"
|
assert res.windowTitle() == "BEC - PluginAutoUpdate"
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ def fill_commponents(components: dict[str, DynamicFormItem]):
|
|||||||
|
|
||||||
|
|
||||||
def test_griditems_are_correct_class(
|
def test_griditems_are_correct_class(
|
||||||
metadata_widget: tuple[ScanMetadata, dict[str, DynamicFormItem]]
|
metadata_widget: tuple[ScanMetadata, dict[str, DynamicFormItem]],
|
||||||
):
|
):
|
||||||
_, components = metadata_widget
|
_, components = metadata_widget
|
||||||
assert isinstance(components["sample_name"], StrMetadataField)
|
assert isinstance(components["sample_name"], StrMetadataField)
|
||||||
@@ -162,7 +162,7 @@ def test_validation(metadata_widget: tuple[ScanMetadata, dict[str, DynamicFormIt
|
|||||||
|
|
||||||
|
|
||||||
def test_numbers_clipped_to_limits(
|
def test_numbers_clipped_to_limits(
|
||||||
metadata_widget: tuple[ScanMetadata, dict[str, DynamicFormItem]]
|
metadata_widget: tuple[ScanMetadata, dict[str, DynamicFormItem]],
|
||||||
):
|
):
|
||||||
widget, components = metadata_widget = metadata_widget
|
widget, components = metadata_widget = metadata_widget
|
||||||
fill_commponents(components)
|
fill_commponents(components)
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import json
|
import json
|
||||||
|
from types import SimpleNamespace
|
||||||
|
from unittest import mock
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@@ -19,6 +21,8 @@ from tests.unit_tests.client_mocks import (
|
|||||||
|
|
||||||
from .conftest import create_widget
|
from .conftest import create_widget
|
||||||
|
|
||||||
|
# pylint: disable=unexpected-keyword-arg
|
||||||
|
|
||||||
##################################################
|
##################################################
|
||||||
# Waveform widget base functionality tests
|
# Waveform widget base functionality tests
|
||||||
##################################################
|
##################################################
|
||||||
@@ -541,7 +545,14 @@ def test_on_async_readback_add_update(qtbot, mocked_client):
|
|||||||
|
|
||||||
msg = {"signals": {"async_device": {"value": [100, 200], "timestamp": [1001, 1002]}}}
|
msg = {"signals": {"async_device": {"value": [100, 200], "timestamp": [1001, 1002]}}}
|
||||||
metadata = {"async_update": {"max_shape": [None], "type": "add"}}
|
metadata = {"async_update": {"max_shape": [None], "type": "add"}}
|
||||||
wf.on_async_readback(msg, metadata)
|
|
||||||
|
cb_info_ret = {"scan_id": wf.scan_id}
|
||||||
|
|
||||||
|
def ret_sender():
|
||||||
|
return SimpleNamespace(cb_info={"scan_id": wf.scan_id})
|
||||||
|
|
||||||
|
with mock.patch.object(wf, "sender", side_effect=ret_sender):
|
||||||
|
wf.on_async_readback(msg, metadata, _override_slot_params={"verify_sender": False})
|
||||||
|
|
||||||
x_data, y_data = c.get_data()
|
x_data, y_data = c.get_data()
|
||||||
assert len(x_data) == 5
|
assert len(x_data) == 5
|
||||||
@@ -553,7 +564,8 @@ def test_on_async_readback_add_update(qtbot, mocked_client):
|
|||||||
# instruction='replace'
|
# instruction='replace'
|
||||||
msg2 = {"signals": {"async_device": {"value": [999], "timestamp": [555]}}}
|
msg2 = {"signals": {"async_device": {"value": [999], "timestamp": [555]}}}
|
||||||
metadata2 = {"async_update": {"max_shape": [None], "type": "replace"}}
|
metadata2 = {"async_update": {"max_shape": [None], "type": "replace"}}
|
||||||
wf.on_async_readback(msg2, metadata2)
|
with mock.patch.object(wf, "sender", side_effect=ret_sender):
|
||||||
|
wf.on_async_readback(msg2, metadata2, _override_slot_params={"verify_sender": False})
|
||||||
x_data2, y_data2 = c.get_data()
|
x_data2, y_data2 = c.get_data()
|
||||||
np.testing.assert_array_equal(x_data2, [0])
|
np.testing.assert_array_equal(x_data2, [0])
|
||||||
|
|
||||||
@@ -568,7 +580,8 @@ def test_on_async_readback_add_update(qtbot, mocked_client):
|
|||||||
metadata = {
|
metadata = {
|
||||||
"async_update": {"max_shape": [None, waveform_shape], "index": 0, "type": "add_slice"}
|
"async_update": {"max_shape": [None, waveform_shape], "index": 0, "type": "add_slice"}
|
||||||
}
|
}
|
||||||
wf.on_async_readback(msg, metadata)
|
with mock.patch.object(wf, "sender", side_effect=ret_sender):
|
||||||
|
wf.on_async_readback(msg, metadata, _override_slot_params={"verify_sender": False})
|
||||||
|
|
||||||
# Old data should be deleted since the slice_index did not match
|
# Old data should be deleted since the slice_index did not match
|
||||||
x_data, y_data = c.get_data()
|
x_data, y_data = c.get_data()
|
||||||
@@ -595,7 +608,8 @@ def test_on_async_readback_add_update(qtbot, mocked_client):
|
|||||||
metadata = {
|
metadata = {
|
||||||
"async_update": {"max_shape": [None, waveform_shape], "index": 0, "type": "add_slice"}
|
"async_update": {"max_shape": [None, waveform_shape], "index": 0, "type": "add_slice"}
|
||||||
}
|
}
|
||||||
wf.on_async_readback(msg, metadata)
|
with mock.patch.object(wf, "sender", side_effect=ret_sender):
|
||||||
|
wf.on_async_readback(msg, metadata, _override_slot_params={"verify_sender": False})
|
||||||
x_data, y_data = c.get_data()
|
x_data, y_data = c.get_data()
|
||||||
assert len(y_data) == waveform_shape
|
assert len(y_data) == waveform_shape
|
||||||
assert len(x_data) == waveform_shape
|
assert len(x_data) == waveform_shape
|
||||||
@@ -616,7 +630,8 @@ def test_on_async_readback_add_update(qtbot, mocked_client):
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
metadata = {"async_update": {"type": "replace"}}
|
metadata = {"async_update": {"type": "replace"}}
|
||||||
wf.on_async_readback(msg, metadata)
|
with mock.patch.object(wf, "sender", side_effect=ret_sender):
|
||||||
|
wf.on_async_readback(msg, metadata, _override_slot_params={"verify_sender": False})
|
||||||
|
|
||||||
x_data, y_data = c.get_data()
|
x_data, y_data = c.get_data()
|
||||||
assert np.array_equal(y_data, np.array(range(waveform_shape)))
|
assert np.array_equal(y_data, np.array(range(waveform_shape)))
|
||||||
|
|||||||
Reference in New Issue
Block a user