From a87289ebf82cda86d15f28f3031fc06dbe5a9bb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Weber?= Date: Tue, 6 Oct 2026 21:50:48 +0200 Subject: [PATCH 1/6] Use the plugin acceptance checks provided by PyMoDAQ 5.3 Replace the copied generic tests with a stub using pymodaq.utils.plugin_testing. The module is skipped with pymodaq < 5.3. Co-Authored-By: Claude Sonnet 5.5 --- tests/test_plugin_package_structure.py | 144 +++---------------------- 1 file changed, 17 insertions(+), 127 deletions(-) diff --git a/tests/test_plugin_package_structure.py b/tests/test_plugin_package_structure.py index bde966f..0616957 100644 --- a/tests/test_plugin_package_structure.py +++ b/tests/test_plugin_package_structure.py @@ -1,134 +1,24 @@ -# -*- 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() - try: - move_mod = importlib.import_module(f'{pkg_name}.daq_move_plugins') - plugin_list = [mod for mod in [mod[1] for mod in - pkgutil.iter_modules([str(move_mod.path.parent)])] - if 'daq_move_' in mod] - except ModuleNotFoundError: - plugin_list = [] - move_mod = None - return plugin_list, move_mod - - -def get_viewer_plugins(dim='0D'): - pkg_name = get_package_name() - try: - viewer_mod = importlib.import_module(f'{pkg_name}.daq_viewer_plugins.plugins_{dim}') +"""Acceptance tests of the plugin, provided by PyMoDAQ (from version 5.3.0). - 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] - except ModuleNotFoundError: - plugin_list = [] - viewer_mod = None - 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') - - -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}') - - -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) - - -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): - if isinstance(klass._controller_units, dict): - units = list(klass._controller_units.values()) - elif isinstance(klass._controller_units, str): - units = [klass._controller_units] - else: - raise TypeError(f'{klass._controller_units} is an invalid type') - else: - units = klass._controller_units - for unit in units: - Unit(unit) # check if the unit is known from pint +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. +""" +from importlib.metadata import version -@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) +import pytest +from packaging.version import Version -def test_compatibility(capsys): - capsys.disabled() - try: - from pymodaq_plugin_manager.compatibility_checker import PyMoDAQPlugin - except (ModuleNotFoundError, ImportError) as e: - pytest.fail(f"Please update pymodaq_plugin_manager to a newer version: {e}") +if Version(version('pymodaq')).release[:2] < (5, 3): + pytest.skip('The plugin acceptance checks need pymodaq >= 5.3', allow_module_level=True) - plugin = PyMoDAQPlugin(get_package_name(), None) - success = plugin.all_imports_valid() - msg = '\n'.join(plugin.failed_imports + ['']) +from pymodaq.utils.plugin_testing import PluginPackageChecks - if not success: - plugin.save_import_report(".") - assert success, msg +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 = True # fail if a module cannot be imported because of a missing dependency From 5714a5fb672ecd2fe6127781e05c2ae7bc60888c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Weber?= Date: Tue, 6 Oct 2026 21:54:01 +0200 Subject: [PATCH 2/6] Stub: use pymodaq_utils.plugin_testing, skipped when it does not exist Co-Authored-By: Claude Sonnet 5.5 --- tests/test_plugin_package_structure.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/tests/test_plugin_package_structure.py b/tests/test_plugin_package_structure.py index 0616957..462103c 100644 --- a/tests/test_plugin_package_structure.py +++ b/tests/test_plugin_package_structure.py @@ -1,4 +1,4 @@ -"""Acceptance tests of the plugin, provided by PyMoDAQ (from version 5.3.0). +"""Acceptance tests of the plugin, provided by PyMoDAQ (from version 5.4.0). 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 @@ -7,15 +7,18 @@ 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. """ -from importlib.metadata import version +import importlib.util import pytest -from packaging.version import Version -if Version(version('pymodaq')).release[:2] < (5, 3): - pytest.skip('The plugin acceptance checks need pymodaq >= 5.3', allow_module_level=True) +try: + available = importlib.util.find_spec('pymodaq_utils.plugin_testing') is not None +except ModuleNotFoundError: # pymodaq_utils is not installed + available = False +if not available: + pytest.skip('The plugin acceptance checks need PyMoDAQ >= 5.4', allow_module_level=True) -from pymodaq.utils.plugin_testing import PluginPackageChecks +from pymodaq_utils.plugin_testing import PluginPackageChecks class TestPlugin(PluginPackageChecks): From 8f39fb815acd1f785e46bd7ac53f8434819a1626 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Weber?= Date: Tue, 6 Oct 2026 21:54:42 +0200 Subject: [PATCH 3/6] Restore the GitHub workflows removed by 7799554 (tests, compatibility, publication, updater) Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/Test.yml | 10 ++++ .github/workflows/Testbase.yml | 44 ++++++++++++++++ .github/workflows/compatibility.yml | 78 ++++++++++++++++++++++++++++ .github/workflows/python-publish.yml | 40 ++++++++++++++ .github/workflows/updater.yml | 24 +++++++++ 5 files changed, 196 insertions(+) create mode 100644 .github/workflows/Test.yml create mode 100644 .github/workflows/Testbase.yml create mode 100644 .github/workflows/compatibility.yml create mode 100644 .github/workflows/python-publish.yml create mode 100644 .github/workflows/updater.yml diff --git a/.github/workflows/Test.yml b/.github/workflows/Test.yml new file mode 100644 index 0000000..ffd1522 --- /dev/null +++ b/.github/workflows/Test.yml @@ -0,0 +1,10 @@ +name: 'Tests' + +on: [push] + +jobs: + call_workflow: + uses: ./.github/workflows/Testbase.yml + with: + python: '3.11' + qt5: 'pyqt5' \ No newline at end of file diff --git a/.github/workflows/Testbase.yml b/.github/workflows/Testbase.yml new file mode 100644 index 0000000..2d00e2f --- /dev/null +++ b/.github/workflows/Testbase.yml @@ -0,0 +1,44 @@ +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@v5.0.0 + - name: Install dependencies + uses: actions/setup-python@v6.0.0 + 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 + 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 pyqt5 + 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 -n auto -k "not test_compatibility" diff --git a/.github/workflows/compatibility.yml b/.github/workflows/compatibility.yml new file mode 100644 index 0000000..01003f5 --- /dev/null +++ b/.github/workflows/compatibility.yml @@ -0,0 +1,78 @@ +name: Compatibility with pymodaq (latest release) + +on: + workflow_call: + + pull_request: + + push: + branches: + - '*' + +concurrency: + # github.workflow: name of the workflow + # github.event.pull_request.number || github.ref: pull request number or branch name if not a pull request + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + # Cancel in-progress runs when a new workflow with the same group name is triggered + cancel-in-progress: true + +jobs: + tests: + continue-on-error: true + strategy: + fail-fast: false + matrix: + os: ["ubuntu-latest", "windows-latest"] + python-version: ["3.9", "3.10", "3.11", "3.12"] + qt-backend: ["pyqt5", "pyqt6", "pyside6"] + runs-on: ${{ matrix.os }} + env: + DISPLAY: ':99' + QT_DEBUG_PLUGINS: 1 + + steps: + - name: Set project name environment variable + run: | + echo "plugin_name=$(echo '${{ github.repository }}' | cut -d'/' -f2)" >> $GITHUB_ENV + + - name: Checkout the repo + uses: actions/checkout@v5.0.0 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v6.0.0 + with: + python-version: ${{ matrix.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install flake8 pytest pytest-cov pytest-qt pytest-xvfb pytest-xdist setuptools wheel numpy h5py pymodaq ${{ matrix.qt-backend }} + pip install -e . + + # Create folder and set permissions on Ubuntu + - name: Create local pymodaq folder setup env (Linux) + if: runner.os == 'Linux' + run: | + sudo apt update + sudo apt install -y libxkbcommon-x11-0 libxcb-icccm4 libxcb-image0 libxcb-keysyms1 libxcb-cursor0 libxcb-randr0 libxcb-render-util0 libxcb-xinerama0 libxcb-xfixes0 x11-utils libgl1 libegl1 + export QT_DEBUG_PLUGINS=1 + sudo mkdir -p /etc/.pymodaq + sudo chmod uo+rw /etc/.pymodaq + - name: Exporting debug variables (Windows) + if: runner.os == 'Windows' + run: | + set QT_DEBUG_PLUGINS=1 + + - name: Compatibility tests with ${{ matrix.os }} ${{ matrix.python-version }} ${{ matrix.qt-backend}} + run: | + pytest -vv -n 1 -k "test_compatibility" + + - name: Upload compatibility report + if: failure() + uses: actions/upload-artifact@v4.6.2 + with: + name: + path: 'import_report_tests_${{ env.plugin_name }}_None.txt' + if-no-files-found: error + + \ No newline at end of file diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml new file mode 100644 index 0000000..67c4a59 --- /dev/null +++ b/.github/workflows/python-publish.yml @@ -0,0 +1,40 @@ +# 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 + +name: Upload Python Package + +on: + release: + types: [created] + +jobs: + deploy: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v5.0.0 + - name: Set up Python + uses: actions/setup-python@v6.0.0 + with: + python-version: '3.x' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install hatch hatchling toml twine + - 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: Check the build + run: twine check dist/* + - name: publish + env: + HATCH_INDEX_USER: ${{ secrets.PYPI_USERNAME }} + HATCH_INDEX_AUTH: ${{ secrets.PYPI_PASSWORD }} + run: | + hatch publish diff --git a/.github/workflows/updater.yml b/.github/workflows/updater.yml new file mode 100644 index 0000000..8c60cea --- /dev/null +++ b/.github/workflows/updater.yml @@ -0,0 +1,24 @@ +name: GitHub Actions Version Updater + +# Controls when the action will run. +on: + workflow_dispatch: + schedule: + # Automatically run at 00:00 on day-of-month 5. + - cron: '0 0 5 * *' + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v5.0.0 + with: + # [Required] Access token with `workflow` scope. + token: ${{ secrets.WORKFLOW_SECRET }} + + - name: Run GitHub Actions Version Updater + uses: saadmk11/github-actions-version-updater@v0.9.0 + with: + # [Required] Access token with `workflow` scope. + token: ${{ secrets.WORKFLOW_SECRET }} \ No newline at end of file From ddd9a34268a3a87f0f25865d1b0d8140036ebf46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Weber?= Date: Tue, 6 Oct 2026 21:58:15 +0200 Subject: [PATCH 4/6] Stub: the checks come with PyMoDAQ 5.3.1 Co-Authored-By: Claude Sonnet 5.5 --- tests/test_plugin_package_structure.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_plugin_package_structure.py b/tests/test_plugin_package_structure.py index 462103c..ed27047 100644 --- a/tests/test_plugin_package_structure.py +++ b/tests/test_plugin_package_structure.py @@ -1,4 +1,4 @@ -"""Acceptance tests of the plugin, provided by PyMoDAQ (from version 5.4.0). +"""Acceptance tests of the plugin, provided by PyMoDAQ (from version 5.3.1). 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 @@ -16,7 +16,7 @@ except ModuleNotFoundError: # pymodaq_utils is not installed available = False if not available: - pytest.skip('The plugin acceptance checks need PyMoDAQ >= 5.4', allow_module_level=True) + pytest.skip('The plugin acceptance checks need PyMoDAQ >= 5.3.1', allow_module_level=True) from pymodaq_utils.plugin_testing import PluginPackageChecks From dea3964ff3cf87f1ded278485e16152d7022fe6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Weber?= Date: Wed, 7 Oct 2026 00:25:52 +0200 Subject: [PATCH 5/6] Template: no undefined names (arg1, arg2) in the actuator, the flake8 gate of the CI fails on them Co-Authored-By: Claude Sonnet 5.5 --- .../daq_move_plugins/daq_move_Template.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pymodaq_plugins_template/daq_move_plugins/daq_move_Template.py b/src/pymodaq_plugins_template/daq_move_plugins/daq_move_Template.py index 2b0097f..1dc9f25 100644 --- a/src/pymodaq_plugins_template/daq_move_plugins/daq_move_Template.py +++ b/src/pymodaq_plugins_template/daq_move_plugins/daq_move_Template.py @@ -146,7 +146,7 @@ def ini_stage(self, controller=None): """ raise NotImplementedError # TODO when writing your own plugin remove this line and modify the ones below if self.is_master: # is needed when controller is master - self.controller = PythonWrapperObjectOfYourInstrument(arg1, arg2, ...) # arguments for instantiation!) + self.controller = PythonWrapperObjectOfYourInstrument() # TODO add the arguments for the instantiation initialized = self.controller.a_method_or_atttribute_to_check_if_init() # todo # todo: enter here whatever is needed for your controller initialization and eventual # opening of the communication channel From 445169b01854055e1bdafcfe7a774e5aec4e04e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Weber?= Date: Wed, 7 Oct 2026 00:32:47 +0200 Subject: [PATCH 6/6] Stub: strict_imports is False by default, the vendor drivers cannot be installed in the CI Co-Authored-By: Claude Sonnet 5.5 --- tests/test_plugin_package_structure.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_plugin_package_structure.py b/tests/test_plugin_package_structure.py index ed27047..8be6086 100644 --- a/tests/test_plugin_package_structure.py +++ b/tests/test_plugin_package_structure.py @@ -24,4 +24,5 @@ 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 = True # fail if a module cannot be imported because of a missing dependency + 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