pymodaq_plugins_utils 5.0.4__tar.gz → 5.0.5__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_plugins_utils-5.0.4 → pymodaq_plugins_utils-5.0.5}/.github/workflows/Testbase.yml +2 -2
- {pymodaq_plugins_utils-5.0.4 → pymodaq_plugins_utils-5.0.5}/.github/workflows/compatibility.yml +3 -3
- {pymodaq_plugins_utils-5.0.4 → pymodaq_plugins_utils-5.0.5}/.github/workflows/python-publish.yml +2 -2
- {pymodaq_plugins_utils-5.0.4 → pymodaq_plugins_utils-5.0.5}/.github/workflows/updater.yml +1 -1
- {pymodaq_plugins_utils-5.0.4 → pymodaq_plugins_utils-5.0.5}/PKG-INFO +2 -1
- {pymodaq_plugins_utils-5.0.4 → pymodaq_plugins_utils-5.0.5}/pyproject.toml +1 -0
- pymodaq_plugins_utils-5.0.5/src/pymodaq_plugins_utils/hardware/__init__.py +33 -0
- pymodaq_plugins_utils-5.0.5/src/pymodaq_plugins_utils/hardware/base.py +59 -0
- pymodaq_plugins_utils-5.0.5/src/pymodaq_plugins_utils/hardware/serial_ports.py +63 -0
- pymodaq_plugins_utils-5.0.5/src/pymodaq_plugins_utils/hardware/visa.py +84 -0
- pymodaq_plugins_utils-5.0.5/tests/hardware_test.py +249 -0
- pymodaq_plugins_utils-5.0.4/src/pymodaq_plugins_utils/resources/__init__.py +0 -0
- {pymodaq_plugins_utils-5.0.4 → pymodaq_plugins_utils-5.0.5}/.gitattributes +0 -0
- {pymodaq_plugins_utils-5.0.4 → pymodaq_plugins_utils-5.0.5}/.github/workflows/Test.yml +0 -0
- {pymodaq_plugins_utils-5.0.4 → pymodaq_plugins_utils-5.0.5}/.gitignore +0 -0
- {pymodaq_plugins_utils-5.0.4 → pymodaq_plugins_utils-5.0.5}/LICENSE +0 -0
- {pymodaq_plugins_utils-5.0.4 → pymodaq_plugins_utils-5.0.5}/README.rst +0 -0
- {pymodaq_plugins_utils-5.0.4 → pymodaq_plugins_utils-5.0.5}/hatch_build.py +0 -0
- {pymodaq_plugins_utils-5.0.4 → pymodaq_plugins_utils-5.0.5}/icon.ico +0 -0
- {pymodaq_plugins_utils-5.0.4 → pymodaq_plugins_utils-5.0.5}/src/pymodaq_plugins_utils/__init__.py +0 -0
- {pymodaq_plugins_utils-5.0.4 → pymodaq_plugins_utils-5.0.5}/src/pymodaq_plugins_utils/app/__init__.py +0 -0
- {pymodaq_plugins_utils-5.0.4 → pymodaq_plugins_utils-5.0.5}/src/pymodaq_plugins_utils/exporters/__init__.py +0 -0
- {pymodaq_plugins_utils-5.0.4 → pymodaq_plugins_utils-5.0.5}/src/pymodaq_plugins_utils/extensions/__init__.py +0 -0
- {pymodaq_plugins_utils-5.0.4 → pymodaq_plugins_utils-5.0.5}/src/pymodaq_plugins_utils/extensions/custom_extension_template.py +0 -0
- {pymodaq_plugins_utils-5.0.4 → pymodaq_plugins_utils-5.0.5}/src/pymodaq_plugins_utils/hardware/camera_base_pylablib.py +0 -0
- {pymodaq_plugins_utils-5.0.4 → pymodaq_plugins_utils-5.0.5}/src/pymodaq_plugins_utils/models/__init__.py +0 -0
- {pymodaq_plugins_utils-5.0.4/src/pymodaq_plugins_utils/hardware → pymodaq_plugins_utils-5.0.5/src/pymodaq_plugins_utils/resources}/__init__.py +0 -0
- {pymodaq_plugins_utils-5.0.4 → pymodaq_plugins_utils-5.0.5}/src/pymodaq_plugins_utils/resources/config_template.toml +0 -0
- {pymodaq_plugins_utils-5.0.4 → pymodaq_plugins_utils-5.0.5}/src/pymodaq_plugins_utils/scanners/__init__.py +0 -0
- {pymodaq_plugins_utils-5.0.4 → pymodaq_plugins_utils-5.0.5}/src/pymodaq_plugins_utils/utils.py +0 -0
- {pymodaq_plugins_utils-5.0.4 → pymodaq_plugins_utils-5.0.5}/tests/test_plugin_package_structure.py +0 -0
|
@@ -18,9 +18,9 @@ jobs:
|
|
|
18
18
|
QT_DEBUG_PLUGINS: 1
|
|
19
19
|
steps:
|
|
20
20
|
- name: Set up Python ${{ inputs.python }}
|
|
21
|
-
uses: actions/checkout@
|
|
21
|
+
uses: actions/checkout@v6.0.3
|
|
22
22
|
- name: Install dependencies
|
|
23
|
-
uses: actions/setup-python@v6.
|
|
23
|
+
uses: actions/setup-python@v6.2.0
|
|
24
24
|
with:
|
|
25
25
|
python-version: ${{ inputs.python }}
|
|
26
26
|
- name: Install package
|
{pymodaq_plugins_utils-5.0.4 → pymodaq_plugins_utils-5.0.5}/.github/workflows/compatibility.yml
RENAMED
|
@@ -36,10 +36,10 @@ jobs:
|
|
|
36
36
|
echo "plugin_name=$(echo '${{ github.repository }}' | cut -d'/' -f2)" >> $GITHUB_ENV
|
|
37
37
|
|
|
38
38
|
- name: Checkout the repo
|
|
39
|
-
uses: actions/checkout@
|
|
39
|
+
uses: actions/checkout@v6.0.3
|
|
40
40
|
|
|
41
41
|
- name: Set up Python ${{ matrix.python-version }}
|
|
42
|
-
uses: actions/setup-python@v6.
|
|
42
|
+
uses: actions/setup-python@v6.2.0
|
|
43
43
|
with:
|
|
44
44
|
python-version: ${{ matrix.python-version }}
|
|
45
45
|
|
|
@@ -69,7 +69,7 @@ jobs:
|
|
|
69
69
|
|
|
70
70
|
- name: Upload compatibility report
|
|
71
71
|
if: failure()
|
|
72
|
-
uses: actions/upload-artifact@
|
|
72
|
+
uses: actions/upload-artifact@v7.0.1
|
|
73
73
|
with:
|
|
74
74
|
name:
|
|
75
75
|
path: 'import_report_tests_${{ env.plugin_name }}_None.txt'
|
{pymodaq_plugins_utils-5.0.4 → pymodaq_plugins_utils-5.0.5}/.github/workflows/python-publish.yml
RENAMED
|
@@ -13,9 +13,9 @@ jobs:
|
|
|
13
13
|
runs-on: ubuntu-latest
|
|
14
14
|
|
|
15
15
|
steps:
|
|
16
|
-
- uses: actions/checkout@
|
|
16
|
+
- uses: actions/checkout@v6.0.3
|
|
17
17
|
- name: Set up Python
|
|
18
|
-
uses: actions/setup-python@v6.
|
|
18
|
+
uses: actions/setup-python@v6.2.0
|
|
19
19
|
with:
|
|
20
20
|
python-version: '3.13'
|
|
21
21
|
- name: Install dependencies
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pymodaq_plugins_utils
|
|
3
|
-
Version: 5.0.
|
|
3
|
+
Version: 5.0.5
|
|
4
4
|
Summary: Set of utility methods and classes to interact with instruments
|
|
5
5
|
Project-URL: Homepage, https://pymodaq.cnrs.fr
|
|
6
6
|
Project-URL: Documentation , https://pymodaq.cnrs.fr
|
|
@@ -45,6 +45,7 @@ Classifier: Topic :: Software Development :: User Interfaces
|
|
|
45
45
|
Requires-Python: >=3.8
|
|
46
46
|
Requires-Dist: pymodaq>=5.0.0
|
|
47
47
|
Provides-Extra: serial
|
|
48
|
+
Requires-Dist: pyserial; extra == 'serial'
|
|
48
49
|
Requires-Dist: pyvisa; extra == 'serial'
|
|
49
50
|
Description-Content-Type: text/x-rst
|
|
50
51
|
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Hardware discovery caches for PyMoDAQ plugins.
|
|
2
|
+
|
|
3
|
+
Each backend (:mod:`~pymodaq_utils.hardware.visa`,
|
|
4
|
+
:mod:`~pymodaq_utils.hardware.serial_ports`) queries the OS exactly once per
|
|
5
|
+
process. Subsequent calls reuse the cached result, so plugin startup cost is
|
|
6
|
+
paid at most once regardless of how many plugins share the same backend.
|
|
7
|
+
|
|
8
|
+
Quick reference::
|
|
9
|
+
|
|
10
|
+
# VISA-based plugin (Newport, Thorlabs, PI, ...)
|
|
11
|
+
from pymodaq_utils.hardware.visa import list_serial_resources
|
|
12
|
+
ports = list_serial_resources()
|
|
13
|
+
|
|
14
|
+
# pyserial-based plugin (Arduino, Ocean Optics, ...)
|
|
15
|
+
from pymodaq_utils.hardware.serial_ports import list_resources
|
|
16
|
+
ports = list_resources()
|
|
17
|
+
|
|
18
|
+
# After hot-plugging a device
|
|
19
|
+
from pymodaq_utils.hardware import invalidate_all_caches
|
|
20
|
+
invalidate_all_caches()
|
|
21
|
+
"""
|
|
22
|
+
from .visa import invalidate_cache as _invalidate_visa
|
|
23
|
+
from .serial_ports import invalidate_cache as _invalidate_serial
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def invalidate_all_caches() -> None:
|
|
27
|
+
"""Clear both the VISA and serial discovery caches.
|
|
28
|
+
|
|
29
|
+
Call this after hot-plugging a device so the next call to any
|
|
30
|
+
``list_*`` function re-discovers the current set of instruments.
|
|
31
|
+
"""
|
|
32
|
+
_invalidate_visa()
|
|
33
|
+
_invalidate_serial()
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
|
|
2
|
+
class HardwareCache:
|
|
3
|
+
"""Base class for process-lifetime hardware discovery caches.
|
|
4
|
+
|
|
5
|
+
Each subclass calls its backend (pyvisa, pyserial, …) exactly once per
|
|
6
|
+
process. The result is stored as a class variable and reused by every
|
|
7
|
+
caller, regardless of which plugin package triggered the first call.
|
|
8
|
+
|
|
9
|
+
Subclasses must override :meth:`_fetch` and :meth:`list_resources`.
|
|
10
|
+
Call :meth:`invalidate_cache` to force re-discovery, for example after
|
|
11
|
+
hot-plugging a device.
|
|
12
|
+
|
|
13
|
+
Example — defining a new backend::
|
|
14
|
+
|
|
15
|
+
class MyCache(HardwareCache):
|
|
16
|
+
_cache = None
|
|
17
|
+
|
|
18
|
+
@classmethod
|
|
19
|
+
def _fetch(cls):
|
|
20
|
+
return some_expensive_os_call()
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def list_resources(cls) -> list[str]:
|
|
24
|
+
return [item.id for item in cls._get_cache()]
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
_cache = None
|
|
28
|
+
|
|
29
|
+
@classmethod
|
|
30
|
+
def _fetch(cls):
|
|
31
|
+
"""Perform the actual hardware discovery.
|
|
32
|
+
|
|
33
|
+
Called at most once per process. Must return a value that can be
|
|
34
|
+
stored and reused (list, dict, …). Should catch all exceptions and
|
|
35
|
+
return an empty container so that callers never need to guard against
|
|
36
|
+
missing backends.
|
|
37
|
+
"""
|
|
38
|
+
raise NotImplementedError
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def _get_cache(cls):
|
|
42
|
+
"""Return the cached discovery result, populating it on first call."""
|
|
43
|
+
if cls._cache is None:
|
|
44
|
+
cls._cache = cls._fetch()
|
|
45
|
+
return cls._cache
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def invalidate_cache(cls) -> None:
|
|
49
|
+
"""Clear the cache so the next call to any list_* method re-discovers.
|
|
50
|
+
|
|
51
|
+
Use this after hot-plugging a device or when the set of available
|
|
52
|
+
instruments may have changed since process startup.
|
|
53
|
+
"""
|
|
54
|
+
cls._cache = None
|
|
55
|
+
|
|
56
|
+
@classmethod
|
|
57
|
+
def list_resources(cls) -> list[str]:
|
|
58
|
+
"""Return a list of connectable resource strings for this backend."""
|
|
59
|
+
raise NotImplementedError
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""pyserial hardware discovery cache.
|
|
2
|
+
|
|
3
|
+
Wraps :mod:`serial.tools.list_ports` to enumerate available serial ports
|
|
4
|
+
exactly once per process. ``pyserial`` is an optional dependency: if it is
|
|
5
|
+
not installed, all functions return empty lists and a warning is logged.
|
|
6
|
+
|
|
7
|
+
Typical usage in a plugin::
|
|
8
|
+
|
|
9
|
+
from pymodaq_utils.hardware.serial_ports import list_resources
|
|
10
|
+
|
|
11
|
+
ports = list_resources() # e.g. ['/dev/ttyUSB0', 'COM3']
|
|
12
|
+
|
|
13
|
+
After hot-plugging a device, refresh the cache with::
|
|
14
|
+
|
|
15
|
+
from pymodaq_utils.hardware.serial_ports import invalidate_cache
|
|
16
|
+
invalidate_cache()
|
|
17
|
+
"""
|
|
18
|
+
import pymodaq_utils.logger as logger_module
|
|
19
|
+
from .base import HardwareCache
|
|
20
|
+
|
|
21
|
+
logger = logger_module.set_logger(logger_module.get_module_name(__file__))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class SerialPortsCache(HardwareCache):
|
|
25
|
+
_cache = None
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def _fetch(cls) -> list:
|
|
29
|
+
try:
|
|
30
|
+
from serial.tools.list_ports import comports
|
|
31
|
+
return list(comports())
|
|
32
|
+
except ImportError:
|
|
33
|
+
logger.warning('pyserial is not installed — serial port discovery unavailable. '
|
|
34
|
+
'Install it with: pip install pyserial')
|
|
35
|
+
return []
|
|
36
|
+
except Exception as e:
|
|
37
|
+
logger.warning(f'Serial port discovery failed: {e}')
|
|
38
|
+
return []
|
|
39
|
+
|
|
40
|
+
@classmethod
|
|
41
|
+
def list_resources(cls) -> list[str]:
|
|
42
|
+
"""Serial port device strings (e.g. '/dev/ttyUSB0', 'COM3')."""
|
|
43
|
+
return [p.device for p in cls._get_cache()]
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def list_port_descriptions(cls) -> list[str]:
|
|
47
|
+
"""Human-readable descriptions for each serial port."""
|
|
48
|
+
return [p.description for p in cls._get_cache()]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def list_resources() -> list[str]:
|
|
52
|
+
"""Serial port device strings (e.g. '/dev/ttyUSB0', 'COM3')."""
|
|
53
|
+
return SerialPortsCache.list_resources()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def list_port_descriptions() -> list[str]:
|
|
57
|
+
"""Human-readable descriptions for each serial port."""
|
|
58
|
+
return SerialPortsCache.list_port_descriptions()
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def invalidate_cache() -> None:
|
|
62
|
+
"""Clear the serial port cache so the next call re-discovers."""
|
|
63
|
+
SerialPortsCache.invalidate_cache()
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""VISA hardware discovery cache.
|
|
2
|
+
|
|
3
|
+
Wraps :mod:`pyvisa` to enumerate available VISA resources exactly once per
|
|
4
|
+
process. ``pyvisa`` is an optional dependency: if it is not installed, or no
|
|
5
|
+
VISA backend is found, all functions return empty lists and a warning is logged.
|
|
6
|
+
|
|
7
|
+
Typical usage in a plugin::
|
|
8
|
+
|
|
9
|
+
from pymodaq_utils.hardware.visa import list_serial_resources
|
|
10
|
+
|
|
11
|
+
ports = list_serial_resources() # e.g. ['ASRL/dev/ttyUSB0::INSTR']
|
|
12
|
+
|
|
13
|
+
After hot-plugging a device, refresh the cache with::
|
|
14
|
+
|
|
15
|
+
from pymodaq_utils.hardware.visa import invalidate_cache
|
|
16
|
+
invalidate_cache()
|
|
17
|
+
"""
|
|
18
|
+
import pymodaq_utils.logger as logger_module
|
|
19
|
+
from .base import HardwareCache
|
|
20
|
+
|
|
21
|
+
logger = logger_module.set_logger(logger_module.get_module_name(__file__))
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class VisaCache(HardwareCache):
|
|
25
|
+
_cache = None
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def _fetch(cls) -> dict:
|
|
29
|
+
try:
|
|
30
|
+
import pyvisa
|
|
31
|
+
rm = pyvisa.ResourceManager()
|
|
32
|
+
info = dict(rm.list_resources_info())
|
|
33
|
+
rm.close()
|
|
34
|
+
return info
|
|
35
|
+
except ImportError:
|
|
36
|
+
logger.warning('pyvisa is not installed — VISA resource discovery unavailable. '
|
|
37
|
+
'Install it with: pip install pyvisa pyvisa-py')
|
|
38
|
+
return {}
|
|
39
|
+
except Exception as e:
|
|
40
|
+
logger.warning(f'VISA resource discovery failed: {e}')
|
|
41
|
+
return {}
|
|
42
|
+
|
|
43
|
+
@classmethod
|
|
44
|
+
def list_resources(cls) -> list[str]:
|
|
45
|
+
"""All available VISA resource strings (e.g. 'GPIB0::5::INSTR')."""
|
|
46
|
+
return list(cls._get_cache().keys())
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def list_serial_resources(cls) -> list[str]:
|
|
50
|
+
"""ASRL (serial-over-VISA) resource strings only.
|
|
51
|
+
|
|
52
|
+
Linux: 'ASRL/dev/ttyUSB0::INSTR'
|
|
53
|
+
Windows: 'ASRL3::INSTR'
|
|
54
|
+
"""
|
|
55
|
+
return [r for r in cls._get_cache() if r.startswith('ASRL')]
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def list_resource_aliases(cls) -> list[str]:
|
|
59
|
+
"""Human-readable aliases where available (e.g. 'COM3' on Windows)."""
|
|
60
|
+
return [i.alias for i in cls._get_cache().values() if i.alias]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def list_resources() -> list[str]:
|
|
64
|
+
"""All available VISA resource strings (e.g. 'GPIB0::5::INSTR', 'TCPIP0::...')."""
|
|
65
|
+
return VisaCache.list_resources()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def list_serial_resources() -> list[str]:
|
|
69
|
+
"""ASRL (serial-over-VISA) resource strings only.
|
|
70
|
+
|
|
71
|
+
Linux: ``'ASRL/dev/ttyUSB0::INSTR'``
|
|
72
|
+
Windows: ``'ASRL3::INSTR'``
|
|
73
|
+
"""
|
|
74
|
+
return VisaCache.list_serial_resources()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def list_resource_aliases() -> list[str]:
|
|
78
|
+
"""Human-readable aliases where available (e.g. ``'COM3'`` on Windows)."""
|
|
79
|
+
return VisaCache.list_resource_aliases()
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def invalidate_cache() -> None:
|
|
83
|
+
"""Clear the VISA resource cache so the next call re-discovers."""
|
|
84
|
+
VisaCache.invalidate_cache()
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import pytest
|
|
2
|
+
|
|
3
|
+
from pymodaq_plugins_utils.hardware.base import HardwareCache
|
|
4
|
+
from pymodaq_plugins_utils.hardware import visa, serial_ports, invalidate_all_caches
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# ---------------------------------------------------------------------------
|
|
8
|
+
# Helpers
|
|
9
|
+
# ---------------------------------------------------------------------------
|
|
10
|
+
|
|
11
|
+
class _CountingCache(HardwareCache):
|
|
12
|
+
"""Minimal concrete subclass used to test base-class caching logic."""
|
|
13
|
+
_cache = None
|
|
14
|
+
fetch_count = 0
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def _fetch(cls):
|
|
18
|
+
cls.fetch_count += 1
|
|
19
|
+
return ['resource_a', 'resource_b']
|
|
20
|
+
|
|
21
|
+
@classmethod
|
|
22
|
+
def list_resources(cls):
|
|
23
|
+
return list(cls._get_cache())
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def reset(cls):
|
|
27
|
+
cls._cache = None
|
|
28
|
+
cls.fetch_count = 0
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# ---------------------------------------------------------------------------
|
|
32
|
+
# HardwareCache base behaviour
|
|
33
|
+
# ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
class TestHardwareCacheBase:
|
|
36
|
+
|
|
37
|
+
def setup_method(self):
|
|
38
|
+
_CountingCache.reset()
|
|
39
|
+
|
|
40
|
+
def test_fetch_called_once(self):
|
|
41
|
+
# Core guarantee: no matter how many times list_resources() is called,
|
|
42
|
+
# the expensive OS-level _fetch() runs exactly once per process lifetime.
|
|
43
|
+
_CountingCache.list_resources()
|
|
44
|
+
_CountingCache.list_resources()
|
|
45
|
+
assert _CountingCache.fetch_count == 1
|
|
46
|
+
|
|
47
|
+
def test_returns_correct_data(self):
|
|
48
|
+
assert _CountingCache.list_resources() == ['resource_a', 'resource_b']
|
|
49
|
+
|
|
50
|
+
def test_invalidate_triggers_refetch(self):
|
|
51
|
+
# After invalidation the cache is empty, so the next call must
|
|
52
|
+
# re-run _fetch() — this is the hot-plug refresh path.
|
|
53
|
+
_CountingCache.list_resources()
|
|
54
|
+
_CountingCache.invalidate_cache()
|
|
55
|
+
_CountingCache.list_resources()
|
|
56
|
+
assert _CountingCache.fetch_count == 2
|
|
57
|
+
|
|
58
|
+
def test_cache_is_none_after_invalidate(self):
|
|
59
|
+
# Validates the internal reset so _get_cache() knows to call _fetch()
|
|
60
|
+
# again on the next access (tested separately in test_invalidate_triggers_refetch).
|
|
61
|
+
_CountingCache.list_resources()
|
|
62
|
+
_CountingCache.invalidate_cache()
|
|
63
|
+
assert _CountingCache._cache is None
|
|
64
|
+
|
|
65
|
+
def test_subclasses_have_independent_caches(self):
|
|
66
|
+
# Each subclass stores its result in its own _cache class variable.
|
|
67
|
+
# Invalidating one must not affect the other — otherwise a Newport
|
|
68
|
+
# plugin resetting its cache would silently clear an Arduino plugin's cache.
|
|
69
|
+
class CacheA(HardwareCache):
|
|
70
|
+
_cache = None
|
|
71
|
+
|
|
72
|
+
@classmethod
|
|
73
|
+
def _fetch(cls):
|
|
74
|
+
return ['a']
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def list_resources(cls):
|
|
78
|
+
return list(cls._get_cache())
|
|
79
|
+
|
|
80
|
+
class CacheB(HardwareCache):
|
|
81
|
+
_cache = None
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def _fetch(cls):
|
|
85
|
+
return ['b']
|
|
86
|
+
|
|
87
|
+
@classmethod
|
|
88
|
+
def list_resources(cls):
|
|
89
|
+
return list(cls._get_cache())
|
|
90
|
+
|
|
91
|
+
assert CacheA.list_resources() == ['a']
|
|
92
|
+
assert CacheB.list_resources() == ['b']
|
|
93
|
+
CacheA.invalidate_cache()
|
|
94
|
+
assert CacheA._cache is None
|
|
95
|
+
assert CacheB._cache is not None # CacheB untouched
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# ---------------------------------------------------------------------------
|
|
99
|
+
# visa module
|
|
100
|
+
# ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
class TestVisaModule:
|
|
103
|
+
|
|
104
|
+
def setup_method(self):
|
|
105
|
+
# Start each test with a clean cache so tests are isolated.
|
|
106
|
+
visa.VisaCache.invalidate_cache()
|
|
107
|
+
|
|
108
|
+
def test_list_resources_returns_list(self):
|
|
109
|
+
# The module must never raise, even when pyvisa is not installed.
|
|
110
|
+
assert isinstance(visa.list_resources(), list)
|
|
111
|
+
|
|
112
|
+
def test_warns_when_pyvisa_absent(self, monkeypatch):
|
|
113
|
+
# When pyvisa is missing the user should get an explicit warning,
|
|
114
|
+
# not a silent empty list that looks like "no devices connected".
|
|
115
|
+
import builtins
|
|
116
|
+
real_import = builtins.__import__
|
|
117
|
+
|
|
118
|
+
def block_pyvisa(name, *args, **kwargs):
|
|
119
|
+
if name == 'pyvisa':
|
|
120
|
+
raise ImportError('pyvisa blocked for test')
|
|
121
|
+
return real_import(name, *args, **kwargs)
|
|
122
|
+
|
|
123
|
+
warned = []
|
|
124
|
+
monkeypatch.setattr(builtins, '__import__', block_pyvisa)
|
|
125
|
+
monkeypatch.setattr(visa.logger, 'warning', lambda msg: warned.append(msg))
|
|
126
|
+
visa.VisaCache.invalidate_cache()
|
|
127
|
+
|
|
128
|
+
visa.list_resources()
|
|
129
|
+
assert any('pyvisa' in msg for msg in warned)
|
|
130
|
+
|
|
131
|
+
def test_list_serial_resources_returns_list(self):
|
|
132
|
+
assert isinstance(visa.list_serial_resources(), list)
|
|
133
|
+
|
|
134
|
+
def test_list_resource_aliases_returns_list(self):
|
|
135
|
+
assert isinstance(visa.list_resource_aliases(), list)
|
|
136
|
+
|
|
137
|
+
def test_serial_resources_are_subset_of_all(self):
|
|
138
|
+
# list_serial_resources() is a filtered view of list_resources();
|
|
139
|
+
# every ASRL entry must also appear in the full resource list.
|
|
140
|
+
# Skipped when pyvisa is absent: an empty list would make this pass
|
|
141
|
+
# vacuously without testing anything.
|
|
142
|
+
pytest.importorskip('pyvisa')
|
|
143
|
+
all_r = visa.list_resources()
|
|
144
|
+
serial_r = visa.list_serial_resources()
|
|
145
|
+
assert all(r in all_r for r in serial_r)
|
|
146
|
+
|
|
147
|
+
def test_serial_resources_start_with_asrl(self):
|
|
148
|
+
# VISA serial resources always begin with 'ASRL' by the VISA standard.
|
|
149
|
+
# Skipped when pyvisa is absent for the same reason as above.
|
|
150
|
+
pytest.importorskip('pyvisa')
|
|
151
|
+
for r in visa.list_serial_resources():
|
|
152
|
+
assert r.startswith('ASRL')
|
|
153
|
+
|
|
154
|
+
def test_fetch_called_once_across_multiple_functions(self, monkeypatch):
|
|
155
|
+
# Calling list_resources(), list_serial_resources(), and
|
|
156
|
+
# list_resource_aliases() in sequence must trigger only one backend
|
|
157
|
+
# query — the whole point of this module.
|
|
158
|
+
call_count = []
|
|
159
|
+
original_fetch = visa.VisaCache._fetch.__func__
|
|
160
|
+
|
|
161
|
+
@classmethod
|
|
162
|
+
def counting_fetch(cls):
|
|
163
|
+
call_count.append(1)
|
|
164
|
+
return original_fetch(cls)
|
|
165
|
+
|
|
166
|
+
monkeypatch.setattr(visa.VisaCache, '_fetch', counting_fetch)
|
|
167
|
+
visa.VisaCache.invalidate_cache()
|
|
168
|
+
|
|
169
|
+
visa.list_resources()
|
|
170
|
+
visa.list_serial_resources()
|
|
171
|
+
visa.list_resource_aliases()
|
|
172
|
+
|
|
173
|
+
assert len(call_count) == 1
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
# ---------------------------------------------------------------------------
|
|
177
|
+
# serial_ports module
|
|
178
|
+
# ---------------------------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
class TestSerialPortsModule:
|
|
181
|
+
|
|
182
|
+
def setup_method(self):
|
|
183
|
+
serial_ports.SerialPortsCache.invalidate_cache()
|
|
184
|
+
|
|
185
|
+
def test_list_resources_returns_list(self):
|
|
186
|
+
# The module must never raise, even when pyserial is not installed.
|
|
187
|
+
assert isinstance(serial_ports.list_resources(), list)
|
|
188
|
+
|
|
189
|
+
def test_warns_when_pyserial_absent(self, monkeypatch):
|
|
190
|
+
# Same contract as the visa equivalent: missing backend → warning, not silent empty list.
|
|
191
|
+
import builtins
|
|
192
|
+
real_import = builtins.__import__
|
|
193
|
+
|
|
194
|
+
def block_serial(name, *args, **kwargs):
|
|
195
|
+
if name == 'serial.tools.list_ports':
|
|
196
|
+
raise ImportError('pyserial blocked for test')
|
|
197
|
+
return real_import(name, *args, **kwargs)
|
|
198
|
+
|
|
199
|
+
warned = []
|
|
200
|
+
monkeypatch.setattr(builtins, '__import__', block_serial)
|
|
201
|
+
monkeypatch.setattr(serial_ports.logger, 'warning', lambda msg: warned.append(msg))
|
|
202
|
+
serial_ports.SerialPortsCache.invalidate_cache()
|
|
203
|
+
|
|
204
|
+
serial_ports.list_resources()
|
|
205
|
+
assert any('pyserial' in msg for msg in warned)
|
|
206
|
+
|
|
207
|
+
def test_list_port_descriptions_returns_list(self):
|
|
208
|
+
assert isinstance(serial_ports.list_port_descriptions(), list)
|
|
209
|
+
|
|
210
|
+
def test_resources_and_descriptions_same_length(self):
|
|
211
|
+
# Both lists are derived from the same cached port objects, so they
|
|
212
|
+
# must always be parallel (index N in one matches index N in the other).
|
|
213
|
+
# Skipped when pyserial is absent: both would be empty and the assertion
|
|
214
|
+
# passes vacuously without testing anything.
|
|
215
|
+
pytest.importorskip('serial')
|
|
216
|
+
assert len(serial_ports.list_resources()) == len(serial_ports.list_port_descriptions())
|
|
217
|
+
|
|
218
|
+
def test_fetch_called_once_across_multiple_functions(self, monkeypatch):
|
|
219
|
+
# Same single-fetch guarantee as the visa module.
|
|
220
|
+
call_count = []
|
|
221
|
+
original_fetch = serial_ports.SerialPortsCache._fetch.__func__
|
|
222
|
+
|
|
223
|
+
@classmethod
|
|
224
|
+
def counting_fetch(cls):
|
|
225
|
+
call_count.append(1)
|
|
226
|
+
return original_fetch(cls)
|
|
227
|
+
|
|
228
|
+
monkeypatch.setattr(serial_ports.SerialPortsCache, '_fetch', counting_fetch)
|
|
229
|
+
serial_ports.SerialPortsCache.invalidate_cache()
|
|
230
|
+
|
|
231
|
+
serial_ports.list_resources()
|
|
232
|
+
serial_ports.list_port_descriptions()
|
|
233
|
+
|
|
234
|
+
assert len(call_count) == 1
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
# ---------------------------------------------------------------------------
|
|
238
|
+
# Package-level helper
|
|
239
|
+
# ---------------------------------------------------------------------------
|
|
240
|
+
|
|
241
|
+
def test_invalidate_all_caches_clears_both():
|
|
242
|
+
# Populate both caches first so the assertion is meaningful.
|
|
243
|
+
visa.list_resources()
|
|
244
|
+
serial_ports.list_resources()
|
|
245
|
+
|
|
246
|
+
invalidate_all_caches()
|
|
247
|
+
|
|
248
|
+
assert visa.VisaCache._cache is None
|
|
249
|
+
assert serial_ports.SerialPortsCache._cache is None
|
|
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_plugins_utils-5.0.4 → pymodaq_plugins_utils-5.0.5}/src/pymodaq_plugins_utils/__init__.py
RENAMED
|
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_plugins_utils-5.0.4 → pymodaq_plugins_utils-5.0.5}/src/pymodaq_plugins_utils/utils.py
RENAMED
|
File without changes
|
{pymodaq_plugins_utils-5.0.4 → pymodaq_plugins_utils-5.0.5}/tests/test_plugin_package_structure.py
RENAMED
|
File without changes
|