-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #21 from malik-irain/plugins-compatibility-check
Weekly github action to check plugins compatibility
- Loading branch information
Showing
2 changed files
with
171 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,68 @@ | ||
name: Compatibility verification between PyMoDAQ and plugins | ||
|
||
on: | ||
schedule: | ||
- cron: '0 0 * * 0' # Runs at midnight UTC every Sunday | ||
workflow_dispatch: | ||
|
||
jobs: | ||
check-compatibility: | ||
strategy: | ||
fail-fast: false | ||
matrix: | ||
pymodaq-version: ["4.4.x", "5.0.x_dev"] | ||
runs-on: ubuntu-latest | ||
steps: | ||
- name: Set up Python | ||
uses: actions/setup-python@v5 | ||
with: | ||
python-version: '3.11' | ||
|
||
- name: Install Qt required packages | ||
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 | ||
python -m pip install --upgrade pip | ||
pip install hatch pyqt5 | ||
- name: Checkout pymodaq_utils | ||
uses: actions/checkout@v4 | ||
|
||
- name: Install pymodaq_utils | ||
run: | | ||
pip install . -e | ||
- name: Checkout PyMoDAQ | ||
uses: actions/checkout@v4 | ||
with: | ||
repository: 'PyMoDAQ/PyMoDAQ' | ||
ref: ${{ matrix.pymodaq-version }} | ||
path: PyMoDAQ | ||
|
||
- name: Install PyMoDAQ | ||
run: | | ||
cd PyMoDAQ | ||
git fetch --prune --unshallow | ||
git fetch --depth=1 origin +refs/tags/*:refs/tags/* | ||
pip install . -e | ||
- name: Switch back to pymodaq_utils | ||
run: cd $GITHUB_WORKSPACE | ||
|
||
- name: Run script | ||
run: | | ||
python compatibility/checker.py | ||
- name: Upload reports if failed | ||
if: failure() | ||
uses: actions/upload-artifact@v4 | ||
with: | ||
name: failure-reports-${{ pymodaq-version }} | ||
path: reports | ||
|
||
- name: Show pip list | ||
if: always() | ||
run: | | ||
pip list |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,103 @@ | ||
import chardet | ||
import logging | ||
import ast | ||
import sys | ||
from pathlib import Path | ||
import subprocess | ||
import importlib | ||
|
||
|
||
from pymodaq_plugin_manager.utils import get_pymodaq_version | ||
from pymodaq_plugin_manager.validate import get_plugins, get_pypi_pymodaq, get_package_metadata, get_pypi_plugins | ||
|
||
REPORT_FOLDER = Path("./reports/") | ||
|
||
def _detect_encoding(filename): | ||
with open(filename, "rb") as f: | ||
raw = f.read() | ||
return chardet.detect(raw)['encoding'] | ||
|
||
class PyMoDAQPlugin: | ||
def __init__(self, name, version): | ||
self._name = name | ||
self._version = version | ||
self._install_result = None | ||
|
||
@property | ||
def name(self): | ||
return self._name | ||
|
||
@property | ||
def version(self): | ||
return self._version | ||
|
||
def _get_location(self): | ||
return importlib.util.find_spec(self._name).submodule_search_locations[0] | ||
|
||
def install(self) -> bool: | ||
#print(f'Trying to install {self._name}=={self._version}') | ||
command = [sys.executable, '-m', 'pip', 'install', f'{self._name}=={self._version}'] | ||
self._install_result = subprocess.run(command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) | ||
return self._install_result.returncode == 0 | ||
|
||
def _save_report(self, name, stream): | ||
with open(REPORT_FOLDER / name, 'w') as f: | ||
f.write(stream) | ||
|
||
def save_install_report(self): | ||
self._save_report(f'install_report_{self._name}_{self._version}.txt', self._install_result.stdout) | ||
|
||
def save_import_report(self): | ||
self._save_report(f'import_report_{self._name}_{self._version}.txt', '\n'.join(self._failed_imports + [''])) | ||
|
||
def all_imports_valid(self) -> bool: | ||
self._failed_imports = [] | ||
install_path = self._get_location() | ||
|
||
for filename in Path(install_path).glob('**/*.py'): | ||
with open(filename, 'r', encoding=_detect_encoding(filename)) as f: | ||
tree = ast.parse(f.read(), filename=filename) | ||
for node in tree.body: | ||
try: | ||
if (isinstance(node, ast.ImportFrom) and 'pymodaq' in node.module) \ | ||
or (isinstance(node, ast.Import) and any('pymodaq' in name.name for name in node.names)): | ||
for name in node.names: | ||
try: | ||
if isinstance(node, ast.ImportFrom): | ||
import_code = f'from {node.module} import {name.name}' | ||
getattr(importlib.import_module(node.module), name.name) | ||
elif isinstance(node, ast.Import): | ||
import_code = f'import {name.name}' | ||
if name.asname: | ||
import_code += f' as {name.asname}' | ||
importlib.import_module(node.module) | ||
|
||
except (ImportError, ModuleNotFoundError): | ||
self._failed_imports.append(f'"{import_code}" in {filename} ({node.lineno})') | ||
except Exception as e: | ||
print(f'Unknown: {e}') | ||
except TypeError as te: | ||
pass | ||
|
||
return len(self._failed_imports) == 0 | ||
|
||
|
||
|
||
def main(): | ||
code = 0 | ||
REPORT_FOLDER.mkdir(parents=True, exist_ok=True) | ||
|
||
plugin_list = get_pypi_plugins() | ||
|
||
for p in plugin_list: | ||
plugin = PyMoDAQPlugin(p['plugin-name'], p['version']) | ||
if plugin.install(): | ||
if not plugin.all_imports_valid(): | ||
plugin.save_import_report() | ||
code = 1 | ||
else: | ||
plugin.save_install_report() | ||
code = 1 | ||
sys.exit(code) | ||
if __name__ == '__main__': | ||
main() |