Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .github/workflows/Test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
name: 'Tests'

on: [push]

jobs:
call_workflow:
uses: ./.github/workflows/Testbase.yml
with:
python: '3.11'
qt5: 'pyqt5'
44 changes: 44 additions & 0 deletions .github/workflows/Testbase.yml
Original file line number Diff line number Diff line change
@@ -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/[email protected]
- name: Install dependencies
uses: actions/[email protected]
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"
78 changes: 78 additions & 0 deletions .github/workflows/compatibility.yml
Original file line number Diff line number Diff line change
@@ -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/[email protected]

- name: Set up Python ${{ matrix.python-version }}
uses: actions/[email protected]
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/[email protected]
with:
name:
path: 'import_report_tests_${{ env.plugin_name }}_None.txt'
if-no-files-found: error


40 changes: 40 additions & 0 deletions .github/workflows/python-publish.yml
Original file line number Diff line number Diff line change
@@ -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/[email protected]
- name: Set up Python
uses: actions/[email protected]
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
24 changes: 24 additions & 0 deletions .github/workflows/updater.yml
Original file line number Diff line number Diff line change
@@ -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/[email protected]
with:
# [Required] Access token with `workflow` scope.
token: ${{ secrets.WORKFLOW_SECRET }}

- name: Run GitHub Actions Version Updater
uses: saadmk11/[email protected]
with:
# [Required] Access token with `workflow` scope.
token: ${{ secrets.WORKFLOW_SECRET }}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
148 changes: 21 additions & 127 deletions tests/test_plugin_package_structure.py
Original file line number Diff line number Diff line change
@@ -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
Loading