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 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 diff --git a/tests/test_plugin_package_structure.py b/tests/test_plugin_package_structure.py index bde966f..8be6086 100644 --- a/tests/test_plugin_package_structure.py +++ b/tests/test_plugin_package_structure.py @@ -1,134 +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() - 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.1). - 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. +""" +import importlib.util -@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 -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}") +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.3.1', 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 = 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