pymodaq_utils 5.3.4__tar.gz → 5.3.6__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pymodaq_utils-5.3.4 → pymodaq_utils-5.3.6}/PKG-INFO +1 -1
- pymodaq_utils-5.3.6/src/pymodaq_utils/plugin_checks.py +14 -0
- pymodaq_utils-5.3.6/src/pymodaq_utils/plugin_rules.py +14 -0
- pymodaq_utils-5.3.6/src/pymodaq_utils/plugin_scaffold.py +14 -0
- pymodaq_utils-5.3.6/src/pymodaq_utils/plugin_tools/__init__.py +12 -0
- pymodaq_utils-5.3.4/src/pymodaq_utils/plugin_checks.py → pymodaq_utils-5.3.6/src/pymodaq_utils/plugin_tools/checks.py +38 -13
- pymodaq_utils-5.3.6/src/pymodaq_utils/plugin_tools/contract.py +115 -0
- pymodaq_utils-5.3.6/src/pymodaq_utils/plugin_tools/platforms.py +229 -0
- pymodaq_utils-5.3.4/src/pymodaq_utils/plugin_rules.py → pymodaq_utils-5.3.6/src/pymodaq_utils/plugin_tools/rules.py +105 -14
- pymodaq_utils-5.3.4/src/pymodaq_utils/plugin_scaffold.py → pymodaq_utils-5.3.6/src/pymodaq_utils/plugin_tools/scaffold.py +3 -3
- {pymodaq_utils-5.3.4 → pymodaq_utils-5.3.6}/src/pymodaq_utils/resources/hatch_build_plugins.py +6 -0
- {pymodaq_utils-5.3.4 → pymodaq_utils-5.3.6}/.gitignore +0 -0
- {pymodaq_utils-5.3.4 → pymodaq_utils-5.3.6}/LICENSE +0 -0
- {pymodaq_utils-5.3.4 → pymodaq_utils-5.3.6}/README.rst +0 -0
- {pymodaq_utils-5.3.4 → pymodaq_utils-5.3.6}/pyproject.toml +0 -0
- {pymodaq_utils-5.3.4 → pymodaq_utils-5.3.6}/src/pymodaq_utils/__init__.py +0 -0
- {pymodaq_utils-5.3.4 → pymodaq_utils-5.3.6}/src/pymodaq_utils/abstract.py +0 -0
- {pymodaq_utils-5.3.4 → pymodaq_utils-5.3.6}/src/pymodaq_utils/array_manipulation.py +0 -0
- {pymodaq_utils-5.3.4 → pymodaq_utils-5.3.6}/src/pymodaq_utils/categorizing.py +0 -0
- {pymodaq_utils-5.3.4 → pymodaq_utils-5.3.6}/src/pymodaq_utils/config.py +0 -0
- {pymodaq_utils-5.3.4 → pymodaq_utils-5.3.6}/src/pymodaq_utils/enums.py +0 -0
- {pymodaq_utils-5.3.4 → pymodaq_utils-5.3.6}/src/pymodaq_utils/environment.py +0 -0
- {pymodaq_utils-5.3.4 → pymodaq_utils-5.3.6}/src/pymodaq_utils/factory.py +0 -0
- {pymodaq_utils-5.3.4 → pymodaq_utils-5.3.6}/src/pymodaq_utils/help.py +0 -0
- {pymodaq_utils-5.3.4 → pymodaq_utils-5.3.6}/src/pymodaq_utils/icon.ico +0 -0
- {pymodaq_utils-5.3.4 → pymodaq_utils-5.3.6}/src/pymodaq_utils/logger.py +0 -0
- {pymodaq_utils-5.3.4 → pymodaq_utils-5.3.6}/src/pymodaq_utils/math_utils.py +0 -0
- {pymodaq_utils-5.3.4 → pymodaq_utils-5.3.6}/src/pymodaq_utils/mysocket.py +0 -0
- {pymodaq_utils-5.3.4 → pymodaq_utils-5.3.6}/src/pymodaq_utils/packages.py +0 -0
- {pymodaq_utils-5.3.4 → pymodaq_utils-5.3.6}/src/pymodaq_utils/plugin_testing.py +0 -0
- {pymodaq_utils-5.3.4 → pymodaq_utils-5.3.6}/src/pymodaq_utils/resources/__init__.py +0 -0
- {pymodaq_utils-5.3.4 → pymodaq_utils-5.3.6}/src/pymodaq_utils/resources/config_template.toml +0 -0
- {pymodaq_utils-5.3.4 → pymodaq_utils-5.3.6}/src/pymodaq_utils/serialize/__init__.py +0 -0
- {pymodaq_utils-5.3.4 → pymodaq_utils-5.3.6}/src/pymodaq_utils/serialize/factory.py +0 -0
- {pymodaq_utils-5.3.4 → pymodaq_utils-5.3.6}/src/pymodaq_utils/serialize/mysocket.py +0 -0
- {pymodaq_utils-5.3.4 → pymodaq_utils-5.3.6}/src/pymodaq_utils/serialize/serializer.py +0 -0
- {pymodaq_utils-5.3.4 → pymodaq_utils-5.3.6}/src/pymodaq_utils/serialize/serializer_legacy.py +0 -0
- {pymodaq_utils-5.3.4 → pymodaq_utils-5.3.6}/src/pymodaq_utils/serialize/utils.py +0 -0
- {pymodaq_utils-5.3.4 → pymodaq_utils-5.3.6}/src/pymodaq_utils/singleton.py +0 -0
- {pymodaq_utils-5.3.4 → pymodaq_utils-5.3.6}/src/pymodaq_utils/splash.png +0 -0
- {pymodaq_utils-5.3.4 → pymodaq_utils-5.3.6}/src/pymodaq_utils/units.py +0 -0
- {pymodaq_utils-5.3.4 → pymodaq_utils-5.3.6}/src/pymodaq_utils/utils.py +0 -0
- {pymodaq_utils-5.3.4 → pymodaq_utils-5.3.6}/src/pymodaq_utils/warnings.py +0 -0
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Moved to :mod:`pymodaq_utils.plugin_tools.checks`, this module is an alias kept for existing code.
|
|
2
|
+
|
|
3
|
+
.. versionadded:: 5.3.1
|
|
4
|
+
.. deprecated:: 5.3.5
|
|
5
|
+
Use ``pymodaq_utils.plugin_tools.checks``.
|
|
6
|
+
"""
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
from pymodaq_utils.plugin_tools import checks as _module
|
|
10
|
+
|
|
11
|
+
if __name__ == '__main__': # python -m pymodaq_utils.plugin_checks
|
|
12
|
+
raise SystemExit(_module.main())
|
|
13
|
+
|
|
14
|
+
sys.modules[__name__] = _module
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Moved to :mod:`pymodaq_utils.plugin_tools.rules`, this module is an alias kept for existing code.
|
|
2
|
+
|
|
3
|
+
.. versionadded:: 5.3.1
|
|
4
|
+
.. deprecated:: 5.3.5
|
|
5
|
+
Use ``pymodaq_utils.plugin_tools.rules``.
|
|
6
|
+
"""
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
from pymodaq_utils.plugin_tools import rules as _module
|
|
10
|
+
|
|
11
|
+
if __name__ == '__main__': # python -m pymodaq_utils.plugin_rules
|
|
12
|
+
raise SystemExit(_module.main())
|
|
13
|
+
|
|
14
|
+
sys.modules[__name__] = _module
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Moved to :mod:`pymodaq_utils.plugin_tools.scaffold`, this module is an alias kept for existing code.
|
|
2
|
+
|
|
3
|
+
.. versionadded:: 5.3.1
|
|
4
|
+
.. deprecated:: 5.3.5
|
|
5
|
+
Use ``pymodaq_utils.plugin_tools.scaffold``.
|
|
6
|
+
"""
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
from pymodaq_utils.plugin_tools import scaffold as _module
|
|
10
|
+
|
|
11
|
+
if __name__ == '__main__': # python -m pymodaq_utils.plugin_scaffold
|
|
12
|
+
raise SystemExit(_module.main())
|
|
13
|
+
|
|
14
|
+
sys.modules[__name__] = _module
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Tools to develop and maintain PyMoDAQ instrument plugins.
|
|
2
|
+
|
|
3
|
+
* :mod:`~pymodaq_utils.plugin_tools.contract`: the methods of a plugin (single definition)
|
|
4
|
+
* :mod:`~pymodaq_utils.plugin_tools.rules`: the static rules (``PMQxxx`` codes)
|
|
5
|
+
* :mod:`~pymodaq_utils.plugin_tools.checks`: the report on a plugin package (``check_plugin`` command)
|
|
6
|
+
* :mod:`~pymodaq_utils.plugin_tools.scaffold`: creation of new plugin modules
|
|
7
|
+
|
|
8
|
+
They read the source files of the plugins and do not import ``pymodaq``. The previous locations
|
|
9
|
+
(``pymodaq_utils.plugin_tools.checks``, ``plugin_rules`` and ``plugin_scaffold``) still work.
|
|
10
|
+
|
|
11
|
+
.. versionadded:: 5.3.5
|
|
12
|
+
"""
|
|
@@ -12,7 +12,7 @@ Every check is also exposed as a plain function returning a list of problems (em
|
|
|
12
12
|
|
|
13
13
|
To get a report without pytest, for any installed plugin package::
|
|
14
14
|
|
|
15
|
-
from pymodaq_utils.
|
|
15
|
+
from pymodaq_utils.plugin_tools.checks import check_plugin_package
|
|
16
16
|
print(check_plugin_package('pymodaq_plugins_mock'))
|
|
17
17
|
|
|
18
18
|
.. versionadded:: 5.3.1
|
|
@@ -33,19 +33,21 @@ import toml
|
|
|
33
33
|
|
|
34
34
|
from pymodaq_utils.utils import get_entrypoints
|
|
35
35
|
|
|
36
|
-
from pymodaq_utils.
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
from pymodaq_utils.plugin_tools.contract import legacy_names
|
|
37
|
+
from pymodaq_utils.plugin_tools import platforms
|
|
38
|
+
from pymodaq_utils.plugin_tools.rules import (MANDATORY_MOVE_METHODS, MANDATORY_VIEWER_METHODS, PLACEHOLDER_NAMES_RE,
|
|
39
|
+
Finding, Severity, check_file_names, check_leftovers,
|
|
40
|
+
check_os_classifiers, check_plugin_source, check_python_code,
|
|
41
|
+
check_pyproject, colorize, is_valid_unit, package_root, project_root,
|
|
42
|
+
use_color)
|
|
39
43
|
|
|
40
44
|
VIEWER_DIMS = ('0D', '1D', '2D', 'ND')
|
|
41
45
|
ENTRYPOINT_GROUPS = ('pymodaq.plugins', 'pymodaq.instruments')
|
|
42
46
|
OPTIONAL_ENTRYPOINT_GROUPS = ('pymodaq.extensions', 'pymodaq.models', 'pymodaq.pid_models',
|
|
43
47
|
'pymodaq.scanners', 'pymodaq.h5exporters')
|
|
44
48
|
|
|
45
|
-
# methods that have to be overridden by the plugin
|
|
46
|
-
# enforced as the bases are QObjects)
|
|
47
|
-
MANDATORY_MOVE_METHODS = ('ini_stage', 'get_actuator_value', 'close') # stop_motion has a default that does nothing
|
|
48
|
-
MANDATORY_VIEWER_METHODS = ('ini_detector', 'grab_data', 'stop', 'close')
|
|
49
|
+
# the methods that have to be overridden by the plugin are defined in pymodaq_utils.plugin_tools.contract (they are abstract
|
|
50
|
+
# in the base classes, but this is not enforced as the bases are QObjects)
|
|
49
51
|
|
|
50
52
|
# files generated by parser generators (pycparser/ply) that can end up within the plugin packages
|
|
51
53
|
IGNORED_MODULES = ('lextab', 'yacctab')
|
|
@@ -206,8 +208,10 @@ def _check_params(klass: type) -> list[str]:
|
|
|
206
208
|
return problems
|
|
207
209
|
|
|
208
210
|
|
|
209
|
-
def _check_methods(klass: type, base: type, mandatory) -> list[str]:
|
|
210
|
-
|
|
211
|
+
def _check_methods(klass: type, base: type, mandatory, legacy: Optional[dict] = None) -> list[str]:
|
|
212
|
+
legacy = legacy or {} # old names still called by the base class if the method is not defined (see PMQ312)
|
|
213
|
+
problems = [f'{klass.__name__} should override {m}()' for m in mandatory
|
|
214
|
+
if not _is_overridden(klass, base, m) and not (m in legacy and hasattr(klass, legacy[m]))]
|
|
211
215
|
if inspect.isabstract(klass): # in case a base ever becomes a real ABC
|
|
212
216
|
problems.append(f'{klass.__name__} is abstract: {sorted(klass.__abstractmethods__)}')
|
|
213
217
|
return problems
|
|
@@ -218,7 +222,7 @@ def check_move_class(klass: type) -> list[str]:
|
|
|
218
222
|
from pymodaq.control_modules.move_utility_classes import DAQ_Move_base # only needed when checking the classes
|
|
219
223
|
if not (inspect.isclass(klass) and issubclass(klass, DAQ_Move_base)):
|
|
220
224
|
return [f'{klass} should derive from DAQ_Move_base']
|
|
221
|
-
problems = _check_methods(klass, DAQ_Move_base, MANDATORY_MOVE_METHODS)
|
|
225
|
+
problems = _check_methods(klass, DAQ_Move_base, MANDATORY_MOVE_METHODS, legacy_names('move'))
|
|
222
226
|
problems += _check_params(klass)
|
|
223
227
|
|
|
224
228
|
axis_names = klass.get_class_axis_names()
|
|
@@ -331,7 +335,7 @@ class CheckResult:
|
|
|
331
335
|
|
|
332
336
|
``problems`` are defects of the plugin found on the imported classes. ``warnings`` mean the item could not be
|
|
333
337
|
fully checked, for instance because a third party module or a vendor SDK is not available in the current
|
|
334
|
-
environment. ``findings`` come from the static rules of :mod:`pymodaq_utils.
|
|
338
|
+
environment. ``findings`` come from the static rules of :mod:`pymodaq_utils.plugin_tools.rules`.
|
|
335
339
|
"""
|
|
336
340
|
item: str
|
|
337
341
|
problems: list[str] = field(default_factory=list)
|
|
@@ -462,6 +466,7 @@ def check_package_sources(package: str, project: Optional[Path] = None) -> Check
|
|
|
462
466
|
project = project or project_root(package) # None if installed from a wheel: no pyproject.toml to check
|
|
463
467
|
if project is not None:
|
|
464
468
|
result.findings.extend(check_pyproject(package, project))
|
|
469
|
+
result.findings.extend(check_os_classifiers(root, project))
|
|
465
470
|
result.findings.extend(check_file_names(root))
|
|
466
471
|
result.findings.extend(check_leftovers(package, root, project))
|
|
467
472
|
result.findings.extend(check_python_code(root))
|
|
@@ -607,7 +612,7 @@ def check_plugin_file(path: str | Path, fail_on: str = 'error', strict_imports:
|
|
|
607
612
|
|
|
608
613
|
def main(argv=None) -> int:
|
|
609
614
|
"""Print the report of a plugin package, for the developer: the ``check_plugin`` command (or
|
|
610
|
-
``python -m pymodaq_utils.
|
|
615
|
+
``python -m pymodaq_utils.plugin_tools.checks``)
|
|
611
616
|
|
|
612
617
|
Returns 0 if the checks pass, 1 otherwise (so that it can also be used in a script or a CI).
|
|
613
618
|
"""
|
|
@@ -623,6 +628,9 @@ def main(argv=None) -> int:
|
|
|
623
628
|
parser.add_argument('--strict-imports', action='store_true',
|
|
624
629
|
help='fail on the modules that cannot be imported (missing dependency or SDK)')
|
|
625
630
|
parser.add_argument('-v', '--verbose', action='store_true', help='list every todo')
|
|
631
|
+
parser.add_argument('--fix-classifiers', action='store_true',
|
|
632
|
+
help='write the Operating System classifiers of the pyproject.toml from the '
|
|
633
|
+
'supported_platforms declared by the instrument plugins')
|
|
626
634
|
parser.add_argument('--color', choices=['auto', 'always', 'never'], default='auto',
|
|
627
635
|
help='color the report: auto (default) if the output is a terminal and NO_COLOR is not set')
|
|
628
636
|
args = parser.parse_args(argv)
|
|
@@ -637,9 +645,26 @@ def main(argv=None) -> int:
|
|
|
637
645
|
package, project = resolve_target(args.package)
|
|
638
646
|
except ValueError as e:
|
|
639
647
|
parser.error(str(e))
|
|
648
|
+
if args.fix_classifiers:
|
|
649
|
+
root = package_root(package)
|
|
650
|
+
if root is None or project is None:
|
|
651
|
+
parser.error('--fix-classifiers needs the folder of the plugin repository (with its pyproject.toml)')
|
|
652
|
+
if 'classifiers' in toml.load(project / 'pyproject.toml').get('project', {}).get('dynamic', []):
|
|
653
|
+
print('pyproject.toml: the classifiers are computed when the package is built, nothing to write')
|
|
654
|
+
else:
|
|
655
|
+
try:
|
|
656
|
+
changed = platforms.update_pyproject_classifiers(project / 'pyproject.toml',
|
|
657
|
+
platforms.package_platforms(root))
|
|
658
|
+
except ValueError as e:
|
|
659
|
+
parser.error(str(e))
|
|
660
|
+
print('pyproject.toml: classifiers ' + ('updated' if changed else 'already up to date'))
|
|
640
661
|
report = check_plugin_package(package, args.fail_on, args.strict_imports, project,
|
|
641
662
|
check_entry_points=is_installed(package) or project is None)
|
|
642
663
|
print(report.format(verbose=args.verbose, color=use_color(args.color)))
|
|
664
|
+
root = package_root(package)
|
|
665
|
+
declared = platforms.package_platforms(root) if root is not None else None
|
|
666
|
+
if declared:
|
|
667
|
+
print(f'Works on: {platforms.describe(declared)}')
|
|
643
668
|
return 0 if report.ok else 1
|
|
644
669
|
|
|
645
670
|
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""The contract of an instrument plugin: the methods a plugin class has to (or should) override.
|
|
2
|
+
|
|
3
|
+
This is the single place where these lists are defined. The static rules (:mod:`pymodaq_utils.plugin_tools.rules`), the
|
|
4
|
+
checks (:mod:`pymodaq_utils.plugin_tools.checks`), the plugin tests and the documentation read them from here, so they
|
|
5
|
+
cannot disagree.
|
|
6
|
+
|
|
7
|
+
.. versionadded:: 5.3.5
|
|
8
|
+
"""
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Literal
|
|
13
|
+
|
|
14
|
+
Kind = Literal['move', 'viewer']
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass(frozen=True)
|
|
18
|
+
class PluginMethod:
|
|
19
|
+
"""One method of an actuator or detector plugin
|
|
20
|
+
|
|
21
|
+
Parameters
|
|
22
|
+
----------
|
|
23
|
+
name: str
|
|
24
|
+
name of the method in the plugin class
|
|
25
|
+
kind: 'move' or 'viewer'
|
|
26
|
+
actuator (``DAQ_Move_base``) or detector (``DAQ_Viewer_base``)
|
|
27
|
+
mandatory: bool
|
|
28
|
+
if True, a plugin that does not override it is wrong (error). Otherwise the base class has a default
|
|
29
|
+
that does nothing or raises ``NotImplementedError``: override it when the instrument can do it.
|
|
30
|
+
purpose: str
|
|
31
|
+
when it is called and what it has to do
|
|
32
|
+
returns: str
|
|
33
|
+
what it has to return ('' if nothing)
|
|
34
|
+
legacy_name: str
|
|
35
|
+
deprecated name that PyMoDAQ still calls if the method is not defined ('' if none): a plugin defining it
|
|
36
|
+
fulfils the contract (with a deprecation warning, see rule PMQ312)
|
|
37
|
+
"""
|
|
38
|
+
name: str
|
|
39
|
+
kind: Kind
|
|
40
|
+
mandatory: bool
|
|
41
|
+
purpose: str
|
|
42
|
+
returns: str = ''
|
|
43
|
+
legacy_name: str = ''
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
PLUGIN_METHODS: tuple[PluginMethod, ...] = (
|
|
47
|
+
PluginMethod('ini_attributes', 'move', False,
|
|
48
|
+
'called first, in the constructor: declare the attributes (type hints, ``self.controller = None``)'),
|
|
49
|
+
PluginMethod('ini_stage', 'move', True,
|
|
50
|
+
'initialize the communication when the actuator is initialized; if ``self.is_master`` create the '
|
|
51
|
+
'controller, otherwise use the ``controller`` argument (shared with the master)',
|
|
52
|
+
'``(info: str, initialized: bool)``'),
|
|
53
|
+
PluginMethod('get_actuator_value', 'move', True,
|
|
54
|
+
'read the current position from the hardware, on request and after each move',
|
|
55
|
+
'a ``DataActuator`` with units (or a float if ``data_actuator_type`` is ``float``)',
|
|
56
|
+
legacy_name='check_position'),
|
|
57
|
+
PluginMethod('move_abs', 'move', True,
|
|
58
|
+
'move to an absolute value (a ``DataActuator``, in the units of the axis)',
|
|
59
|
+
legacy_name='move_Abs'),
|
|
60
|
+
PluginMethod('move_rel', 'move', True,
|
|
61
|
+
'move by a relative value (a ``DataActuator``, in the units of the axis)',
|
|
62
|
+
legacy_name='move_Rel'),
|
|
63
|
+
PluginMethod('move_home', 'move', False, 'send the actuator to its home position'),
|
|
64
|
+
PluginMethod('stop_motion', 'move', False, 'stop the motion now; the default does nothing'),
|
|
65
|
+
PluginMethod('commit_settings', 'move', False,
|
|
66
|
+
'a setting of the tree changed in the UI: send it to the hardware'),
|
|
67
|
+
PluginMethod('close', 'move', True, 'release the hardware (safe state, close the communication)'),
|
|
68
|
+
PluginMethod('ini_attributes', 'viewer', False,
|
|
69
|
+
'called first, in the constructor: declare the attributes (type hints, ``self.controller = None``)'),
|
|
70
|
+
PluginMethod('ini_detector', 'viewer', True,
|
|
71
|
+
'initialize the communication when the detector is initialized; if ``self.is_master`` create the '
|
|
72
|
+
'controller, otherwise use the ``controller`` argument (shared with the master)',
|
|
73
|
+
'``(info: str, initialized: bool)``'),
|
|
74
|
+
PluginMethod('grab_data', 'viewer', True,
|
|
75
|
+
'start an acquisition (``Naverage`` averages); send the data with ``self.dte_signal.emit`` '
|
|
76
|
+
'(or ``dte_signal_temp``) as a ``DataToExport`` with axes and units'),
|
|
77
|
+
PluginMethod('stop', 'viewer', True, 'stop the acquisition in progress'),
|
|
78
|
+
PluginMethod('commit_settings', 'viewer', False,
|
|
79
|
+
'a setting of the tree changed in the UI: send it to the hardware'),
|
|
80
|
+
PluginMethod('close', 'viewer', True, 'release the hardware (safe state, close the communication)'),
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def methods_of(kind: Kind) -> tuple[PluginMethod, ...]:
|
|
85
|
+
"""All the methods of the contract of an actuator ('move') or of a detector ('viewer')"""
|
|
86
|
+
return tuple(m for m in PLUGIN_METHODS if m.kind == kind)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def mandatory_methods(kind: Kind) -> tuple[str, ...]:
|
|
90
|
+
"""Names of the methods that a plugin has to override"""
|
|
91
|
+
return tuple(m.name for m in methods_of(kind) if m.mandatory)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def legacy_names(kind: Kind) -> dict[str, str]:
|
|
95
|
+
"""Deprecated names still accepted in place of the mandatory methods: {method name: old name}"""
|
|
96
|
+
return {m.name: m.legacy_name for m in methods_of(kind) if m.mandatory and m.legacy_name}
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def recommended_methods(kind: Kind) -> tuple[str, ...]:
|
|
100
|
+
"""Names of the other methods of the contract: to override when the instrument can do it"""
|
|
101
|
+
return tuple(m.name for m in methods_of(kind) if not m.mandatory)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def contract_as_rst() -> str:
|
|
105
|
+
"""The contract as two reStructuredText tables, to be included in the documentation"""
|
|
106
|
+
out = []
|
|
107
|
+
for kind, title in (('move', 'Actuator (``DAQ_Move_base``)'), ('viewer', 'Detector (``DAQ_Viewer_base``)')):
|
|
108
|
+
out += [title, '-' * len(title), '',
|
|
109
|
+
'.. list-table::', ' :header-rows: 1', ' :widths: 20 12 48 20', '',
|
|
110
|
+
' * - Method', ' - Required', ' - When it is called, what it does', ' - Returns']
|
|
111
|
+
for m in methods_of(kind):
|
|
112
|
+
out += [f' * - ``{m.name}``', f' - {"yes" if m.mandatory else "no"}',
|
|
113
|
+
f' - {m.purpose}', f' - {m.returns}']
|
|
114
|
+
out.append('')
|
|
115
|
+
return '\n'.join(out)
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
"""Operating systems and processor architectures supported by an instrument plugin.
|
|
2
|
+
|
|
3
|
+
A plugin that only works on some systems (a Windows driver, a Raspberry Pi board...) declares it in the class of its
|
|
4
|
+
instrument plugin::
|
|
5
|
+
|
|
6
|
+
class DAQ_Move_Xxxx(DAQ_Move_base):
|
|
7
|
+
supported_platforms = ('win32',) # values of sys.platform: 'win32', 'linux', 'darwin'
|
|
8
|
+
supported_architectures = ('aarch64', 'armv7l') # optional, values of platform.machine()
|
|
9
|
+
|
|
10
|
+
``None`` (the default of the base classes) means all of them. PyMoDAQ then refuses to initialize the plugin on another
|
|
11
|
+
system, with a message, and ``check_plugin`` reads the declaration to check (and update with ``--fix-classifiers``)
|
|
12
|
+
the ``Operating System`` classifiers of the ``pyproject.toml``.
|
|
13
|
+
|
|
14
|
+
.. versionadded:: 5.3.5
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import ast
|
|
19
|
+
import platform
|
|
20
|
+
import re
|
|
21
|
+
import sys
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import Iterable, Optional
|
|
24
|
+
|
|
25
|
+
#: values of ``sys.platform`` and the matching name and PyPI classifier
|
|
26
|
+
PLATFORMS = {
|
|
27
|
+
'win32': ('Windows', 'Operating System :: Microsoft :: Windows'),
|
|
28
|
+
'linux': ('Linux', 'Operating System :: POSIX :: Linux'),
|
|
29
|
+
'darwin': ('macOS', 'Operating System :: MacOS'),
|
|
30
|
+
}
|
|
31
|
+
OS_INDEPENDENT = 'Operating System :: OS Independent'
|
|
32
|
+
|
|
33
|
+
#: values of ``platform.machine()`` (compared without case), with their aliases
|
|
34
|
+
ARCHITECTURES = {
|
|
35
|
+
'x86_64': ('x86_64', 'amd64'),
|
|
36
|
+
'i686': ('i686', 'i386', 'x86'),
|
|
37
|
+
'aarch64': ('aarch64', 'arm64'),
|
|
38
|
+
'armv7l': ('armv7l', 'armv8l'),
|
|
39
|
+
'armv6l': ('armv6l',),
|
|
40
|
+
}
|
|
41
|
+
_ARCH_ALIASES = {alias: name for name, aliases in ARCHITECTURES.items() for alias in aliases}
|
|
42
|
+
|
|
43
|
+
PLUGIN_CLASS_RE = re.compile(r'^DAQ_(Move|[0-2N]DViewer)_\w+$')
|
|
44
|
+
_OS_CLASSIFIER_RE = re.compile(r'^(?P<indent>\s*)(?P<quote>["\'])Operating System ::.*$')
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _as_tuple(value) -> Optional[tuple]:
|
|
48
|
+
if value is None:
|
|
49
|
+
return None
|
|
50
|
+
return (value,) if isinstance(value, str) else tuple(value)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def normalize_architecture(machine: str) -> str:
|
|
54
|
+
"""Canonical name of an architecture (``arm64`` -> ``aarch64``...), the lowercased input if unknown"""
|
|
55
|
+
return _ARCH_ALIASES.get(machine.lower(), machine.lower())
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def describe(platforms: Optional[Iterable[str]] = None, architectures: Optional[Iterable[str]] = None) -> str:
|
|
59
|
+
"""Text for the user, for instance ``Linux (aarch64, armv7l)``; empty if there is no restriction"""
|
|
60
|
+
platforms, architectures = _as_tuple(platforms), _as_tuple(architectures)
|
|
61
|
+
text = ' or '.join(PLATFORMS.get(p, (p,))[0] for p in platforms) if platforms else ''
|
|
62
|
+
if architectures:
|
|
63
|
+
arch = ', '.join(architectures)
|
|
64
|
+
text = f'{text} ({arch})' if text else f'the architectures {arch}'
|
|
65
|
+
return text
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def unsupported_reason(platforms: Optional[Iterable[str]] = None, architectures: Optional[Iterable[str]] = None,
|
|
69
|
+
system: Optional[str] = None, machine: Optional[str] = None) -> str:
|
|
70
|
+
"""Why a plugin cannot run on this computer, or an empty string if it can
|
|
71
|
+
|
|
72
|
+
Parameters
|
|
73
|
+
----------
|
|
74
|
+
platforms: iterable of str or None
|
|
75
|
+
values of ``sys.platform`` the plugin works on (``None``: all). A prefix is enough: ``linux`` matches
|
|
76
|
+
``linux2``
|
|
77
|
+
architectures: iterable of str or None
|
|
78
|
+
values of ``platform.machine()`` the plugin works on (``None``: all)
|
|
79
|
+
system, machine: str
|
|
80
|
+
replace ``sys.platform`` and ``platform.machine()``, for tests
|
|
81
|
+
"""
|
|
82
|
+
platforms, architectures = _as_tuple(platforms), _as_tuple(architectures)
|
|
83
|
+
system = sys.platform if system is None else system
|
|
84
|
+
machine = platform.machine() if machine is None else machine
|
|
85
|
+
ok_system = not platforms or any(system.startswith(p) for p in platforms)
|
|
86
|
+
ok_machine = not architectures or normalize_architecture(machine) in {normalize_architecture(a)
|
|
87
|
+
for a in architectures}
|
|
88
|
+
if ok_system and ok_machine:
|
|
89
|
+
return ''
|
|
90
|
+
return f'This plugin only works on {describe(platforms, architectures)} (this computer: {system}, {machine})'
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def classifiers_for(platforms: Optional[Iterable[str]] = None) -> list[str]:
|
|
94
|
+
"""The ``Operating System`` classifiers of a plugin working on these systems (``None``: OS independent)"""
|
|
95
|
+
platforms = _as_tuple(platforms)
|
|
96
|
+
if not platforms:
|
|
97
|
+
return [OS_INDEPENDENT]
|
|
98
|
+
return [PLATFORMS[p][1] for p in PLATFORMS if p in platforms]
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def invalid_values(platforms: Optional[Iterable[str]], architectures: Optional[Iterable[str]]) -> list[str]:
|
|
102
|
+
"""Problems of a declaration: unknown system or architecture names, empty lists"""
|
|
103
|
+
problems = []
|
|
104
|
+
platforms, architectures = _as_tuple(platforms), _as_tuple(architectures)
|
|
105
|
+
if platforms is not None:
|
|
106
|
+
if not platforms:
|
|
107
|
+
problems.append('supported_platforms is empty, use None for all the systems')
|
|
108
|
+
problems.extend(f"unknown system '{p}' in supported_platforms, use {list(PLATFORMS)}"
|
|
109
|
+
for p in platforms if p not in PLATFORMS)
|
|
110
|
+
if architectures is not None:
|
|
111
|
+
if not architectures:
|
|
112
|
+
problems.append('supported_architectures is empty, use None for all the architectures')
|
|
113
|
+
problems.extend(f"unknown architecture '{a}' in supported_architectures, use values of platform.machine() "
|
|
114
|
+
f"like {list(ARCHITECTURES)}" for a in architectures if normalize_architecture(a) not in
|
|
115
|
+
ARCHITECTURES)
|
|
116
|
+
return problems
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def declared_in_class(cls: ast.ClassDef) -> tuple[Optional[tuple], Optional[tuple]]:
|
|
120
|
+
"""``(supported_platforms, supported_architectures)`` written in the body of a plugin class (None if absent or
|
|
121
|
+
not a literal)"""
|
|
122
|
+
found = {}
|
|
123
|
+
for node in cls.body:
|
|
124
|
+
targets = []
|
|
125
|
+
if isinstance(node, ast.Assign):
|
|
126
|
+
targets, value = [t for t in node.targets if isinstance(t, ast.Name)], node.value
|
|
127
|
+
elif isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name) and node.value is not None:
|
|
128
|
+
targets, value = [node.target], node.value
|
|
129
|
+
for target in targets:
|
|
130
|
+
if target.id in ('supported_platforms', 'supported_architectures'):
|
|
131
|
+
try:
|
|
132
|
+
found[target.id] = _as_tuple(ast.literal_eval(value))
|
|
133
|
+
except (ValueError, SyntaxError):
|
|
134
|
+
pass
|
|
135
|
+
return found.get('supported_platforms'), found.get('supported_architectures')
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def package_platforms(root: Path) -> Optional[set]:
|
|
139
|
+
"""Systems on which the instruments of a plugin package work (union over the instrument modules), read from the
|
|
140
|
+
sources. ``None`` if at least one instrument works on all of them"""
|
|
141
|
+
systems: set = set()
|
|
142
|
+
files = sorted(set(root.rglob('daq_move_*.py')) | set(root.rglob('daq_*viewer_*.py')))
|
|
143
|
+
for file in files:
|
|
144
|
+
try:
|
|
145
|
+
tree = ast.parse(file.read_text(encoding='utf-8'))
|
|
146
|
+
except (SyntaxError, UnicodeDecodeError, OSError):
|
|
147
|
+
continue
|
|
148
|
+
for cls in (n for n in tree.body if isinstance(n, ast.ClassDef) and PLUGIN_CLASS_RE.match(n.name)):
|
|
149
|
+
declared, _ = declared_in_class(cls)
|
|
150
|
+
if not declared:
|
|
151
|
+
return None
|
|
152
|
+
systems.update(declared)
|
|
153
|
+
return systems or None
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def pyproject_os_classifiers(classifiers: Iterable[str]) -> list[str]:
|
|
157
|
+
return [c for c in classifiers if c.startswith('Operating System ::')]
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def update_pyproject_classifiers(pyproject: Path, platforms: Optional[Iterable[str]]) -> bool:
|
|
161
|
+
"""Replace the ``Operating System`` classifiers in the ``classifiers`` list of a ``pyproject.toml`` (a text edit,
|
|
162
|
+
the rest of the file is kept). Returns True if the file changed"""
|
|
163
|
+
wanted = classifiers_for(platforms)
|
|
164
|
+
lines = pyproject.read_text(encoding='utf-8').splitlines(keepends=True)
|
|
165
|
+
start = next((i for i, line in enumerate(lines) if re.match(r'^\s*classifiers\s*=\s*\[', line)), None)
|
|
166
|
+
if start is None:
|
|
167
|
+
raise ValueError('no classifiers = [...] list in the pyproject.toml (dynamic classifiers?)')
|
|
168
|
+
end = next((i for i in range(start, len(lines)) if lines[i].strip().startswith(']') or
|
|
169
|
+
(i == start and lines[i].rstrip().endswith(']'))), None)
|
|
170
|
+
if end is None or end == start:
|
|
171
|
+
raise ValueError('the classifiers list must be written on several lines')
|
|
172
|
+
indexes = [i for i in range(start + 1, end) if _OS_CLASSIFIER_RE.match(lines[i])]
|
|
173
|
+
sample = lines[indexes[0]] if indexes else lines[end - 1]
|
|
174
|
+
indent = re.match(r'\s*', sample).group(0) or ' '
|
|
175
|
+
quote = '"'
|
|
176
|
+
new = [f'{indent}{quote}{c}{quote},\n' for c in wanted]
|
|
177
|
+
if indexes:
|
|
178
|
+
first = indexes[0]
|
|
179
|
+
rebuilt = [line for i, line in enumerate(lines) if i not in indexes]
|
|
180
|
+
rebuilt[first:first] = new
|
|
181
|
+
else:
|
|
182
|
+
rebuilt = lines[:end] + new + lines[end:]
|
|
183
|
+
if lines[end - 1].rstrip().endswith('"') or lines[end - 1].rstrip().endswith("'"):
|
|
184
|
+
rebuilt[end - 1] = lines[end - 1].rstrip() + ',\n'
|
|
185
|
+
changed = rebuilt != lines
|
|
186
|
+
if changed:
|
|
187
|
+
pyproject.write_text(''.join(rebuilt), encoding='utf-8')
|
|
188
|
+
return changed
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
#: Python versions PyMoDAQ is developed for, they give the ``Programming Language :: Python`` classifiers
|
|
192
|
+
PYTHON_VERSIONS = ('3.10', '3.11', '3.12', '3.13')
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def python_classifiers(requires_python: str = '') -> list[str]:
|
|
196
|
+
"""``Programming Language :: Python :: 3.x`` for each of :data:`PYTHON_VERSIONS` accepted by ``requires-python``
|
|
197
|
+
(all of them if empty or invalid)"""
|
|
198
|
+
from packaging.specifiers import InvalidSpecifier, SpecifierSet
|
|
199
|
+
try:
|
|
200
|
+
spec = SpecifierSet(requires_python or '')
|
|
201
|
+
except InvalidSpecifier:
|
|
202
|
+
spec = SpecifierSet('')
|
|
203
|
+
return [f'Programming Language :: Python :: {v}' for v in PYTHON_VERSIONS if spec.contains(f'{v}.0')]
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def dynamic_classifiers(pyproject: dict, package_folder: Path) -> list[str]:
|
|
207
|
+
"""The classifiers of a plugin whose ``classifiers`` field is *dynamic* in the ``pyproject.toml``: those listed in
|
|
208
|
+
``[tool.pymodaq] classifiers`` (the ``Operating System`` and ``Programming Language :: Python :: 3.x`` ones are
|
|
209
|
+
ignored) followed by the Python versions accepted by ``requires-python`` and the systems computed from the
|
|
210
|
+
``supported_platforms`` of its instrument plugins. Used by the hatch metadata hook at build time"""
|
|
211
|
+
listed = pyproject.get('tool', {}).get('pymodaq', {}).get('classifiers', [])
|
|
212
|
+
kept = [c for c in listed if not c.startswith(('Operating System ::', 'Programming Language :: Python :: 3.'))]
|
|
213
|
+
return (kept + python_classifiers(pyproject.get('project', {}).get('requires-python', '')) +
|
|
214
|
+
classifiers_for(package_platforms(package_folder)))
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def platforms_as_rst() -> str:
|
|
218
|
+
"""The accepted values as two reStructuredText tables, to be included in the documentation"""
|
|
219
|
+
out = ['.. list-table::', ' :header-rows: 1', ' :widths: 20 20 60', '',
|
|
220
|
+
' * - ``supported_platforms``', ' - System', ' - Classifier']
|
|
221
|
+
for key, (label, classifier) in PLATFORMS.items():
|
|
222
|
+
out += [f' * - ``{key}``', f' - {label}', f' - ``{classifier}``']
|
|
223
|
+
out += ['', '.. list-table::', ' :header-rows: 1', ' :widths: 30 70', '',
|
|
224
|
+
' * - ``supported_architectures``', ' - Also accepted (same architecture)']
|
|
225
|
+
for key, aliases in ARCHITECTURES.items():
|
|
226
|
+
other = ', '.join(f'``{a}``' for a in aliases if a != key)
|
|
227
|
+
out += [f' * - ``{key}``', f' - {other}']
|
|
228
|
+
out.append('')
|
|
229
|
+
return '\n'.join(out)
|
|
@@ -10,9 +10,9 @@ Severities:
|
|
|
10
10
|
* ``warning``: deprecated or suspicious, should be fixed
|
|
11
11
|
* ``todo``: the plugin is not finished (leftover of the template: TODO comments, placeholders, ``NotImplementedError``)
|
|
12
12
|
|
|
13
|
-
They are used by :mod:`pymodaq_utils.
|
|
13
|
+
They are used by :mod:`pymodaq_utils.plugin_tools.checks` but can be called directly::
|
|
14
14
|
|
|
15
|
-
from pymodaq_utils.
|
|
15
|
+
from pymodaq_utils.plugin_tools.rules import check_package_sources
|
|
16
16
|
for finding in check_package_sources('pymodaq_plugins_xxxx'):
|
|
17
17
|
print(finding)
|
|
18
18
|
|
|
@@ -21,6 +21,7 @@ They are used by :mod:`pymodaq_utils.plugin_checks` but can be called directly::
|
|
|
21
21
|
from __future__ import annotations
|
|
22
22
|
|
|
23
23
|
import ast
|
|
24
|
+
import functools
|
|
24
25
|
import importlib.util
|
|
25
26
|
import os
|
|
26
27
|
import re
|
|
@@ -33,6 +34,9 @@ from typing import Optional
|
|
|
33
34
|
|
|
34
35
|
import toml
|
|
35
36
|
|
|
37
|
+
from pymodaq_utils.plugin_tools.contract import legacy_names, mandatory_methods
|
|
38
|
+
from pymodaq_utils.plugin_tools import platforms as _platforms
|
|
39
|
+
|
|
36
40
|
DOC = 'https://pymodaq.cnrs.fr/en/latest/developer_folder/instrument_plugins.html'
|
|
37
41
|
TEMPLATE_NAME = 'pymodaq_plugins_template'
|
|
38
42
|
|
|
@@ -43,8 +47,9 @@ PLACEHOLDER_AUTHORS = ('Name Surname', 'myname@test.fr')
|
|
|
43
47
|
PLACEHOLDER_NAMES_RE = re.compile(r'python_wrapper_file_of_your_instrument|PythonWrapperObjectOfYourInstrument|'
|
|
44
48
|
r'your_method_to_\w+|a_method_or_atttribute_to_check_if_init')
|
|
45
49
|
|
|
46
|
-
|
|
47
|
-
|
|
50
|
+
# defined once in pymodaq_utils.plugin_tools.contract
|
|
51
|
+
MANDATORY_MOVE_METHODS = mandatory_methods('move')
|
|
52
|
+
MANDATORY_VIEWER_METHODS = mandatory_methods('viewer')
|
|
48
53
|
|
|
49
54
|
# hooks of the plugins that were renamed: PyMoDAQ still calls the old ones, with a deprecation warning
|
|
50
55
|
DEPRECATED_HOOKS = {'check_position': 'get_actuator_value', 'move_Abs': 'move_abs', 'move_Rel': 'move_rel',
|
|
@@ -131,9 +136,17 @@ class Finding:
|
|
|
131
136
|
def location(self) -> str:
|
|
132
137
|
if self.path is None:
|
|
133
138
|
return ''
|
|
139
|
+
name = self._path_in_package()
|
|
134
140
|
if not self.line:
|
|
135
|
-
return
|
|
136
|
-
return f'{
|
|
141
|
+
return name
|
|
142
|
+
return f'{name}:{self.line}' + (f':{self.column}' if self.column else '')
|
|
143
|
+
|
|
144
|
+
def _path_in_package(self) -> str:
|
|
145
|
+
"""Path of the file from the folder of the plugin package (``hardware/driver.py``), its name if outside"""
|
|
146
|
+
for parent in self.path.parents:
|
|
147
|
+
if parent.name.startswith('pymodaq_plugins_'):
|
|
148
|
+
return self.path.relative_to(parent).as_posix()
|
|
149
|
+
return self.path.name
|
|
137
150
|
|
|
138
151
|
def format(self, color: bool = False) -> str:
|
|
139
152
|
"""Text of the finding, with colors for a terminal if ``color``"""
|
|
@@ -224,9 +237,11 @@ def check_python_code(root: Path) -> list[Finding]:
|
|
|
224
237
|
* PMQ315: invalid escape sequence in a string (``"C:\\Program Files"``): a warning of python since 3.6, a
|
|
225
238
|
``SyntaxWarning`` since 3.12, that will be an error
|
|
226
239
|
* PMQ317: code that only works on Windows and is executed when the module is imported (``windll``, ``winreg``,
|
|
227
|
-
path with a drive letter): the plugin cannot be imported, nor listed, on the other systems
|
|
240
|
+
path with a drive letter): the plugin cannot be imported, nor listed, on the other systems. Not reported when
|
|
241
|
+
all the instruments of the package declare ``supported_platforms = ('win32',)``
|
|
228
242
|
"""
|
|
229
243
|
findings = []
|
|
244
|
+
windows_only_package = _platforms.package_platforms(root) == {'win32'}
|
|
230
245
|
for path in _python_files(root):
|
|
231
246
|
source = path.read_text(errors='replace')
|
|
232
247
|
with warnings.catch_warnings(record=True) as caught:
|
|
@@ -255,7 +270,7 @@ def check_python_code(root: Path) -> list[Finding]:
|
|
|
255
270
|
what = f'the path {node.value!r}'
|
|
256
271
|
if what is not None and not _guarded(node, parents):
|
|
257
272
|
windows.append((node.lineno, what))
|
|
258
|
-
if windows:
|
|
273
|
+
if windows and not windows_only_package:
|
|
259
274
|
line, what = windows[0]
|
|
260
275
|
more = f' (and {len(windows) - 1} more in this file)' if len(windows) > 1 else ''
|
|
261
276
|
findings.append(Finding('PMQ317', Severity.WARNING, f'only works on Windows: {what}{more}',
|
|
@@ -400,6 +415,31 @@ def check_leftovers(package: str, root: Path, project: Optional[Path] = None) ->
|
|
|
400
415
|
return findings
|
|
401
416
|
|
|
402
417
|
|
|
418
|
+
NOQA_RE = re.compile(r'#\s*noqa\s*:?\s*(PMQ\d+(?:\s*,\s*PMQ\d+)*)', re.IGNORECASE)
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def _honour_noqa(rule):
|
|
422
|
+
"""Drop the findings of a rule whose line carries a ``# noqa: PMQxxx`` comment (on purpose, like for flake8)"""
|
|
423
|
+
@functools.wraps(rule)
|
|
424
|
+
def wrapper(*args, **kwargs):
|
|
425
|
+
lines = {}
|
|
426
|
+
kept = []
|
|
427
|
+
for finding in rule(*args, **kwargs):
|
|
428
|
+
if finding.path is not None and finding.line is not None:
|
|
429
|
+
if finding.path not in lines:
|
|
430
|
+
try:
|
|
431
|
+
lines[finding.path] = Path(finding.path).read_text(errors='replace').splitlines()
|
|
432
|
+
except OSError:
|
|
433
|
+
lines[finding.path] = []
|
|
434
|
+
text = lines[finding.path]
|
|
435
|
+
match = NOQA_RE.search(text[finding.line - 1]) if 0 < finding.line <= len(text) else None
|
|
436
|
+
if match and finding.code in {c.strip().upper() for c in match.group(1).split(',')}:
|
|
437
|
+
continue
|
|
438
|
+
kept.append(finding)
|
|
439
|
+
return kept
|
|
440
|
+
return wrapper
|
|
441
|
+
|
|
442
|
+
|
|
403
443
|
def _is_name(node: Optional[ast.AST], name: str) -> bool:
|
|
404
444
|
if isinstance(node, ast.Call):
|
|
405
445
|
node = node.func
|
|
@@ -482,11 +522,19 @@ def check_plugin_source(path: Path, kind: str, class_name: str, static_fallback:
|
|
|
482
522
|
else:
|
|
483
523
|
mandatory = MANDATORY_VIEWER_METHODS
|
|
484
524
|
findings.extend(_check_legacy_settings(path, tree))
|
|
525
|
+
findings.extend(_check_controller_init(path, tree))
|
|
526
|
+
for problem in _platforms.invalid_values(*_platforms.declared_in_class(cls)):
|
|
527
|
+
findings.append(Finding('PMQ319', Severity.WARNING, problem,
|
|
528
|
+
'supported_platforms: values of sys.platform (win32, linux, darwin); '
|
|
529
|
+
'supported_architectures: values of platform.machine() (x86_64, aarch64, armv7l...)',
|
|
530
|
+
path, cls.lineno))
|
|
485
531
|
simple_bases = all(b in ('DAQ_Move_base', 'DAQ_Viewer_base') for b in _base_names(cls))
|
|
486
532
|
if static_fallback and simple_bases:
|
|
487
533
|
for meth in mandatory:
|
|
488
|
-
# the base class still calls check_position if
|
|
489
|
-
|
|
534
|
+
# the base class still calls the old names (check_position, move_Abs...) if the method is not defined
|
|
535
|
+
# (see PMQ312)
|
|
536
|
+
old = legacy_names(kind).get(meth) if kind == 'move' else None
|
|
537
|
+
if meth not in methods and not (old and old in methods):
|
|
490
538
|
findings.append(Finding('PMQ306', Severity.ERROR, f'{class_name} should define {meth}()',
|
|
491
539
|
'implement it (see the template)', path, cls.lineno))
|
|
492
540
|
init = methods.get('ini_stage' if kind == 'move' else 'ini_detector')
|
|
@@ -512,7 +560,8 @@ def _check_move_source(path: Path, cls: ast.ClassDef, attrs: dict, static_fallba
|
|
|
512
560
|
findings.append(Finding('PMQ302', Severity.ERROR, "the axes are declared as 'axis_names', that is ignored",
|
|
513
561
|
"use '_axis_names'", path, attrs['axis_names'].lineno))
|
|
514
562
|
dat = attrs.get('data_actuator_type')
|
|
515
|
-
|
|
563
|
+
inherits = dat is None and not all(b == 'DAQ_Move_base' for b in _base_names(cls)) # may be set by a parent
|
|
564
|
+
if not inherits and (dat is None or 'DataActuator' not in ast.dump(dat)):
|
|
516
565
|
findings.append(Finding('PMQ303', Severity.WARNING, 'data_actuator_type is not DataActuatorType.DataActuator',
|
|
517
566
|
'new plugins should exchange DataActuator objects: set '
|
|
518
567
|
'data_actuator_type = DataActuatorType.DataActuator', path,
|
|
@@ -572,14 +621,28 @@ def _check_legacy_settings(path: Path, tree: ast.Module) -> list[Finding]:
|
|
|
572
621
|
more = f' ({len(hits)} times in this file)' if len(hits) > 1 else ''
|
|
573
622
|
return [Finding('PMQ316', Severity.WARNING, f"settings group '{first.value}' was renamed "
|
|
574
623
|
f"'{LEGACY_SETTINGS[first.value]}'{more}",
|
|
575
|
-
"PyMoDAQ
|
|
576
|
-
"new ones", path,
|
|
624
|
+
"PyMoDAQ still translates the old names in settings[...] (deprecated), but not in "
|
|
625
|
+
"settings.child(...): use the new ones", path,
|
|
577
626
|
first.lineno)]
|
|
578
627
|
|
|
579
628
|
|
|
629
|
+
def _check_controller_init(path: Path, tree: ast.Module) -> list[Finding]:
|
|
630
|
+
"""PMQ318: deprecated call of ini_stage_init / ini_detector_init to initialize the controller"""
|
|
631
|
+
calls = [n for n in ast.walk(tree) if isinstance(n, ast.Call) and isinstance(n.func, ast.Attribute) and
|
|
632
|
+
n.func.attr in ('ini_stage_init', 'ini_detector_init') and
|
|
633
|
+
isinstance(n.func.value, ast.Name) and n.func.value.id == 'self']
|
|
634
|
+
if not calls:
|
|
635
|
+
return []
|
|
636
|
+
first = min(calls, key=lambda n: n.lineno)
|
|
637
|
+
more = f' ({len(calls)} times in this file)' if len(calls) > 1 else ''
|
|
638
|
+
return [Finding('PMQ318', Severity.WARNING, f'{first.func.attr}() is deprecated{more}',
|
|
639
|
+
"write: if self.is_master: self.controller = AWrapper() else: self.controller = controller",
|
|
640
|
+
path, first.lineno)]
|
|
641
|
+
|
|
642
|
+
|
|
580
643
|
def check_package_sources(package: str) -> list[Finding]:
|
|
581
644
|
"""All the static rules on an installed (or editable) plugin package: packaging, leftovers and plugin classes"""
|
|
582
|
-
from pymodaq_utils.
|
|
645
|
+
from pymodaq_utils.plugin_tools.checks import find_plugin_modules # no import cycle at module level
|
|
583
646
|
|
|
584
647
|
root = package_root(package)
|
|
585
648
|
if root is None:
|
|
@@ -593,3 +656,31 @@ def check_package_sources(package: str) -> list[Finding]:
|
|
|
593
656
|
file = Path(importlib.util.find_spec(mod.import_path).origin)
|
|
594
657
|
findings.extend(check_plugin_source(file, mod.kind, mod.class_name))
|
|
595
658
|
return findings
|
|
659
|
+
|
|
660
|
+
|
|
661
|
+
def check_os_classifiers(package_folder: Path, project: Path) -> list[Finding]:
|
|
662
|
+
"""PMQ320: the ``Operating System`` classifiers of the pyproject.toml do not match the ``supported_platforms``
|
|
663
|
+
declared by the instrument plugins"""
|
|
664
|
+
path = project / 'pyproject.toml'
|
|
665
|
+
try:
|
|
666
|
+
proj = toml.load(path).get('project', {})
|
|
667
|
+
except toml.TomlDecodeError:
|
|
668
|
+
return []
|
|
669
|
+
if 'classifiers' in proj.get('dynamic', []):
|
|
670
|
+
return []
|
|
671
|
+
declared = _platforms.package_platforms(package_folder)
|
|
672
|
+
expected = set(_platforms.classifiers_for(declared))
|
|
673
|
+
actual = set(_platforms.pyproject_os_classifiers(proj.get('classifiers', [])))
|
|
674
|
+
if actual == expected:
|
|
675
|
+
return []
|
|
676
|
+
what = (f"the plugins work on {_platforms.describe(declared)}" if declared else
|
|
677
|
+
'no instrument plugin declares supported_platforms (they work on all the systems)')
|
|
678
|
+
line = next((i for i, text in enumerate(path.read_text().splitlines(), 1) if 'classifiers' in text), None)
|
|
679
|
+
return [Finding('PMQ320', Severity.WARNING,
|
|
680
|
+
f'the Operating System classifiers {sorted(actual) or "(none)"} do not match the code: {what}',
|
|
681
|
+
'run check_plugin --fix-classifiers to write ' + ', '.join(sorted(expected)), path, line)]
|
|
682
|
+
|
|
683
|
+
|
|
684
|
+
check_plugin_source = _honour_noqa(check_plugin_source)
|
|
685
|
+
check_python_code = _honour_noqa(check_python_code)
|
|
686
|
+
check_leftovers = _honour_noqa(check_leftovers)
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
"""Helper to add a new, correctly named, instrument to a PyMoDAQ plugin package (``pymodaq_plugins_*``).
|
|
2
2
|
|
|
3
|
-
It is a development helper, independent of the plugin checks (see :mod:`pymodaq_utils.
|
|
3
|
+
It is a development helper, independent of the plugin checks (see :mod:`pymodaq_utils.plugin_tools.checks`). The generated
|
|
4
4
|
module follows the naming convention, derives from the right base class, declares the mandatory attributes and
|
|
5
5
|
methods, and holds ``TODO`` comments (and ``NotImplementedError``) where the instrument specific code goes::
|
|
6
6
|
|
|
7
|
-
python -m pymodaq_utils.
|
|
8
|
-
python -m pymodaq_utils.
|
|
7
|
+
python -m pymodaq_utils.plugin_tools.scaffold move Xxxx # actuator: daq_move_Xxxx.py, class DAQ_Move_Xxxx
|
|
8
|
+
python -m pymodaq_utils.plugin_tools.scaffold 1D Xxxx # 1D detector: daq_1Dviewer_Xxxx.py, class DAQ_1DViewer_Xxxx
|
|
9
9
|
|
|
10
10
|
from the root of the plugin repository (or use ``--folder`` to give the package folder). The same is available from
|
|
11
11
|
python with :func:`create_instrument`.
|
{pymodaq_utils-5.3.4 → pymodaq_utils-5.3.6}/src/pymodaq_utils/resources/hatch_build_plugins.py
RENAMED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from pathlib import Path
|
|
2
2
|
import toml
|
|
3
3
|
|
|
4
|
+
from pymodaq_utils.plugin_tools.platforms import dynamic_classifiers
|
|
4
5
|
from pymodaq_utils.logger import set_logger, get_module_name
|
|
5
6
|
|
|
6
7
|
logger = set_logger(get_module_name(__file__))
|
|
@@ -43,3 +44,8 @@ def update_metadata_from_toml(metadata: dict, here: Path) -> None:
|
|
|
43
44
|
metadata['entry-points'] = entrypoints
|
|
44
45
|
|
|
45
46
|
logger.debug(f'created entry-points: {entrypoints}')
|
|
47
|
+
|
|
48
|
+
if 'classifiers' in src_dict.get('project', {}).get('dynamic', []):
|
|
49
|
+
folder = here / 'src' / PLUGIN_NAME
|
|
50
|
+
metadata['classifiers'] = dynamic_classifiers(src_dict, folder if folder.is_dir() else here / PLUGIN_NAME)
|
|
51
|
+
logger.debug(f"created classifiers: {metadata['classifiers']}")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pymodaq_utils-5.3.4 → pymodaq_utils-5.3.6}/src/pymodaq_utils/resources/config_template.toml
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pymodaq_utils-5.3.4 → pymodaq_utils-5.3.6}/src/pymodaq_utils/serialize/serializer_legacy.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|