From 1d6b927efcafbe36d9b7e8a44c461333c4be7f5e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 20:47:46 +0000 Subject: [PATCH 01/11] Add the Move and Grab reference extension and its tests Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01Tdiso1u1ZhikSutj2Qs5Cg --- README.rst | 6 + pyproject.toml | 2 +- .../extensions/move_and_grab.py | 151 ++++++++++++++++++ tests/test_move_and_grab.py | 94 +++++++++++ 4 files changed, 252 insertions(+), 1 deletion(-) create mode 100644 src/pymodaq_plugins_mockexamples/extensions/move_and_grab.py create mode 100644 tests/test_move_and_grab.py diff --git a/README.rst b/README.rst index ba7af3e..19c7d51 100644 --- a/README.rst +++ b/README.rst @@ -98,6 +98,12 @@ PID Models Extensions ========== +* **Move and Grab** (*extensions/move_and_grab.py*): the smallest useful Dashboard extension, to start from when + writing yours. It moves an actuator of the Dashboard's experiment to a target, grabs a detector, shows the data and can + save them. It shows how to use the ``modules_manager``, how to wait for the instruments with callbacks instead of + blocking the GUI, units on the positions and a clean shutdown. It works with the mock instruments of PyMoDAQ. See + "How a Dashboard extension works" in the PyMoDAQ documentation. + Installation instructions ========================= diff --git a/pyproject.toml b/pyproject.toml index ff04daf..d465e9b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [features] # defines the plugin features contained into this plugin instruments = true # true if plugin contains instrument classes (else false, notice the lowercase for toml files) -extensions = false # true if plugins contains dashboard extensions +extensions = true # true if plugins contains dashboard extensions models = true # true if plugins contains pid models h5exporters = false # true if plugin contains custom h5 file exporters scanners = false # true if plugin contains custom scan layout (daq_scan extensions) diff --git a/src/pymodaq_plugins_mockexamples/extensions/move_and_grab.py b/src/pymodaq_plugins_mockexamples/extensions/move_and_grab.py new file mode 100644 index 0000000..ee55e57 --- /dev/null +++ b/src/pymodaq_plugins_mockexamples/extensions/move_and_grab.py @@ -0,0 +1,151 @@ +"""Reference Dashboard extension: move an actuator, grab a detector, show the data and save them. + +It is the smallest useful ``CustomExt`` and shows what an extension driving instruments has to get right: + +* it uses the actuators and detectors of the Dashboard through ``self.modules_manager`` and never creates control + modules, nor its own experiment or state managers +* it never blocks the GUI thread: the move and the grab are started with a callback, the next step is done in the callback + when the control module signals that it is finished (no polling, no sleep) +* the position is a ``DataActuator`` with units (the ones of the actuator), the data come back as a ``DataToExport`` +* the extension can be closed cleanly: callbacks are forgotten, the file is closed, and quitting is refused while busy + +Read the page "How a Dashboard extension works" of the PyMoDAQ documentation for the structure. +""" +from qtpy import QtWidgets + +from pymodaq_utils.logger import set_logger, get_module_name + +from pymodaq_data import DataToExport +from pymodaq_data.h5modules.data_saving import DataToExportSaver + +from pymodaq_gui import utils as gutils +from pymodaq_gui.plotting.data_viewers import ViewerDispatcher + +from pymodaq.control_modules.enums import MoveType +from pymodaq.utils.custom_ext import CustomExt +from pymodaq.utils.data import DataActuator +from pymodaq.utils.managers.modules import ModuleType + +logger = set_logger(get_module_name(__file__)) + +EXTENSION_NAME = 'Move and Grab' # displayed in the Extensions menu of the Dashboard +CLASS_NAME = 'MoveAndGrab' # the name of the class below + + +class MoveAndGrab(CustomExt): + + params = [ + {'title': 'Actuator:', 'name': 'actuator', 'type': 'list', 'limits': []}, + {'title': 'Target:', 'name': 'target', 'type': 'float', 'value': 1.}, # in the units of the actuator + {'title': 'Detector:', 'name': 'detector', 'type': 'list', 'limits': []}, + {'title': 'Save the data:', 'name': 'save', 'type': 'bool', 'value': False}, + {'title': 'Status:', 'name': 'status', 'type': 'str', 'value': 'Idle', 'readonly': True}, + ] + + def __init__(self, parent: gutils.DockArea, dashboard): + super().__init__(parent, dashboard) # also creates self.modules_manager for this extension + self._busy = False + self._positions: DataToExport = None + self.setup_ui() + + def setup_docks_and_widgets(self): + self.docks['settings'] = gutils.Dock('Settings') + self.dockarea.addDock(self.docks['settings']) + self.docks['settings'].addWidget(self.settings_tree) + + self.viewer = ViewerDispatcher(self.dockarea, title='Data') # shows each data of a DataToExport, in docks + + def setup_menus_and_toolbars(self, menubar: QtWidgets.QMenuBar = None): + self.create_dashboard_toolbar(add_break=False) # show/hide the Dashboard, experiment and state + + def setup_actions(self): + self.add_action('move_and_grab', 'Move and grab', 'camera', + tip='Move the selected actuator to the target, then grab the selected detector') + + def connect_things(self): + self.connect_action('move_and_grab', self.move_and_grab) + self.modules_manager.timeout_signal.connect(self._on_timeout) + + def do_things_after_experiment_set(self, experiment_name: str, show_dashboard: bool = None): + """The instruments just changed: refresh what depends on them (the base class updates the modules_manager)""" + super().do_things_after_experiment_set(experiment_name, show_dashboard) + self.settings.child('actuator').setLimits(self.modules_manager.actuators_name) + self.settings.child('detector').setLimits(self.modules_manager.detectors_name) + + def move_and_grab(self): + if self._busy: + return + actuator = self.modules_manager.get_mod_from_name(self.settings['actuator'], ModuleType.Actuator) + detector = self.modules_manager.get_mod_from_name(self.settings['detector'], ModuleType.Detector) + if actuator is None or detector is None: + self.update_status('Select an actuator and a detector (set an experiment in the Dashboard first)') + return + self._set_busy(True, f'Moving {actuator.title}') + self.modules_manager.selected_actuators_name = [actuator.title] + self.modules_manager.selected_detectors_name = [detector.title] + # the target has units: those of the actuator, so that no conversion is needed + target = DataToExport('target', data=[DataActuator(actuator.title, data=self.settings['target'], + units=actuator.units)]) + self.modules_manager.move_actuators_with_callback(target, mode=MoveType.ABS, callback=self._on_move_done) + + def _on_move_done(self, positions: DataToExport): + """Slot called, in the GUI thread, once the actuator reached its target""" + self.modules_manager.forget_callback(self._on_move_done, ModuleType.Actuator) + self._positions = positions + self.set_status('Grabbing') + self.modules_manager.grab_data_with_callback(callback=self._on_data) + + def _on_data(self, data: DataToExport): + """Slot called, in the GUI thread, once the detector sent its data""" + self.modules_manager.forget_callback(self._on_data, ModuleType.Detector) + self.viewer.show_data(data) + if self.settings['save']: + self._save(data) + self._set_busy(False, 'Done') + + def _save(self, data: DataToExport): + h5saver = self.h5_manager.h5saver # opens the file, or creates a new one, if needed + node = h5saver.get_set_group('/RawData', 'MoveAndGrab') + DataToExportSaver(h5saver).add_data(node, DataToExport('moveandgrab', data=[*self._positions, *data])) + h5saver.flush() + + def _on_timeout(self, missing_modules: list): + """A module did not answer: do not stay blocked""" + self.modules_manager.forget_callback(self._on_move_done, ModuleType.Control) + self.modules_manager.forget_callback(self._on_data, ModuleType.Control) + self._set_busy(False, f'Timeout: {", ".join(missing_modules)}') + + def _set_busy(self, busy: bool, status: str): + self._busy = busy + self.set_action_enabled('move_and_grab', not busy) + self.set_status(status) + + def set_status(self, status: str): + self.settings.child('status').setValue(status) + + def _quit_fun(self) -> bool: + """Return True to let the extension quit; False to refuse, here while a move or a grab is going on""" + if self._busy: + self.update_status('Wait for the end of the move and grab before quitting') + return False + self.modules_manager.timeout_signal.disconnect(self._on_timeout) + self.h5_manager.close_file() + return True + + +def main(): + """Run the extension on its own (the Dashboard stays hidden), see ``-x EXPERIMENT_NAME -s STATE_NAME``""" + import sys + from pymodaq_gui.qt_utils import mkQApp + from pymodaq.dashboard import load_dashboard_with_arguments + from pymodaq.utils.gui_utils.loader_utils import create_extension + + app = mkQApp('Move and Grab') + win, dashboard, _ = load_dashboard_with_arguments(show_dashboard=False, load_extension=False) + win.mainwindow.setVisible(False) + win_ext, ext = create_extension(dashboard, MoveAndGrab, show_extension=True) + sys.exit(app.exec()) + + +if __name__ == '__main__': + main() diff --git a/tests/test_move_and_grab.py b/tests/test_move_and_grab.py new file mode 100644 index 0000000..5493e96 --- /dev/null +++ b/tests/test_move_and_grab.py @@ -0,0 +1,94 @@ +"""Tests of the reference extension: it moves a mock actuator, grabs a mock detector, shows and saves the data""" +import pytest +import qt_themes +from pytest import fixture +from qtpy import QtWidgets + +pytest.importorskip('pymodaq_plugins_mock', reason='The mock instruments of PyMoDAQ are needed') + +from pymodaq_utils.config import GlobalConfig + +from pymodaq.control_modules.enums import DAQTypesEnum +from pymodaq.dashboard import create_load_dashboard +from pymodaq.utils.custom_ext import CustomExt +from pymodaq.utils.gui_utils.loader_utils import create_extension + +from pymodaq_plugins_mockexamples.extensions import move_and_grab +from pymodaq_plugins_mockexamples.extensions.move_and_grab import MoveAndGrab + + +def test_is_a_dashboard_extension(): + assert move_and_grab.EXTENSION_NAME + assert move_and_grab.CLASS_NAME == MoveAndGrab.__name__ + assert issubclass(MoveAndGrab, CustomExt) + + +@fixture +def extension(qtbot): + """The extension, running in a Dashboard with an initialized mock actuator and a mock 0D detector""" + config = GlobalConfig() + qt_themes.set_theme(theme=config('gui', 'style', 'theme')[0], style=config('gui', 'style', 'style')[0]) + shared_ui, dashboard = create_load_dashboard(show_dashboard=False) + qtbot.addWidget(shared_ui.parent) + qtbot.addWidget(dashboard.tree) + + creator = dashboard.module_creator + actuator = creator.create_actuator('Move', 'Mock', None) + creator.set_actuator_type(actuator, 'Mock') + creator.add_actuator(actuator) + detector = creator.create_detector('Det', DAQTypesEnum.DAQ0D) + creator.set_detector_type(detector, DAQTypesEnum.DAQ0D, 'Mock') + creator.add_detector(detector) + for module in (actuator, detector): + module.init_hardware_ui(True) + qtbot.waitUntil(lambda: actuator.initialized_state and detector.initialized_state, timeout=10000) + dashboard.modules_manager.actuators_all = [actuator] + dashboard.modules_manager.detectors_all = [detector] + + win_ext, ext = create_extension(dashboard, MoveAndGrab, show_extension=False) + ext.do_things_after_experiment_set('mock experiment', False) # what the Dashboard does once an experiment is set + yield ext, actuator + ext.quit_fun() + QtWidgets.QApplication.processEvents() + dashboard.quit_fun() + + +def test_lists_the_instruments_of_the_dashboard(extension): + ext, _ = extension + assert ext.settings['actuator'] == 'Move' + assert ext.settings['detector'] == 'Det' + + +def test_move_and_grab(extension, qtbot): + ext, actuator = extension + ext.settings['target'] = 5. + ext.move_and_grab() + assert ext._busy and not ext.is_action_enabled('move_and_grab') # the GUI is not blocked, the action is disabled + qtbot.waitUntil(lambda: not ext._busy, timeout=15000) + + assert ext.settings['status'] == 'Done' + position = ext._positions.get_data_from_name('Move') + assert position.units == actuator.units # the target and the positions have the units of the actuator + assert float(position.data[0][0]) == pytest.approx(5., abs=actuator.epsilon) # reached within the precision + assert len(ext.viewer.viewers) == 1 # the data of the detector is displayed + + +def test_cannot_quit_while_busy(extension, qtbot): + ext, _ = extension + ext.move_and_grab() + assert ext._quit_fun() is False + qtbot.waitUntil(lambda: not ext._busy, timeout=15000) + assert ext._quit_fun() is True + ext.modules_manager.timeout_signal.connect(ext._on_timeout) # reconnected for the quit of the fixture + + +def test_save(extension, qtbot, tmp_path): + ext, _ = extension + ext.h5_manager.get_h5saver().settings['base_path'] = str(tmp_path) # not in the user's data folder + ext.settings['save'] = True + ext.move_and_grab() + qtbot.waitUntil(lambda: not ext._busy, timeout=15000) + + h5saver = ext.h5_manager.h5saver + group = h5saver.get_node('/RawData/MoveAndGrab') + assert len(h5saver.get_children(group)) > 0 # the position and the data have been saved From 8ebab75b815fe80177c52f6b5fad16c29b6bd861 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 20:50:06 +0000 Subject: [PATCH 02/11] CI: install libegl1 and libgl1 needed by Qt on the runner Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01Tdiso1u1ZhikSutj2Qs5Cg --- .github/workflows/Testbase.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/Testbase.yml b/.github/workflows/Testbase.yml index 53745b6..d908826 100644 --- a/.github/workflows/Testbase.yml +++ b/.github/workflows/Testbase.yml @@ -25,7 +25,7 @@ jobs: python-version: ${{ inputs.python }} - name: Install package run: | - sudo apt install libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0 x11-utils + sudo apt install libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0 x11-utils libegl1 libgl1 python -m pip install --upgrade pip export QT_DEBUG_PLUGINS=1 pip install flake8 pytest pytest-cov pytest-qt pytest-xdist pytest-xvfb setuptools wheel numpy h5py ${{ inputs.qt5 }} toml From af3141de39832cf0ad67e85f16f12d2cad17fe65 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 20:51:47 +0000 Subject: [PATCH 03/11] CI: install libxcb-cursor0, required by the Qt 6 xcb plugin Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01Tdiso1u1ZhikSutj2Qs5Cg --- .github/workflows/Testbase.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/Testbase.yml b/.github/workflows/Testbase.yml index d908826..89ea857 100644 --- a/.github/workflows/Testbase.yml +++ b/.github/workflows/Testbase.yml @@ -25,7 +25,7 @@ jobs: python-version: ${{ inputs.python }} - name: Install package run: | - sudo apt install libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0 x11-utils libegl1 libgl1 + sudo apt install libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0 x11-utils libegl1 libgl1 libxcb-cursor0 python -m pip install --upgrade pip export QT_DEBUG_PLUGINS=1 pip install flake8 pytest pytest-cov pytest-qt pytest-xdist pytest-xvfb setuptools wheel numpy h5py ${{ inputs.qt5 }} toml From c51a3ac0656c2ccd084072d6cae3a1abeb029e8e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 20:53:04 +0000 Subject: [PATCH 04/11] CI: run pytest serially, the Qt tests collected differently across xdist workers Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01Tdiso1u1ZhikSutj2Qs5Cg --- .github/workflows/Testbase.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/Testbase.yml b/.github/workflows/Testbase.yml index 89ea857..f0a3e3d 100644 --- a/.github/workflows/Testbase.yml +++ b/.github/workflows/Testbase.yml @@ -41,5 +41,5 @@ jobs: flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics --exclude=src/pymodaq/resources/QtDesigner_Ressources,docs - name: Test with pytest run: | - pytest -n auto + pytest From 99d07a0855f0d6ead741eed3b9b435735b83c0e7 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 11 Oct 2026 20:55:11 +0000 Subject: [PATCH 05/11] tests: import the h5 modules from pymodaq_data, the old pymodaq.utils path no longer exists Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01Tdiso1u1ZhikSutj2Qs5Cg --- tests/test_exporters.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/test_exporters.py b/tests/test_exporters.py index 93d8a36..ca53b77 100644 --- a/tests/test_exporters.py +++ b/tests/test_exporters.py @@ -8,12 +8,12 @@ from pathlib import Path import pytest -from pymodaq.utils.h5modules.saving import H5SaverLowLevel -from pymodaq.utils import data as data_mod -from pymodaq.utils.h5modules.data_saving import DataSaverLoader, AxisSaverLoader +from pymodaq_data.h5modules.saving import H5SaverLowLevel +from pymodaq_data import data as data_mod +from pymodaq_data.h5modules.data_saving import DataSaverLoader, AxisSaverLoader -from pymodaq.utils.h5modules import exporter as h5export -from pymodaq.utils.h5modules.utils import register_exporter, register_exporters +from pymodaq_data.h5modules import exporter as h5export +from pymodaq_data.h5modules.utils import register_exporter, register_exporters def test_register_exporter_mock_examples(): From 77d032f05149fd8956cdf826ac2fa0dcadeb8a0c Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 06:21:29 +0000 Subject: [PATCH 06/11] tests: update test_axis_names to the current actuator API (units, epsilons, controller axis) Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01Tdiso1u1ZhikSutj2Qs5Cg --- tests/test_axis_names.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tests/test_axis_names.py b/tests/test_axis_names.py index 5e469a4..1e28485 100644 --- a/tests/test_axis_names.py +++ b/tests/test_axis_names.py @@ -30,9 +30,15 @@ def test_stage_names(init_qt): prog.axis_names = axis_names assert prog.axis_names == axis_names + # units and epsilons are defined per axis: they must follow the new axis names + prog.axis_units = '' + prog.epsilons = prog.epsilon + # the (mock) controller must know the new axis, the position is read when the axis changes + prog.controller.axes_indexes = [*prog.controller.axes_indexes, index_axis] + prog.controller._current_value = [*prog.controller._current_value, 0.] assert name_axis in prog.axis_names prog.axis_name = name_axis assert name_axis == prog.axis_name - assert prog.settings.child('multiaxes', 'axis').value() == index_axis + assert prog.settings.child('controller', 'axis').value() == index_axis assert prog.axis_value == index_axis From af05dbdef02bfbe9c6b6f92f18db3e6aa4c2b118 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 06:26:30 +0000 Subject: [PATCH 07/11] Follow the plugin template: acceptance stub from pymodaq.utils.plugin_testing, shared workflows, AGENTS.md Remove the copies of the workflows (Test.yml, Testbase.yml) and call the reusable ones of pymodaq_plugins_template; add AGENTS.md and CLAUDE.md for AI coding assistants. This replaces the CI fixes made earlier on this branch to the copied workflow. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01Tdiso1u1ZhikSutj2Qs5Cg --- .github/workflows/Test.yml | 10 -- .github/workflows/Testbase.yml | 45 ------ .github/workflows/compatibility.yml | 15 ++ .github/workflows/python-publish.yml | 35 +---- .github/workflows/tests.yml | 8 + AGENTS.md | 206 +++++++++++++++++++++++++ CLAUDE.md | 3 + tests/test_plugin_package_structure.py | 118 +++----------- 8 files changed, 257 insertions(+), 183 deletions(-) delete mode 100644 .github/workflows/Test.yml delete mode 100644 .github/workflows/Testbase.yml create mode 100644 .github/workflows/compatibility.yml create mode 100644 .github/workflows/tests.yml create mode 100644 AGENTS.md create mode 100644 CLAUDE.md diff --git a/.github/workflows/Test.yml b/.github/workflows/Test.yml deleted file mode 100644 index 05bc4e5..0000000 --- a/.github/workflows/Test.yml +++ /dev/null @@ -1,10 +0,0 @@ -name: 'Tests' - -on: [push] - -jobs: - call_workflow: - uses: ./.github/workflows/Testbase.yml - with: - python: '3.12' - qt5: 'pyqt6' diff --git a/.github/workflows/Testbase.yml b/.github/workflows/Testbase.yml deleted file mode 100644 index f0a3e3d..0000000 --- a/.github/workflows/Testbase.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: Base - -on: - workflow_call: - inputs: - python: - required: true - type: string - qt5: - required: true - type: string - -jobs: - build: - runs-on: ubuntu-latest - env: - DISPLAY: ':99.0' - QT_DEBUG_PLUGINS: 1 - steps: - - name: Set up Python ${{ inputs.python }} - uses: actions/checkout@v3 - - name: Install dependencies - uses: actions/setup-python@v4 - with: - python-version: ${{ inputs.python }} - - name: Install package - run: | - sudo apt install libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0 x11-utils libegl1 libgl1 libxcb-cursor0 - python -m pip install --upgrade pip - export QT_DEBUG_PLUGINS=1 - pip install flake8 pytest pytest-cov pytest-qt pytest-xdist pytest-xvfb setuptools wheel numpy h5py ${{ inputs.qt5 }} toml - pip install pymodaq - pip install -e . - - name: create local pymodaq folder and setting permissions - run: | - sudo mkdir /etc/.pymodaq - sudo chmod uo+rw /etc/.pymodaq - - name: Linting with flake8 - run: | - # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics --exclude=src/pymodaq/resources/QtDesigner_Ressources,docs - - name: Test with pytest - run: | - pytest - diff --git a/.github/workflows/compatibility.yml b/.github/workflows/compatibility.yml new file mode 100644 index 0000000..3779c46 --- /dev/null +++ b/.github/workflows/compatibility.yml @@ -0,0 +1,15 @@ +# Everything is defined once in PyMoDAQ/pymodaq_plugins_template (reusable-*.yml). The template is never released, so the ref after @ is its 5.3.x branch. +name: Compatibility with pymodaq (latest release) + +on: + pull_request: + push: + branches: [main, master, '[0-9]*.x'] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + compatibility: + uses: PyMoDAQ/pymodaq_plugins_template/.github/workflows/reusable-compatibility.yml@5.3.x diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml index 76fca2c..2b1d214 100644 --- a/.github/workflows/python-publish.yml +++ b/.github/workflows/python-publish.yml @@ -1,6 +1,4 @@ -# This workflow will upload a Python Package using Twine when a release is created -# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries - +# Everything is defined once in PyMoDAQ/pymodaq_plugins_template (reusable-*.yml). The template is never released, so the ref after @ is its 5.3.x branch. name: Upload Python Package on: @@ -8,31 +6,6 @@ on: types: [created] jobs: - deploy: - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.x' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install hatch hatchling toml - - name: Get history and tags for SCM versioning to work - run: | - git branch - git fetch --prune --unshallow - git fetch --depth=1 origin +refs/tags/*:refs/tags/* - hatch version - - name: Build - run: hatch build - - name: publish - env: - HATCH_INDEX_USER: ${{ secrets.PYPI_USERNAME }} - HATCH_INDEX_AUTH: ${{ secrets.PYPI_PASSWORD }} - run: | - hatch publish + publish: + uses: PyMoDAQ/pymodaq_plugins_template/.github/workflows/reusable-publish.yml@5.3.x + secrets: inherit diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..4291bb9 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,8 @@ +# Everything is defined once in PyMoDAQ/pymodaq_plugins_template (reusable-*.yml). The template is never released, so the ref after @ is its 5.3.x branch. +name: Tests + +on: [push, pull_request] + +jobs: + tests: + uses: PyMoDAQ/pymodaq_plugins_template/.github/workflows/reusable-tests.yml@5.3.x diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..e36f049 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,206 @@ +# AGENTS.md + +Guidance for AI coding assistants (and humans) building on **PyMoDAQ 5.3.x** from this template. +PyMoDAQ is a Qt framework that drives lab experiments: hardware is added through small *plugins*, and PyMoDAQ +provides the GUI, threading, data model, scans and HDF5 saving. Python 3.10 to 3.13. + +This repository is a plugin package named `pymodaq_plugins_`. Rename everything that still says `template` +before publishing. + +## 1. Choose what to build + +Prefer the first option that fits: + +1. **Instrument plugin** (`daq_move_plugins/`, `daq_viewer_plugins/`): a new device must appear in the Dashboard. +2. **Dashboard extension** (`extensions/`, class derived from `CustomExt`): a workflow that uses instruments + (scans, feedback, custom panels, logging). It runs inside the Dashboard and reuses its stable mechanisms for + instruments: the experiment manager and the state manager (how a Dashboard setup is described, loaded and + restored; the word "preset" is obsolete), the module manager, control-module threading, shared controllers, + saving. **If the application drives instruments, build a `CustomExt`, not a `CustomApp`.** +3. **Standalone app** (`app/`, class derived from `CustomApp`): only when the application needs **no + instruments** (data viewer, analysis, h5 browsing, configuration tools). +4. A plain script, for a one-off sequence that does not need a GUI. + +A **model** (section 6) is not a separate option: it tailors an existing extension (PID, optimizers, data mixer, +sequencer) to your experiment. + +## 2. Workflow + +```bash +pip install -e . # editable install of this plugin, from the repository root +check_plugin # static report on this package (hardware not needed) +check_plugin --fail-on todo -v # also list every unfinished TODO +pytest # template tests (package structure, plugin checks, behaviour against a fake controller) +``` + +Installing PyMoDAQ itself is a separate matter. A plugin only needs the released packages (`pymodaq`, +`pymodaq_utils`, `pymodaq_data`, `pymodaq_gui`, installed as dependencies). If you also have to modify them, work in a +clone of the PyMoDAQ repository and install the four packages in editable mode with the script at its root, never with +`pip install -e .` inside each package (the script handles the order and the options): + +```bash +python install-packages.py -d # editable + dev extras, from the PyMoDAQ repository root +python install-packages.py -d -qt pyside6 # choose the Qt backend (pyqt5, pyqt6 or pyside6) +``` + +Do not consider a task finished until `check_plugin` and `pytest` pass. Do not invent PyMoDAQ APIs: if a method +or attribute is not in the base classes (`DAQ_Move_base`, `DAQ_Viewer_base`, `CustomExt`) or in an existing +plugin, look it up in the installed `pymodaq` package instead of guessing. + +## 3. Instrument plugins + +File and class names must match: `daq_move_.py` holds `DAQ_Move_`; `daq_Dviewer_.py` holds +`DAQ_DViewer_` (N = 0, 1, 2 or N). Each file ends with `if __name__ == '__main__': main(__file__)`. + +**Actuator** (`DAQ_Move_base`). Override at least, in the order of the template (the order in which to develop them), +`ini_attributes`, `ini_stage`, `close`, `get_actuator_value`, `move_abs`, `move_rel`, `move_home`, `stop_motion`, +`commit_settings`. + +- `ini_stage(controller=None)` returns `(info: str, initialized: bool)`. If `self.is_master`, create the controller; + otherwise store the `controller` passed in (several axes share one controller). +- Class attributes: `is_multiaxes`, `_axis_names`, `_controller_units`, `_epsilons`, + `data_actuator_type = DataActuatorType.DataActuator`, optionally `ui_type` and `has_encoder`. Always write the + `data_actuator_type` line: the default of the base class is `DataActuatorType.float`, a backcompatibility mode where + `move_abs`/`move_rel` receive a plain float (already in the axis unit) and `get_actuator_value` returns one. PyMoDAQ + guards that case, but do not use it in new code. +- `params` = list of dicts + `comon_parameters_fun(is_multiaxes, axis_names=..., epsilon=...)`. +- Positions are `DataActuator` objects with **units** (`units=self.axis_unit`); use `check_bound`, + `set_position_with_scaling` and `get_position_with_scaling` as in the template. + +**Detector** (`DAQ_Viewer_base`). Override at least, in the order of the template, `ini_attributes`, `ini_detector`, +`close`, `grab_data`, `stop`, `commit_settings`. + +- `ini_detector(controller=None)` returns `(info, initialized)`, same master/slave logic. +- `params = comon_parameters + [...]`. +- Data leaves the plugin as `DataToExport` containing `DataFromPlugins` (with `dim='Data0D'|'Data1D'|'Data2D'|'DataND'`, + `labels`, `axes=[Axis(...)]`) emitted through `self.dte_signal` (grab) or `self.dte_signal_temp` (live preview). + +**Rules that matter** + +- Plugin code runs in a worker thread. **Never create or touch Qt widgets** from it. Report progress with + `self.emit_status(ThreadCommand('Update_Status', ['message']))`. +- Do not block for long inside `grab_data` or a move; use the asynchronous pattern shown in the template and the + polling that PyMoDAQ already does (`epsilon`). +- Always release the hardware in `close()`, only if `self.is_master and self.controller is not None` (the init may have + failed before the controller existed). Leave actuators in a safe state on error. +- Settings are `pyqtgraph` Parameter dicts (`{'title', 'name', 'type', 'value', ...}`). React to changes in + `commit_settings(param)` by `param.name()`. +- Units are `pint`-based. Give every actuator value and every `Axis` a unit; mismatches raise `DataUnitError`. + What you send to a controller must be expressed in the controller units: use `value.value(self.axis_unit)` in + `move_abs` / `move_rel`, never `value.value()` (the magnitude in whatever unit the `DataActuator` carries, so 1 cm + would be sent as 1.0 to a controller in mm). `axis_unit` is the unit of the current axis, `axis_units` the list/dict + of all the axes of a multiaxes controller. +- Hardware access code goes in `hardware/`, behind a small wrapper class. This lets the plugin be tested with a + mock controller and keeps the plugin file readable. +- Defaults live in `resources/config_template.toml`, not in the code. Read configuration values through + `GlobalConfig` from `pymodaq_utils.config`: a singleton that wraps the `Config` objects of all packages, e.g. + `GlobalConfig()('gui', 'style', 'theme')`. Do not parse the toml files yourself, and do not instantiate a `Config` + class directly (deprecated): PyMoDAQ registers the `Config` of `/utils.py` in `GlobalConfig` when it + discovers the plugin, under the package name without `pymodaq_plugins_`. + +**Testing behaviour without hardware.** `pymodaq.utils.plugin_testing` (shipped with PyMoDAQ >= 5.3.2) drives a plugin the way PyMoDAQ does, without a GUI: +`make_actuator` / `make_detector` (instantiate and call `ini_stage` / `ini_detector`), `move_abs_and_wait`, +`move_rel_and_wait`, `move_home_and_wait`, `grab_and_wait` (return the final position or the `DataToExport`, and fail +on a timeout) and `assert_units`. `tests/example_mock_plugins.py` shows a fake controller and the smallest actuator, +multi-axes actuator and 1D detector; `tests/test_plugin_behaviour.py` shows the tests to copy. Write the vendor +communication behind a wrapper class in `hardware/`, write a fake with the same public methods, and run your real +plugin class against it (monkeypatch the wrapper class). Targets reach the plugin in the axis unit, as in PyMoDAQ. + +These tests only check your side of the PyMoDAQ contract (return types, units, signals, shared controller, `close`) +against a fake that encodes what you believe the driver does. They say nothing about the device: timing, error replies, +real ranges and units. A passing run is not a hardware validation: never state that a plugin works on an instrument +that was not used, say what was tested on the real device and what only against a fake. + +**Legacy patterns to avoid** (older tutorials and models still produce them; `check_plugin` flags several): +`stage_names` (use `_axis_names`), `_epsilon` (use `_epsilons`), `data_actuator_type = float` (use +`DataActuatorType.DataActuator`), the group names `multiaxes` and `multi_status` (now `controller` and +`controller_status`), importing `CustomExt` from `pymodaq.extensions.custom_ext` (use `pymodaq.utils.custom_ext`), +and the deprecated `CustomApp` / `CustomExt` hooks `setup_docks` and `setup_menu` (use `setup_docks_and_widgets` and +`setup_menus_and_toolbars`). + +## 4. Dashboard extensions (`CustomExt`) + +- Entry point group `pymodaq.extensions` in `pyproject.toml`; each module in `extensions/` defines + `EXTENSION_NAME` (menu label), `CLASS_NAME` (class name) and a class derived from `CustomExt`. +- Import: `from pymodaq.utils.custom_ext import CustomExt`. +- A plugin extension follows exactly the pattern of the core extensions (`daq_scan`, `sequencer`, `ramping`, data + mixer). The only difference is `EXTENSION_NAME` and `CLASS_NAME`, which the Dashboard needs to recognise it. +- **`_quit_fun(self) -> bool` must be overridden and return `True`** (return `False`, after telling the user, while + the extension is running). The base implementation returns `None`, which prevents closing. +- **`main()` pattern** (to run the extension alone, same as the core ones): + + ```python + def main(): + import sys + from pymodaq_gui.qt_utils import mkQApp + from pymodaq.dashboard import load_dashboard_with_arguments + from pymodaq.utils.gui_utils.loader_utils import create_extension + + app = mkQApp('My Extension') + win, dashboard, _ = load_dashboard_with_arguments(show_dashboard=False, load_extension=False) + win.mainwindow.setVisible(False) + win_ext, ext = create_extension(dashboard, MyExtension, show_extension=True) + sys.exit(app.exec()) + ``` + + `load_dashboard_with_arguments` reads the command line (`-x EXPERIMENT_NAME`, `-s STATE_NAME`). + `create_extension` already shows the window: do not call `win_ext.show()` again, and do not use `create_load_dashboard` + here. +- Long-running work (a scan, a sequence, a ramp) goes in an `ExtensionWorker` subclass run in a thread, started and + stopped from the extension's workflow actions, as in `sequencer.py` and `ramping.py`. +- `__init__(self, parent: DockArea, dashboard)`: call `super().__init__(parent, dashboard)`, then `self.setup_ui()`. +- **Experiment and state belong to the Dashboard.** The *experiment* is the list of instruments (actuators and + detectors) the Dashboard manages and controls; the *state* is the state of that experiment, mostly the values of the + instruments' settings and of the Dashboard's actuators. An extension must use the managers it gets through its + `dashboard` argument, as `self.experiment_manager` and `self.state_manager`, and never create its own, nor its own + save/load of instrument configurations. Override `do_things_after_experiment_set(experiment_name, show_dashboard)` + to react when an experiment is set (the base class refreshes `self.modules_manager` there), and use + `create_dashboard_toolbar(...)` to expose the experiment and state actions in your extension. +- The Dashboard's instruments are reached through `self.modules_manager`. This `ModulesManager` is created anew for + each extension by the base class `__init__`, so that every extension can handle different instruments: never share + it between extensions, replace it, or reuse the Dashboard's one. Do not create control modules yourself inside an + extension, and do not talk to the hardware directly. +- Implement the lifecycle, in the order `setup_ui` calls it: `setup_docks_and_widgets`, `setup_menus_and_toolbars` + (only creates the menus and toolbars), `setup_actions` (creates the actions and can put them in those menus and + toolbars with `add_action(..., menu=..., toolbar=...)`), `connect_things`, then `value_changed`, `quit_fun`. `setup_docks` and `setup_menu` are deprecated. The GUI thread runs + your code; react to module signals, never poll or sleep. + +## 5. Standalone apps (`CustomApp`) + +Only when no instrument is needed. Same (non-deprecated) lifecycle as above, `parent` is a `DockArea` or `QMainWindow`, and +`main()` creates the app with `mkQApp`. If instruments become necessary, move the logic to a `CustomExt`. + +## 6. Models of the extensions (`models/`) + +Four Dashboard extensions delegate the part that depends on your experiment to a model class that your package +provides: PID (`PIDModelGeneric`), Bayesian and Adaptive optimizers (`OptimizerModelGeneric` or `OptimizerModelDefault`), +DataMixer (`DataMixerModel`) and the Sequencer (`ChoiceModelBase`). Write a model, not an extension, when one of these +extensions already does the job and only the conversion of the measurements or of the outputs is specific. + +- Set `models = true` in `[features]`; entry points are generated only then (`pymodaq.models`). +- Create the `models/` folder at the root of the package, one model per module; the module name is the name shown in + the extension. The model is the class defined in the module that derives from the base class of the extension. It may + derive from your own intermediate class, but PID and DataMixer only accepted a direct subclass of the generic base + before the release that follows 5.3.3, so derive directly from it if you must support older releases. The Sequencer + is the exception: it does not look at base classes, you register each choice model with + `@ChoiceModelFactory.register_choice()`. +- The template ships no model on purpose. Start from the working examples of + [`pymodaq_plugins_mockexamples`](https://github.com/PyMoDAQ/pymodaq_plugins_mockexamples) (`PIDModelBeamSteering`, + `PIDModelMockHeater`, `BayesianModelMock`, `DataMixerModelFit`; for the Sequencer, the choice models of + `pymodaq/extensions/sequencer/models`) and from the page "Models of the extensions" of the PyMoDAQ + documentation, which lists the methods to implement for each kind. Test a model with the mock actuators and + detectors of that package. +- Do not use the old `pid_models` flag nor the word "preset" in new code: a model script that opens the Dashboard loads + an experiment file. + +## 7. Done checklist + +- [ ] Names (package, files, classes) match and the `template` placeholders are gone. +- [ ] The `[features]` flags at the top of `pyproject.toml` match the folders you use (`instruments`, `extensions`, + `models`...): entry points are generated only for the features set to `true` (`check_plugin` warns, PMQ109). +- [ ] `check_plugin` passes; no `NotImplementedError` or `TODO` left in the files you wrote. +- [ ] Units set on all actuator values and axes; safe behaviour in `close()` and on errors. +- [ ] Tests added for new behaviour using a mock controller (no real hardware in CI). +- [ ] Docstring of each plugin class states: compatible devices, what was tested, PyMoDAQ and OS versions, + drivers to install. +- [ ] `README.rst` updated; target PyMoDAQ version stated. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..bf683e0 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,3 @@ +# CLAUDE.md + +Read and follow [AGENTS.md](AGENTS.md): it holds all the guidance for AI coding assistants working in this repository. diff --git a/tests/test_plugin_package_structure.py b/tests/test_plugin_package_structure.py index 8bcd08c..a7bdf2b 100644 --- a/tests/test_plugin_package_structure.py +++ b/tests/test_plugin_package_structure.py @@ -1,104 +1,28 @@ -# -*- coding: utf-8 -*- -""" -Created the 17/10/2023 - -@author: Sebastien Weber -""" -import pytest -from pathlib import Path -import importlib -import pkgutil -from collections.abc import Iterable - -from pymodaq_data import Q_, Unit - -MANDATORY_MOVE_METHODS = ['ini_attributes', 'get_actuator_value', 'close', 'commit_settings', - 'ini_stage', 'move_abs', 'move_home', 'move_rel', 'stop_motion'] -MANDATORY_VIEWER_METHODS = ['ini_attributes', 'grab_data', 'close', 'commit_settings', - 'ini_detector', ] - - -def get_package_name(): - here = Path(__file__).parent - package_name = here.parent.stem - return package_name - - -def get_move_plugins(): - pkg_name = get_package_name() - move_mod = importlib.import_module(f'{pkg_name}.daq_move_plugins') +"""Acceptance tests of the plugin, provided by PyMoDAQ (from version 5.3.2). - plugin_list = [mod for mod in [mod[1] for mod in - pkgutil.iter_modules([str(move_mod.path.parent)])] - if 'daq_move_' in mod] - return plugin_list, move_mod - - -def get_viewer_plugins(dim='0D'): - pkg_name = get_package_name() - viewer_mod = importlib.import_module(f'{pkg_name}.daq_viewer_plugins.plugins_{dim}') - - plugin_list = [mod for mod in [mod[1] for mod in - pkgutil.iter_modules([str(viewer_mod.path.parent)])] - if f'daq_{dim}viewer_' in mod] - return plugin_list, viewer_mod - - -def test_package_name_ok(): - assert 'pymodaq_plugins_' in get_package_name()[0:16] - - -def test_imports(): - pkg_name = get_package_name() - mod = importlib.import_module(pkg_name) - assert hasattr(mod, 'config') - assert hasattr(mod, '__version__') - move_mod = importlib.import_module(f'{pkg_name}', 'daq_move_plugins') - importlib.import_module(f'{pkg_name}', 'daq_viewer_plugins') - importlib.import_module(f'{pkg_name}', 'extensions') - importlib.import_module(f'{pkg_name}', 'models') - importlib.import_module(f'{pkg_name}.daq_viewer_plugins', 'plugins_0D') - importlib.import_module(f'{pkg_name}.daq_viewer_plugins', 'plugins_1D') - importlib.import_module(f'{pkg_name}.daq_viewer_plugins', 'plugins_2D') - importlib.import_module(f'{pkg_name}.daq_viewer_plugins', 'plugins_ND') +They check the package structure, the entry points, the naming of the instrument modules and classes, the mandatory +attributes and methods of the plugin classes, and apply the static rules of PyMoDAQ on the sources +(see https://pymodaq.cnrs.fr/en/latest/developer_folder/instrument_plugins.html#testing-your-plugin). +The unfinished parts of the plugin (TODO comments, placeholders) are listed but do not fail the tests, set +``fail_on = 'todo'`` before a release. Add your own tests, specific to your instruments, in other files. +""" +import importlib.util -def test_move_inst_plugins_name(): - plugin_list, move_mod = get_move_plugins() - for plug in plugin_list: - name = plug.split('daq_move_')[1] - assert hasattr(getattr(move_mod, plug), f'DAQ_Move_{name}') +import pytest +try: + available = importlib.util.find_spec('pymodaq.utils.plugin_testing') is not None +except ModuleNotFoundError: # pymodaq is not installed + available = False +if not available: + pytest.skip('The plugin acceptance checks need PyMoDAQ >= 5.3.2', allow_module_level=True) -def test_move_has_mandatory_methods(): - plugin_list, move_mod = get_move_plugins() - for plug in plugin_list: - name = plug.split('daq_move_')[1] - klass = getattr(getattr(move_mod, plug), f'DAQ_Move_{name}') - for meth in MANDATORY_MOVE_METHODS: - assert hasattr(klass, meth) +from pymodaq.utils.plugin_testing import PluginPackageChecks -def test_move_has_correct_units(): - plugin_list, move_mod = get_move_plugins() - for plug in plugin_list: - name = plug.split('daq_move_')[1] - klass = getattr(getattr(move_mod, plug), f'DAQ_Move_{name}') - if not isinstance(klass._controller_units, list): - units = [klass._controller_units] - else: - units = klass._controller_units - for unit in units: - Unit(unit) # check if the unit is known from pint -@pytest.mark.parametrize('dim', ('0D', '1D', '2D', 'ND')) -def test_viewer_has_mandatory_methods(dim): - plugin_list, mod = get_viewer_plugins(dim) - for plug in plugin_list: - name = plug.split(f'daq_{dim}viewer_')[1] - try: - module = importlib.import_module(f'.{plug}', mod.__package__) - except Exception: - break - klass = getattr(module, f'DAQ_{dim}Viewer_{name}') - for meth in MANDATORY_VIEWER_METHODS: - assert hasattr(klass, meth) +class TestPlugin(PluginPackageChecks): + # package_name = 'pymodaq_plugins_xxxx' # only if it cannot be found from the pyproject.toml and the package folder + fail_on = 'error' # 'error', 'warning' or 'todo' (also fail on the unfinished parts) + strict_imports = False # True: also fail when a module cannot be imported (missing dependency or driver); the + # modules of an instrument whose driver cannot be installed on the CI runner are then skipped, not failed From 6d0cd15ad7fd2a21b72227b1a9860004292255f3 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 06:27:24 +0000 Subject: [PATCH 08/11] MockHeater: implement close(), required by the plugin acceptance checks Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01Tdiso1u1ZhikSutj2Qs5Cg --- .../daq_move_plugins/daq_move_MockHeater.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/pymodaq_plugins_mockexamples/daq_move_plugins/daq_move_MockHeater.py b/src/pymodaq_plugins_mockexamples/daq_move_plugins/daq_move_MockHeater.py index b9eef54..1445635 100644 --- a/src/pymodaq_plugins_mockexamples/daq_move_plugins/daq_move_MockHeater.py +++ b/src/pymodaq_plugins_mockexamples/daq_move_plugins/daq_move_MockHeater.py @@ -35,6 +35,10 @@ def get_actuator_value(self) -> DataActuator: pos = self.get_position_with_scaling(pos) return pos + def close(self): + """Terminate the communication protocol (nothing to release for the simulated heater)""" + pass + def commit_settings(self, param): pass From 579b6035f91e78fd4a86dd9b2f80fe870b21153b Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 06:30:44 +0000 Subject: [PATCH 09/11] flake8: skip json-examples, it uses Python 3.12 syntax and is not part of the package Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01Tdiso1u1ZhikSutj2Qs5Cg --- .flake8 | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .flake8 diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..7f3c578 --- /dev/null +++ b/.flake8 @@ -0,0 +1,3 @@ +[flake8] +# json-examples uses Python 3.12 syntax (type aliases) and is not part of the installed package +exclude = json-examples,.git,build,dist From d6cf9881feb85697a7ffbf93760bc13855233685 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 06:48:42 +0000 Subject: [PATCH 10/11] Declare _axis_names in MockTempController The acceptance checks need one axis name per unit in _controller_units. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01Tdiso1u1ZhikSutj2Qs5Cg --- .../daq_move_plugins/daq_move_MockTempController.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pymodaq_plugins_mockexamples/daq_move_plugins/daq_move_MockTempController.py b/src/pymodaq_plugins_mockexamples/daq_move_plugins/daq_move_MockTempController.py index f55fe57..bb6e551 100644 --- a/src/pymodaq_plugins_mockexamples/daq_move_plugins/daq_move_MockTempController.py +++ b/src/pymodaq_plugins_mockexamples/daq_move_plugins/daq_move_MockTempController.py @@ -16,7 +16,7 @@ class DAQ_Move_MockTempController(DAQ_Move_base): """ _controller_units = ['K', 'W'] is_multiaxes = True - stage_names = ['Temperature', 'Power'] + _axis_names = ['Temperature', 'Power'] _epsilon = [0.01, 0.01] params = [ @@ -29,7 +29,7 @@ class DAQ_Move_MockTempController(DAQ_Move_base): {'title': 'HasCooling:', 'name': 'has_cooling', 'type': 'led', 'value': TempController.has_cooling}, {'title': 'Enabled:', 'name': 'enabled', 'type': 'led', 'value': False}, {'title': 'Reset', 'name': 'reset', 'type': 'bool_push', 'value': False, 'label': 'Reset'}, - ] + comon_parameters_fun(is_multiaxes, stage_names, epsilon=_epsilon) + ] + comon_parameters_fun(is_multiaxes, _axis_names, epsilon=_epsilon) data_actuator_type = DataActuatorType.DataActuator def ini_attributes(self): From 137654e12a3c594b3c26202187e977246592fd04 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 12 Oct 2026 08:13:46 +0000 Subject: [PATCH 11/11] Raise the dependency floors to the template ones pymodaq>=5.3.2, pymodaq_utils>=5.3.1, pymodaq_gui>=5.3.0, pymodaq_data>=5.3.0, so that the acceptance tests and the rest of this change are met at install time. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01Tdiso1u1ZhikSutj2Qs5Cg --- pyproject.toml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index d465e9b..ee7e8e9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,8 +12,10 @@ package-url = 'https://github.com/PyMoDAQ/pymodaq_plugins_mockexamples' name = "pymodaq_plugins_mockexamples" description = 'Set of advanced Mock examples to test PyMoDAQ functionnalities' dependencies = [ - "pymodaq>=5.2.8", - "pymodaq_data>=5.2.8", + "pymodaq>=5.3.2", + "pymodaq_utils>=5.3.1", + "pymodaq_gui>=5.3.0", + "pymodaq_data>=5.3.0", "pillow", ]