pymmcore-plus 0.15.3__tar.gz → 0.16.0__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.
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/PKG-INFO +3 -2
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/pyproject.toml +9 -2
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/__init__.py +20 -1
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/_accumulator.py +23 -5
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/_cli.py +44 -26
- pymmcore_plus-0.16.0/src/pymmcore_plus/_discovery.py +344 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/_logger.py +1 -1
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/_util.py +9 -245
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/core/_device.py +20 -7
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/core/_mmcore_plus.py +16 -8
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/core/_property.py +34 -28
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/core/events/_device_signal_view.py +8 -1
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/core/events/_protocol.py +3 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/core/events/_psygnal.py +1 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/core/events/_qsignals.py +1 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/experimental/unicore/_device_manager.py +20 -13
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/experimental/unicore/core/_unicore.py +4 -1
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/install.py +150 -18
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/mda/_engine.py +268 -73
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/mda/events/__init__.py +1 -1
- pymmcore_plus-0.16.0/src/pymmcore_plus/metadata/_ome.py +499 -0
- {pymmcore_plus-0.15.3/tests/unicore → pymmcore_plus-0.16.0/tests/00_unicore}/test_unicore.py +20 -20
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/tests/test_accumulators.py +1 -1
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/tests/test_cli.py +21 -15
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/tests/test_core.py +3 -3
- pymmcore_plus-0.16.0/tests/test_core_references.py +194 -0
- pymmcore_plus-0.16.0/tests/test_install.py +171 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/tests/test_ipy_completions.py +5 -5
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/tests/test_mda.py +119 -2
- pymmcore_plus-0.16.0/tests/test_metadata_to_ome.py +169 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/tests/test_model.py +9 -1
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/tests/test_sequencing.py +1 -1
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/tests/test_slm_image.py +1 -1
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/.gitignore +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/LICENSE +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/README.md +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/_benchmark.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/_build.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/_ipy_completion.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/_pymmcore.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/core/__init__.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/core/_adapter.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/core/_config.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/core/_config_group.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/core/_constants.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/core/_metadata.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/core/_sequencing.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/core/events/__init__.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/core/events/_deprecated.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/core/events/_norm_slot.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/core/events/_prop_event_mixin.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/experimental/__init__.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/experimental/unicore/__init__.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/experimental/unicore/_proxy.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/experimental/unicore/core/__init__.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/experimental/unicore/core/_sequence_buffer.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/experimental/unicore/devices/__init__.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/experimental/unicore/devices/_camera.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/experimental/unicore/devices/_device_base.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/experimental/unicore/devices/_generic_device.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/experimental/unicore/devices/_properties.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/experimental/unicore/devices/_shutter.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/experimental/unicore/devices/_slm.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/experimental/unicore/devices/_stage.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/experimental/unicore/devices/_state.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/mda/__init__.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/mda/_protocol.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/mda/_runner.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/mda/_thread_relay.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/mda/events/_protocol.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/mda/events/_psygnal.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/mda/events/_qsignals.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/mda/handlers/_5d_writer_base.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/mda/handlers/__init__.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/mda/handlers/_img_sequence_writer.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/mda/handlers/_ome_tiff_writer.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/mda/handlers/_ome_zarr_writer.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/mda/handlers/_tensorstore_handler.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/mda/handlers/_util.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/metadata/__init__.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/metadata/functions.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/metadata/schema.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/metadata/serialize.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/mocks.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/model/__init__.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/model/_config_file.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/model/_config_group.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/model/_core_device.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/model/_core_link.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/model/_device.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/model/_microscope.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/model/_pixel_size_config.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/model/_property.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/py.typed +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/src/pymmcore_plus/seq_tester.py +0 -0
- {pymmcore_plus-0.15.3/tests/unicore → pymmcore_plus-0.16.0/tests/00_unicore}/conftest.py +0 -0
- {pymmcore_plus-0.15.3/tests/unicore → pymmcore_plus-0.16.0/tests/00_unicore}/test_camera.py +0 -0
- {pymmcore_plus-0.15.3/tests/unicore → pymmcore_plus-0.16.0/tests/00_unicore}/test_sequence_buffer.py +0 -0
- {pymmcore_plus-0.15.3/tests/unicore → pymmcore_plus-0.16.0/tests/00_unicore}/test_shutter.py +0 -0
- {pymmcore_plus-0.15.3/tests/unicore → pymmcore_plus-0.16.0/tests/00_unicore}/test_slm.py +0 -0
- {pymmcore_plus-0.15.3/tests/unicore → pymmcore_plus-0.16.0/tests/00_unicore}/test_state.py +0 -0
- {pymmcore_plus-0.15.3/tests/unicore → pymmcore_plus-0.16.0/tests/00_unicore}/test_xy_stage.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/tests/__init__.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/tests/conftest.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/tests/io/test_image_sequence_writer.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/tests/io/test_ome_tiff.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/tests/io/test_zarr_writers.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/tests/local_config.cfg +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/tests/test_adapter_class.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/tests/test_bench.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/tests/test_config_group_class.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/tests/test_device_class.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/tests/test_events.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/tests/test_metadata.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/tests/test_misc.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/tests/test_pixel_config_class.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/tests/test_property_class.py +0 -0
- {pymmcore_plus-0.15.3 → pymmcore_plus-0.16.0}/tests/test_thread_relay.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pymmcore-plus
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.16.0
|
|
4
4
|
Summary: pymmcore superset providing improved APIs, event handling, and a pure python acquisition engine
|
|
5
5
|
Project-URL: Source, https://github.com/pymmcore-plus/pymmcore-plus
|
|
6
6
|
Project-URL: Tracker, https://github.com/pymmcore-plus/pymmcore-plus/issues
|
|
@@ -28,9 +28,10 @@ Requires-Python: >=3.9
|
|
|
28
28
|
Requires-Dist: numpy>=1.25.2
|
|
29
29
|
Requires-Dist: numpy>=1.26.0; python_version >= '3.12'
|
|
30
30
|
Requires-Dist: numpy>=2.1.0; python_version >= '3.13'
|
|
31
|
+
Requires-Dist: ome-types>=0.6.0
|
|
31
32
|
Requires-Dist: platformdirs>=3.0.0
|
|
32
33
|
Requires-Dist: psygnal>=0.10
|
|
33
|
-
Requires-Dist: pymmcore>=11.
|
|
34
|
+
Requires-Dist: pymmcore>=11.10.0.74.0
|
|
34
35
|
Requires-Dist: rich>=10.2.0
|
|
35
36
|
Requires-Dist: tensorstore!=0.1.72,>=0.1.67
|
|
36
37
|
Requires-Dist: tensorstore!=0.1.72,>=0.1.71; python_version >= '3.13'
|
|
@@ -40,7 +40,7 @@ dependencies = [
|
|
|
40
40
|
"numpy >=1.26.0; python_version >= '3.12'",
|
|
41
41
|
"numpy >=1.25.2",
|
|
42
42
|
"psygnal >=0.10",
|
|
43
|
-
"pymmcore >=11.
|
|
43
|
+
"pymmcore >=11.10.0.74.0",
|
|
44
44
|
"typing-extensions >=4", # not actually required at runtime
|
|
45
45
|
"useq-schema >=0.7.2",
|
|
46
46
|
"tensorstore >=0.1.71,!=0.1.72; python_version >= '3.13'",
|
|
@@ -48,6 +48,7 @@ dependencies = [
|
|
|
48
48
|
# cli requirements included by default for now
|
|
49
49
|
"typer >=0.4.2",
|
|
50
50
|
"rich >=10.2.0",
|
|
51
|
+
"ome-types >=0.6.0",
|
|
51
52
|
]
|
|
52
53
|
|
|
53
54
|
# extras
|
|
@@ -61,6 +62,7 @@ PyQt5 = ["PyQt5 >=5.15.4"]
|
|
|
61
62
|
PyQt6 = ["PyQt6 >=6.4.2"]
|
|
62
63
|
|
|
63
64
|
[dependency-groups]
|
|
65
|
+
nano = ["pymmcore-nano==11.10.0.74.0"]
|
|
64
66
|
docs = [
|
|
65
67
|
"mkdocs >=1.4",
|
|
66
68
|
"mkdocs-material>=9.5",
|
|
@@ -73,10 +75,11 @@ test = [
|
|
|
73
75
|
"pymmcore-plus[io]",
|
|
74
76
|
"msgspec >= 0.19",
|
|
75
77
|
"msgpack >=1",
|
|
76
|
-
"pytest-cov >=
|
|
78
|
+
"pytest-cov >=7",
|
|
77
79
|
"ipython>=8.18.0",
|
|
78
80
|
"pytest >=8",
|
|
79
81
|
"xarray >=2024.1",
|
|
82
|
+
"lxml >=6.0",
|
|
80
83
|
]
|
|
81
84
|
test-codspeed = [{ include-group = "test" }, "pytest-codspeed >=3.2.0"]
|
|
82
85
|
test-qt = [{ include-group = 'test' }, "pytest-qt ==4.4", "qtpy >=2"]
|
|
@@ -173,6 +176,9 @@ filterwarnings = [
|
|
|
173
176
|
"error",
|
|
174
177
|
"ignore:Failed to disconnect::pytestqt",
|
|
175
178
|
"ignore:numpy.core.multiarray is deprecated",
|
|
179
|
+
"ignore:Focus direction is unknown",
|
|
180
|
+
"ignore:'BaseCommand' is deprecated:DeprecationWarning:typer",
|
|
181
|
+
"ignore:Failing to pass a value to the 'type_params' parameter",
|
|
176
182
|
]
|
|
177
183
|
markers = ["run_last: mark a test to run last"]
|
|
178
184
|
|
|
@@ -200,6 +206,7 @@ exclude_lines = [
|
|
|
200
206
|
"if TYPE_CHECKING:",
|
|
201
207
|
"@overload",
|
|
202
208
|
"except ImportError",
|
|
209
|
+
"except Exception as e:", # ok not to cover bare exception catches in tests
|
|
203
210
|
"raise AssertionError",
|
|
204
211
|
"\\.\\.\\.",
|
|
205
212
|
"if __name__ == .__main__.:",
|
|
@@ -2,16 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
4
|
from importlib.metadata import PackageNotFoundError, version
|
|
5
|
+
from typing import TYPE_CHECKING, Any
|
|
5
6
|
|
|
6
7
|
try:
|
|
7
8
|
__version__ = version("pymmcore-plus")
|
|
8
9
|
except PackageNotFoundError: # pragma: no cover
|
|
9
10
|
__version__ = "unknown"
|
|
10
11
|
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from ._ipy_completion import install_pymmcore_ipy_completion
|
|
11
14
|
|
|
12
15
|
from ._accumulator import AbstractChangeAccumulator
|
|
16
|
+
from ._discovery import find_micromanager, use_micromanager
|
|
13
17
|
from ._logger import configure_logging
|
|
14
|
-
from ._util import find_micromanager, use_micromanager
|
|
15
18
|
from .core import (
|
|
16
19
|
ActionType,
|
|
17
20
|
CFGCommand,
|
|
@@ -63,10 +66,26 @@ __all__ = [
|
|
|
63
66
|
"__version__",
|
|
64
67
|
"configure_logging",
|
|
65
68
|
"find_micromanager",
|
|
69
|
+
"install_pymmcore_ipy_completion",
|
|
66
70
|
"use_micromanager",
|
|
67
71
|
]
|
|
68
72
|
|
|
69
73
|
|
|
74
|
+
def __getattr__(name: str) -> Any:
|
|
75
|
+
"""Lazy import for compatibility with pymmcore."""
|
|
76
|
+
if name == "install_pymmcore_ipy_completion":
|
|
77
|
+
try:
|
|
78
|
+
from ._ipy_completion import install_pymmcore_ipy_completion
|
|
79
|
+
except ImportError as e: # pragma: no cover
|
|
80
|
+
raise ImportError(
|
|
81
|
+
f"Error importing IPython completion for pymmcore-plus: {e}"
|
|
82
|
+
) from None
|
|
83
|
+
|
|
84
|
+
return install_pymmcore_ipy_completion
|
|
85
|
+
raise AttributeError(f"module '{__name__}' has no attribute '{name}'. ")
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# install the IPython completer when imported, if running in an IPython environment
|
|
70
89
|
def _install_ipy_completer() -> None: # pragma: no cover
|
|
71
90
|
import os
|
|
72
91
|
import sys
|
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import abc
|
|
6
6
|
import sys
|
|
7
|
+
import weakref
|
|
7
8
|
from abc import ABC, abstractmethod
|
|
8
9
|
from collections.abc import Sequence
|
|
9
10
|
from typing import TYPE_CHECKING, Any, ClassVar, Generic, Literal, TypeVar
|
|
@@ -166,9 +167,10 @@ class DeviceAccumulator(abc.ABC, Generic[DT]):
|
|
|
166
167
|
mmcore: CMMCorePlus | None = None,
|
|
167
168
|
**kwargs: Any,
|
|
168
169
|
) -> None:
|
|
169
|
-
|
|
170
|
+
core = mmcore or CMMCorePlus.instance()
|
|
171
|
+
self._mmcore_ref = weakref.ref(core)
|
|
170
172
|
dev_type = self._device_type()
|
|
171
|
-
if not
|
|
173
|
+
if not core.getDeviceType(device_label) == dev_type: # pragma: no cover
|
|
172
174
|
raise ValueError(
|
|
173
175
|
f"Cannot create {self.__class__.__name__}. "
|
|
174
176
|
f"Device {device_label!r} is not a {dev_type.name}. "
|
|
@@ -177,6 +179,16 @@ class DeviceAccumulator(abc.ABC, Generic[DT]):
|
|
|
177
179
|
self._device_label = device_label
|
|
178
180
|
super().__init__(**kwargs)
|
|
179
181
|
|
|
182
|
+
@property
|
|
183
|
+
def _mmcore(self) -> CMMCorePlus:
|
|
184
|
+
"""Get the CMMCorePlus instance for this accumulator."""
|
|
185
|
+
if (mmcore := self._mmcore_ref()) is None: # pragma: no cover
|
|
186
|
+
raise RuntimeError(
|
|
187
|
+
f"The CMMCorePlus instance for this {self.__class__.__name__!r} has "
|
|
188
|
+
"been garbage collected."
|
|
189
|
+
)
|
|
190
|
+
return mmcore
|
|
191
|
+
|
|
180
192
|
def _is_busy(self) -> bool:
|
|
181
193
|
return self._mmcore.deviceBusy(self._device_label)
|
|
182
194
|
|
|
@@ -204,18 +216,24 @@ class DeviceAccumulator(abc.ABC, Generic[DT]):
|
|
|
204
216
|
device_type = mmcore.getDeviceType(device)
|
|
205
217
|
if cache_key not in DeviceAccumulator._CACHE:
|
|
206
218
|
if device_type == cls._device_type():
|
|
207
|
-
|
|
219
|
+
DeviceAccumulator._CACHE[cache_key] = cls(
|
|
220
|
+
device_label=device, mmcore=mmcore
|
|
221
|
+
)
|
|
208
222
|
else:
|
|
209
223
|
for sub in cls.__subclasses__():
|
|
210
224
|
if sub._device_type() == device_type: # noqa: SLF001
|
|
211
|
-
|
|
225
|
+
DeviceAccumulator._CACHE[cache_key] = sub(
|
|
226
|
+
device_label=device, mmcore=mmcore
|
|
227
|
+
)
|
|
212
228
|
break
|
|
213
229
|
else:
|
|
214
230
|
raise ValueError(
|
|
215
231
|
"No matching DeviceTypeMixin subclass found for device type "
|
|
216
232
|
f"{device_type.name} (for device {device!r})."
|
|
217
233
|
)
|
|
218
|
-
|
|
234
|
+
|
|
235
|
+
weakref.finalize(mmcore, DeviceAccumulator._CACHE.pop, cache_key, None)
|
|
236
|
+
obj = DeviceAccumulator._CACHE[cache_key]
|
|
219
237
|
if not isinstance(obj, cls):
|
|
220
238
|
raise TypeError(
|
|
221
239
|
f"Cannot create {cls.__name__} for {device!r}. "
|
|
@@ -7,9 +7,9 @@ import sys
|
|
|
7
7
|
import time
|
|
8
8
|
from contextlib import suppress
|
|
9
9
|
from pathlib import Path
|
|
10
|
+
from platform import system
|
|
10
11
|
from typing import Optional, Union, cast
|
|
11
12
|
|
|
12
|
-
from pymmcore_plus._util import get_device_interface_version
|
|
13
13
|
from pymmcore_plus.core._device import Device
|
|
14
14
|
from pymmcore_plus.core._mmcore_plus import CMMCorePlus
|
|
15
15
|
|
|
@@ -26,9 +26,9 @@ import pymmcore_plus
|
|
|
26
26
|
from pymmcore_plus._build import DEFAULT_PACKAGES, build
|
|
27
27
|
from pymmcore_plus._logger import configure_logging
|
|
28
28
|
from pymmcore_plus._util import USER_DATA_MM_PATH
|
|
29
|
-
from pymmcore_plus.install import PLATFORM
|
|
30
29
|
|
|
31
30
|
app = typer.Typer(name="mmcore", no_args_is_help=True)
|
|
31
|
+
PLATFORM = system()
|
|
32
32
|
|
|
33
33
|
|
|
34
34
|
def _show_version_and_exit(value: bool) -> None:
|
|
@@ -103,32 +103,44 @@ def clean(
|
|
|
103
103
|
|
|
104
104
|
@app.command(name="list")
|
|
105
105
|
def _list() -> None:
|
|
106
|
-
"""Show all Micro-Manager
|
|
106
|
+
"""Show all discovered Micro-Manager installations."""
|
|
107
|
+
from pymmcore_plus import _discovery
|
|
108
|
+
|
|
107
109
|
configure_logging(stderr_level="CRITICAL")
|
|
108
|
-
found: dict[Path, list[str]] = {}
|
|
110
|
+
found: dict[Path, list[tuple[str, _discovery.DiscoveredMM]]] = {}
|
|
109
111
|
with suppress(Exception):
|
|
110
|
-
for
|
|
111
|
-
pth = Path(
|
|
112
|
-
found.setdefault(pth.parent, []).append(pth.name)
|
|
112
|
+
for dm in _discovery.discover_mm():
|
|
113
|
+
pth = Path(dm.path)
|
|
114
|
+
found.setdefault(pth.parent, []).append((pth.name, dm))
|
|
115
|
+
|
|
116
|
+
active_mm = _discovery.find_micromanager(return_first=True)
|
|
117
|
+
required_div = _discovery.PYMMCORE_DIV
|
|
113
118
|
|
|
114
119
|
if found:
|
|
115
|
-
|
|
120
|
+
print(f"[magenta]Required pymmcore device interface version: {required_div}")
|
|
116
121
|
for parent, items in found.items():
|
|
117
|
-
print(f":file_folder:[bold green] {parent}")
|
|
118
|
-
for
|
|
119
|
-
version = ""
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
122
|
+
print(f"\n:file_folder:[bold green] {parent}")
|
|
123
|
+
for item_name, dm in items:
|
|
124
|
+
version = f" (DIV {dm.device_interface})" if dm.device_interface else ""
|
|
125
|
+
is_active = str(dm.path) == active_mm
|
|
126
|
+
is_compatible = dm.div_compatible
|
|
127
|
+
|
|
128
|
+
# Choose bullet and status
|
|
129
|
+
bullet = " •"
|
|
130
|
+
status = ""
|
|
131
|
+
if is_active:
|
|
132
|
+
bullet = " [bold yellow]➤"
|
|
133
|
+
if is_compatible:
|
|
134
|
+
status = " [bold blue](active)[/bold blue]"
|
|
135
|
+
else:
|
|
136
|
+
status = " [bold red](active, incompatible!)[/bold red]"
|
|
137
|
+
else:
|
|
138
|
+
status = " [red](incompatible)[/red]"
|
|
139
|
+
|
|
140
|
+
print(f"{bullet} [cyan]{item_name}{version}{status}")
|
|
129
141
|
else:
|
|
130
|
-
print(":x: [bold red]
|
|
131
|
-
print("[magenta]
|
|
142
|
+
print(":x: [bold red]No Micro-Manager installations found.")
|
|
143
|
+
print("[magenta]Run `mmcore install` to install a version of Micro-Manager")
|
|
132
144
|
|
|
133
145
|
|
|
134
146
|
@app.command()
|
|
@@ -174,18 +186,24 @@ def install(
|
|
|
174
186
|
help="Do not use rich output. Useful for scripting.",
|
|
175
187
|
show_default=False,
|
|
176
188
|
),
|
|
189
|
+
test_adapters: bool = typer.Option(
|
|
190
|
+
False,
|
|
191
|
+
"--test-adapters",
|
|
192
|
+
help="Install only test adapters (e.g. DemoCamera and others for testing).",
|
|
193
|
+
),
|
|
177
194
|
) -> None:
|
|
178
195
|
"""Install Micro-Manager Device adapters from <https://download.micro-manager.org>."""
|
|
179
196
|
import pymmcore_plus.install
|
|
180
197
|
|
|
198
|
+
kwargs = {}
|
|
181
199
|
if plain_output:
|
|
182
200
|
|
|
183
201
|
def _log_msg(text: str, color: str = "", emoji: str = "") -> None:
|
|
184
202
|
print(text)
|
|
185
203
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
204
|
+
kwargs["log_msg"] = _log_msg
|
|
205
|
+
|
|
206
|
+
pymmcore_plus.install.install(dest, release, test_adapters=test_adapters, **kwargs)
|
|
189
207
|
|
|
190
208
|
|
|
191
209
|
@app.command()
|
|
@@ -414,7 +432,7 @@ def use(
|
|
|
414
432
|
),
|
|
415
433
|
) -> None:
|
|
416
434
|
"""Change the currently used Micro-manager version/path."""
|
|
417
|
-
from pymmcore_plus.
|
|
435
|
+
from pymmcore_plus._discovery import use_micromanager
|
|
418
436
|
|
|
419
437
|
_pth = Path(pattern)
|
|
420
438
|
if _pth.exists():
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import ctypes
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import sys
|
|
7
|
+
import warnings
|
|
8
|
+
from contextlib import suppress
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import TYPE_CHECKING, overload
|
|
12
|
+
|
|
13
|
+
from platformdirs import user_data_dir
|
|
14
|
+
|
|
15
|
+
from . import _pymmcore
|
|
16
|
+
|
|
17
|
+
if TYPE_CHECKING:
|
|
18
|
+
from collections.abc import Iterator
|
|
19
|
+
from typing import Literal
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
__all__ = ["discover_mm", "find_micromanager", "use_micromanager"]
|
|
23
|
+
|
|
24
|
+
APP_NAME = "pymmcore-plus"
|
|
25
|
+
USER_DATA_DIR = Path(user_data_dir(appname=APP_NAME))
|
|
26
|
+
USER_DATA_MM_PATH = USER_DATA_DIR / "mm"
|
|
27
|
+
CURRENT_MM_PATH = USER_DATA_MM_PATH / ".current_mm"
|
|
28
|
+
PYMMCORE_PLUS_PATH = Path(__file__).parent.parent
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
PYMMCORE_DIV = _pymmcore.version_info.device_interface
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class DiscoveredMM:
|
|
36
|
+
"""A discovered path with micro-manager devices."""
|
|
37
|
+
|
|
38
|
+
path: Path
|
|
39
|
+
env_var: str | None = None
|
|
40
|
+
device_interface: int | None = None
|
|
41
|
+
device_paths: set[Path] | None = field(default=None, repr=False)
|
|
42
|
+
num_devices: int | None = None
|
|
43
|
+
div_compatible: bool = False
|
|
44
|
+
is_current: bool = False
|
|
45
|
+
|
|
46
|
+
def __post_init__(self) -> None:
|
|
47
|
+
# normalize early to avoid cache key duplication from symlinks or relative paths
|
|
48
|
+
try:
|
|
49
|
+
self.path = self.path.resolve()
|
|
50
|
+
except Exception:
|
|
51
|
+
# be defensive if resolution fails on odd platforms
|
|
52
|
+
self.path = Path(self.path)
|
|
53
|
+
|
|
54
|
+
def _populate_info(self, force: bool = False) -> None:
|
|
55
|
+
# one time on-demand population of additional metadata
|
|
56
|
+
if self.device_interface is None or force:
|
|
57
|
+
self.device_interface = get_first_device_interface_version(self.path)
|
|
58
|
+
self.div_compatible = self.device_interface == PYMMCORE_DIV
|
|
59
|
+
self.num_devices = len(set(_iter_device_paths(self.path)))
|
|
60
|
+
|
|
61
|
+
def merge_from(self, other: DiscoveredMM) -> None:
|
|
62
|
+
# preserve flags from whichever source had them
|
|
63
|
+
if other.is_current:
|
|
64
|
+
self.is_current = True
|
|
65
|
+
if other.env_var and not self.env_var:
|
|
66
|
+
self.env_var = other.env_var
|
|
67
|
+
if other.device_interface and not self.device_interface:
|
|
68
|
+
self.device_interface = other.device_interface
|
|
69
|
+
self.div_compatible = other.div_compatible
|
|
70
|
+
self.num_devices = other.num_devices
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _env_var_mm() -> Iterator[DiscoveredMM]:
|
|
74
|
+
"""Discover Micro-Manager installations from the MICROMANAGER_PATH env var."""
|
|
75
|
+
env_path = os.getenv("MICROMANAGER_PATH")
|
|
76
|
+
if env_path and os.path.isdir(env_path):
|
|
77
|
+
yield DiscoveredMM(path=Path(env_path), env_var="MICROMANAGER_PATH")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _current_mm() -> Iterator[DiscoveredMM]:
|
|
81
|
+
"""Discover Micro-Manager installation at the CURRENT_MM_PATH file."""
|
|
82
|
+
if CURRENT_MM_PATH.exists():
|
|
83
|
+
path = Path(CURRENT_MM_PATH.read_text().strip())
|
|
84
|
+
if path.is_dir():
|
|
85
|
+
yield DiscoveredMM(path=path, is_current=True)
|
|
86
|
+
else:
|
|
87
|
+
from ._logger import logger
|
|
88
|
+
|
|
89
|
+
logger.warning(
|
|
90
|
+
f"User-selected micromanager path {path} is not a directory, clearing."
|
|
91
|
+
)
|
|
92
|
+
CURRENT_MM_PATH.unlink(missing_ok=True)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _mmcore_installed_mm(glob: str = "Micro-Manager*") -> Iterator[DiscoveredMM]:
|
|
96
|
+
"""Discover Micro-Manager installations in the pymmcore-plus user data directory."""
|
|
97
|
+
for path in sorted(USER_DATA_MM_PATH.glob(glob), reverse=True):
|
|
98
|
+
if path.is_dir():
|
|
99
|
+
yield DiscoveredMM(path=path)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _mm_test_adapter_mm() -> Iterator[DiscoveredMM]:
|
|
103
|
+
"""Discover Micro-Manager from the mm-test-adapters package."""
|
|
104
|
+
with suppress(ImportError, AttributeError):
|
|
105
|
+
import mm_test_adapters
|
|
106
|
+
|
|
107
|
+
path = Path(mm_test_adapters.device_adapter_path())
|
|
108
|
+
if path.is_dir():
|
|
109
|
+
yield DiscoveredMM(path=path)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _application_installs() -> Iterator[DiscoveredMM]:
|
|
113
|
+
"""Discover official Micro-Manager installations in the application directory."""
|
|
114
|
+
applications = {
|
|
115
|
+
"darwin": Path("/Applications/"),
|
|
116
|
+
"win32": Path("C:/Program Files/"),
|
|
117
|
+
"linux": Path("/usr/local/lib"),
|
|
118
|
+
}
|
|
119
|
+
if app_path := applications.get(sys.platform):
|
|
120
|
+
for pth in app_path.glob("[m,M]icro-[m,M]anager*"):
|
|
121
|
+
yield DiscoveredMM(path=pth)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _iter_mm_paths() -> Iterator[DiscoveredMM]:
|
|
125
|
+
"""Iterate over all discovered Micro-Manager paths.
|
|
126
|
+
|
|
127
|
+
Order here influences the return value of find_micromanager() when
|
|
128
|
+
`return_first` is True.
|
|
129
|
+
"""
|
|
130
|
+
yield from _env_var_mm()
|
|
131
|
+
yield from _current_mm()
|
|
132
|
+
yield from _mmcore_installed_mm()
|
|
133
|
+
yield from _mm_test_adapter_mm()
|
|
134
|
+
yield from _application_installs()
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
_DISCOVERED_MMS: dict[Path, DiscoveredMM] = {}
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def discover_mm() -> Iterator[DiscoveredMM]:
|
|
141
|
+
"""Discover Micro-Manager installations with caching and dedup by path."""
|
|
142
|
+
yielded: set[Path] = set()
|
|
143
|
+
|
|
144
|
+
for candidate in _iter_mm_paths():
|
|
145
|
+
key = candidate.path
|
|
146
|
+
existing = _DISCOVERED_MMS.get(key)
|
|
147
|
+
|
|
148
|
+
if existing is None:
|
|
149
|
+
candidate._populate_info() # noqa: SLF001
|
|
150
|
+
# only cache and consider entries that look like real installs
|
|
151
|
+
# If the discovery came from an explicit environment variable, keep
|
|
152
|
+
# the entry even if it doesn't currently contain device adapter
|
|
153
|
+
# libraries. This allows users (and tests) to point to a path that
|
|
154
|
+
# may be populated later or is intentionally empty.
|
|
155
|
+
if candidate.device_interface is None and candidate.env_var is None:
|
|
156
|
+
continue
|
|
157
|
+
_DISCOVERED_MMS[key] = existing = candidate
|
|
158
|
+
else:
|
|
159
|
+
# enrich cached entry with flags learned from other discovery sources
|
|
160
|
+
existing.merge_from(candidate)
|
|
161
|
+
|
|
162
|
+
if key not in yielded: # never double yield
|
|
163
|
+
yielded.add(key)
|
|
164
|
+
yield existing
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
@overload
|
|
168
|
+
def find_micromanager(return_first: Literal[True] = ...) -> str | None: ...
|
|
169
|
+
@overload
|
|
170
|
+
def find_micromanager(return_first: Literal[False]) -> list[str]: ...
|
|
171
|
+
def find_micromanager(return_first: bool = True) -> str | None | list[str]:
|
|
172
|
+
r"""Locate a Micro-Manager folder (for device adapters).
|
|
173
|
+
|
|
174
|
+
In order, this will look for:
|
|
175
|
+
|
|
176
|
+
1. An environment variable named `MICROMANAGER_PATH`
|
|
177
|
+
2. A path stored in the `CURRENT_MM_PATH` file (set by `use_micromanager`).
|
|
178
|
+
3. A `Micro-Manager*` folder in the `pymmcore-plus` user data directory
|
|
179
|
+
(this is the default install location when running `mmcore install`)
|
|
180
|
+
|
|
181
|
+
- **Windows**: C:\Users\\[user]\AppData\Local\pymmcore-plus\pymmcore-plus
|
|
182
|
+
- **macOS**: ~/Library/Application Support/pymmcore-plus
|
|
183
|
+
- **Linux**: ~/.local/share/pymmcore-plus
|
|
184
|
+
|
|
185
|
+
4. A `Micro-Manager*` folder in the `pymmcore_plus` package directory (this is the
|
|
186
|
+
default install location when running `python -m pymmcore_plus.install`)
|
|
187
|
+
5. The default micro-manager install location:
|
|
188
|
+
|
|
189
|
+
- **Windows**: `C:/Program Files/`
|
|
190
|
+
- **macOS**: `/Applications`
|
|
191
|
+
- **Linux**: `/usr/local/lib`
|
|
192
|
+
|
|
193
|
+
!!! note
|
|
194
|
+
|
|
195
|
+
This function is used by [`pymmcore_plus.CMMCorePlus`][] to locate the
|
|
196
|
+
micro-manager device adapters. By default, the output of this function
|
|
197
|
+
is passed to
|
|
198
|
+
[`setDeviceAdapterSearchPaths`][pymmcore_plus.CMMCorePlus.setDeviceAdapterSearchPaths]
|
|
199
|
+
when creating a new `CMMCorePlus` instance.
|
|
200
|
+
|
|
201
|
+
Parameters
|
|
202
|
+
----------
|
|
203
|
+
return_first : bool, optional
|
|
204
|
+
If True (default), return the first found path. If False, return a list of
|
|
205
|
+
all found paths.
|
|
206
|
+
"""
|
|
207
|
+
from ._logger import logger
|
|
208
|
+
|
|
209
|
+
for discovered_mm in discover_mm():
|
|
210
|
+
if return_first:
|
|
211
|
+
# If the path was explicitly provided via the MICROMANAGER_PATH
|
|
212
|
+
# environment variable, prefer it even if it doesn't currently
|
|
213
|
+
# contain device adapter libraries. This allows users (and tests)
|
|
214
|
+
# to point to a path that will be used as-is.
|
|
215
|
+
if discovered_mm.env_var is not None:
|
|
216
|
+
logger.debug(
|
|
217
|
+
"Using Micro-Manager path from %s: %s",
|
|
218
|
+
discovered_mm.env_var,
|
|
219
|
+
discovered_mm.path,
|
|
220
|
+
)
|
|
221
|
+
return str(discovered_mm.path)
|
|
222
|
+
|
|
223
|
+
if discovered_mm.div_compatible:
|
|
224
|
+
logger.debug(
|
|
225
|
+
"Using Micro-Manager path: %s (device interface %s)",
|
|
226
|
+
discovered_mm.path,
|
|
227
|
+
discovered_mm.device_interface,
|
|
228
|
+
)
|
|
229
|
+
return str(discovered_mm.path)
|
|
230
|
+
elif discovered_mm.is_current:
|
|
231
|
+
warnings.warn(
|
|
232
|
+
f"The current user-selected version of Micro-Manager at "
|
|
233
|
+
f"{discovered_mm.path} has an incompatible device interface "
|
|
234
|
+
f"({discovered_mm.device_interface}). The installed version "
|
|
235
|
+
f"of pymmcore requires: {PYMMCORE_DIV}). Clearing.",
|
|
236
|
+
stacklevel=2,
|
|
237
|
+
)
|
|
238
|
+
CURRENT_MM_PATH.unlink(missing_ok=True)
|
|
239
|
+
object.__setattr__(discovered_mm, "is_current", False)
|
|
240
|
+
|
|
241
|
+
if return_first:
|
|
242
|
+
# if we got here it means no compatible version was found
|
|
243
|
+
|
|
244
|
+
others = "\n".join(
|
|
245
|
+
[str(d.path) for d in _DISCOVERED_MMS.values() if not d.div_compatible]
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
logger.error(
|
|
249
|
+
f"Could not find a compatible Micro-Manager installation for the "
|
|
250
|
+
f"device interface required by pymmcore {PYMMCORE_DIV}.\n\n"
|
|
251
|
+
f"Discovered installations:\n"
|
|
252
|
+
f"{others}\n"
|
|
253
|
+
f"Please run 'mmcore install'."
|
|
254
|
+
)
|
|
255
|
+
return None
|
|
256
|
+
|
|
257
|
+
return [str(d.path) for d in _DISCOVERED_MMS.values()]
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _match_mm_pattern(pattern: str | re.Pattern[str]) -> Path | None:
|
|
261
|
+
"""Locate an existing Micro-Manager folder using a regex pattern."""
|
|
262
|
+
for _path in find_micromanager(return_first=False):
|
|
263
|
+
if not isinstance(pattern, re.Pattern):
|
|
264
|
+
pattern = str(pattern)
|
|
265
|
+
if re.search(pattern, _path) is not None:
|
|
266
|
+
return Path(_path)
|
|
267
|
+
return None
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def use_micromanager(
|
|
271
|
+
path: str | Path | None = None, pattern: str | re.Pattern[str] | None = None
|
|
272
|
+
) -> Path | None:
|
|
273
|
+
"""Set the preferred Micro-Manager path.
|
|
274
|
+
|
|
275
|
+
This sets the preferred micromanager path, and persists across sessions.
|
|
276
|
+
This path takes precedence over everything *except* the `MICROMANAGER_PATH`
|
|
277
|
+
environment variable.
|
|
278
|
+
|
|
279
|
+
Parameters
|
|
280
|
+
----------
|
|
281
|
+
path : str | Path | None
|
|
282
|
+
Path to an existing directory. This directory should contain micro-manager
|
|
283
|
+
device adapters. If `None`, the path will be determined using `pattern`.
|
|
284
|
+
pattern : str Pattern | | None
|
|
285
|
+
A regex pattern to match against the micromanager paths found by
|
|
286
|
+
`find_micromanager`. If no match is found, a `FileNotFoundError` will be raised.
|
|
287
|
+
"""
|
|
288
|
+
if path is None:
|
|
289
|
+
if pattern is None: # pragma: no cover
|
|
290
|
+
raise ValueError("One of 'path' or 'pattern' must be provided")
|
|
291
|
+
if (path := _match_mm_pattern(pattern)) is None:
|
|
292
|
+
options = "\n".join(find_micromanager(return_first=False))
|
|
293
|
+
raise FileNotFoundError(
|
|
294
|
+
f"No micromanager path found matching: {pattern!r}. Options:\n{options}"
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
if not isinstance(path, Path): # pragma: no cover
|
|
298
|
+
path = Path(path)
|
|
299
|
+
|
|
300
|
+
path = path.expanduser().resolve()
|
|
301
|
+
if not path.is_dir(): # pragma: no cover
|
|
302
|
+
if not path.exists():
|
|
303
|
+
raise FileNotFoundError(f"Path not found: {path!r}")
|
|
304
|
+
raise NotADirectoryError(f"Not a directory: {path!r}")
|
|
305
|
+
|
|
306
|
+
USER_DATA_MM_PATH.mkdir(parents=True, exist_ok=True)
|
|
307
|
+
CURRENT_MM_PATH.write_text(str(path))
|
|
308
|
+
return path
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _iter_device_paths(folder: Path) -> Iterator[Path]:
|
|
312
|
+
"""Iterate over device shared library paths in `folder`."""
|
|
313
|
+
valid_extensions = {".dll", ".0", "", ".so", ".dylib"}
|
|
314
|
+
for lib_path in folder.glob("*mmgr_dal*"):
|
|
315
|
+
if lib_path.suffix in valid_extensions:
|
|
316
|
+
yield lib_path
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def get_first_device_interface_version(folder: Path | str) -> int | None:
|
|
320
|
+
for dev_path in _iter_device_paths(Path(folder)):
|
|
321
|
+
try:
|
|
322
|
+
return get_device_interface_version(dev_path)
|
|
323
|
+
except Exception:
|
|
324
|
+
continue
|
|
325
|
+
return None
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def get_device_interface_version(lib_path: str | Path) -> int:
|
|
329
|
+
"""Return the device interface version from the given library path."""
|
|
330
|
+
if sys.platform.startswith("win"):
|
|
331
|
+
lib = ctypes.WinDLL(str(lib_path))
|
|
332
|
+
else:
|
|
333
|
+
lib = ctypes.CDLL(str(lib_path))
|
|
334
|
+
|
|
335
|
+
try:
|
|
336
|
+
func = lib.GetDeviceInterfaceVersion
|
|
337
|
+
except AttributeError:
|
|
338
|
+
raise RuntimeError(
|
|
339
|
+
f"Function 'GetDeviceInterfaceVersion' not found in {lib_path}"
|
|
340
|
+
) from None
|
|
341
|
+
|
|
342
|
+
func.restype = ctypes.c_long
|
|
343
|
+
func.argtypes = []
|
|
344
|
+
return func() # type: ignore[no-any-return]
|
|
@@ -24,7 +24,7 @@ if "PYTEST_RUNNING" in os.environ:
|
|
|
24
24
|
elif PYMM_LOG_FILE not in ("", "0", "false", "no", "none"):
|
|
25
25
|
LOG_FILE = Path(PYMM_LOG_FILE).expanduser().resolve()
|
|
26
26
|
else:
|
|
27
|
-
from .
|
|
27
|
+
from ._discovery import USER_DATA_DIR
|
|
28
28
|
|
|
29
29
|
LOG_FILE = USER_DATA_DIR / "logs" / "pymmcore-plus.log"
|
|
30
30
|
|