"""Check the modules of Dashboard extensions: the ``check_extension`` command.
It applies the rules PMQ330 to PMQ339 (see :func:`pymodaq_utils.plugin_tools.rules.check_extension_source`) to a python
file or to a folder, whether the extension belongs to a plugin package or to PyMoDAQ itself. The sources are read,
nothing is imported. ``check_plugin`` also applies these rules to the ``extensions`` folder of a plugin package.
.. versionadded:: 5.3.7
"""
from __future__ import annotations
import sys
from pathlib import Path
from typing import Optional, Sequence
from pymodaq_utils.plugin_tools.checks import FAIL_LEVELS, CheckResult, PluginReport
from pymodaq_utils.plugin_tools.rules import check_extension_source, use_color
[docs]
def is_core_extension(path: Path) -> bool:
"""Whether a file is in the ``extensions`` of the ``pymodaq`` package: the core extensions are registered in
``pymodaq.extensions`` and do not need to define ``EXTENSION_NAME`` and ``CLASS_NAME``"""
parts = path.resolve().parts
return any(a == 'pymodaq' and b == 'extensions' for a, b in zip(parts, parts[1:]))
[docs]
def find_extension_files(target: Path) -> list[Path]:
"""The python files to check: the file itself, or the modules of a folder
For a folder holding an ``extensions`` folder (the plugin package, or the repository with its ``src`` folder) the
modules of this folder are taken, otherwise all the modules of the folder and its sub-folders. The ``__init__``
and the private modules (starting with ``_``) are left out.
"""
if target.is_file():
return [target]
folder = next((c for c in (target / 'extensions', *sorted(target.glob('src/*/extensions')),
*sorted(target.glob('pymodaq_plugins_*/extensions'))) if c.is_dir()), target)
return [p for p in sorted(folder.rglob('*.py'))
if not p.name.startswith('_') and '__pycache__' not in p.parts]
[docs]
def check_extension_files(targets: Sequence[str | Path], fail_on: str = 'error',
core: Optional[bool] = None) -> PluginReport:
"""Run the extension rules on files or folders and return a report
Parameters
----------
targets: list of str or Path
python files of extensions, or folders (see :func:`find_extension_files`)
fail_on: str
'error' (default), 'warning' or 'todo', see :data:`pymodaq_utils.plugin_tools.checks.FAIL_LEVELS`
core: bool
True for the extensions of PyMoDAQ: PMQ330 (``EXTENSION_NAME`` and ``CLASS_NAME`` missing) is then not
reported. By default found from the path of each file (``pymodaq/extensions``).
"""
if fail_on not in FAIL_LEVELS:
raise ValueError(f'fail_on should be one of {list(FAIL_LEVELS)}')
results = []
for target in targets:
target = Path(target)
if not target.exists():
raise FileNotFoundError(target)
for path in find_extension_files(target):
is_core = is_core_extension(path) if core is None else core
findings = [f for f in check_extension_source(path) if not (is_core and f.code == 'PMQ330')]
try:
item = path.resolve().relative_to(Path.cwd()).as_posix()
except ValueError:
item = str(path)
results.append(CheckResult(item, findings=findings))
return PluginReport('extensions', results, fail_on)
[docs]
def main(argv=None) -> int:
"""Print the report on extension modules: the ``check_extension`` command (or
``python -m pymodaq_utils.plugin_tools.extension_checks``)
Returns 0 if the checks pass, 1 otherwise (so that it can also be used in a script or a CI).
"""
import argparse
parser = argparse.ArgumentParser(prog='check_extension',
description='Check the modules of PyMoDAQ Dashboard extensions and print a report')
parser.add_argument('paths', nargs='+',
help='python file of an extension, or folder: the extensions folder of a plugin package or of '
'PyMoDAQ, or the plugin repository')
parser.add_argument('--fail-on', choices=list(FAIL_LEVELS), default='error',
help="findings of this level and above give a non zero exit code ('todo': everything)")
parser.add_argument('--core', action='store_true', default=None,
help='the extensions are part of PyMoDAQ: EXTENSION_NAME and CLASS_NAME are not required '
'(found from the path by default)')
parser.add_argument('--color', choices=['auto', 'always', 'never'], default='auto',
help='color the report: auto (default) if the output is a terminal and NO_COLOR is not set')
args = parser.parse_args(argv)
try:
report = check_extension_files(args.paths, args.fail_on, args.core)
except FileNotFoundError as e:
parser.error(f'{e} does not exist')
print(report.format(verbose=True, color=use_color(args.color)))
return 0 if report.ok else 1
if __name__ == '__main__':
sys.exit(main())