pymmcore-plus 0.15.4__py3-none-any.whl → 0.17.0__py3-none-any.whl
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/__init__.py +20 -1
- pymmcore_plus/_accumulator.py +23 -5
- pymmcore_plus/_cli.py +44 -26
- pymmcore_plus/_discovery.py +344 -0
- pymmcore_plus/_ipy_completion.py +1 -1
- pymmcore_plus/_logger.py +3 -3
- pymmcore_plus/_util.py +9 -245
- pymmcore_plus/core/_device.py +57 -13
- pymmcore_plus/core/_mmcore_plus.py +20 -23
- pymmcore_plus/core/_property.py +35 -29
- pymmcore_plus/core/_sequencing.py +2 -0
- pymmcore_plus/core/events/_device_signal_view.py +8 -1
- pymmcore_plus/experimental/simulate/__init__.py +88 -0
- pymmcore_plus/experimental/simulate/_objects.py +670 -0
- pymmcore_plus/experimental/simulate/_render.py +510 -0
- pymmcore_plus/experimental/simulate/_sample.py +156 -0
- pymmcore_plus/experimental/unicore/__init__.py +2 -0
- pymmcore_plus/experimental/unicore/_device_manager.py +46 -13
- pymmcore_plus/experimental/unicore/core/_config.py +706 -0
- pymmcore_plus/experimental/unicore/core/_unicore.py +834 -18
- pymmcore_plus/experimental/unicore/devices/_device_base.py +13 -0
- pymmcore_plus/experimental/unicore/devices/_hub.py +50 -0
- pymmcore_plus/experimental/unicore/devices/_stage.py +46 -1
- pymmcore_plus/experimental/unicore/devices/_state.py +6 -0
- pymmcore_plus/install.py +149 -18
- pymmcore_plus/mda/_engine.py +268 -73
- pymmcore_plus/mda/handlers/_5d_writer_base.py +16 -5
- pymmcore_plus/mda/handlers/_tensorstore_handler.py +7 -1
- pymmcore_plus/metadata/_ome.py +553 -0
- pymmcore_plus/metadata/functions.py +2 -1
- {pymmcore_plus-0.15.4.dist-info → pymmcore_plus-0.17.0.dist-info}/METADATA +7 -4
- {pymmcore_plus-0.15.4.dist-info → pymmcore_plus-0.17.0.dist-info}/RECORD +35 -27
- {pymmcore_plus-0.15.4.dist-info → pymmcore_plus-0.17.0.dist-info}/WHEEL +1 -1
- {pymmcore_plus-0.15.4.dist-info → pymmcore_plus-0.17.0.dist-info}/entry_points.txt +0 -0
- {pymmcore_plus-0.15.4.dist-info → pymmcore_plus-0.17.0.dist-info}/licenses/LICENSE +0 -0
pymmcore_plus/_util.py
CHANGED
|
@@ -2,9 +2,9 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import datetime
|
|
4
4
|
import importlib
|
|
5
|
+
import importlib.metadata
|
|
5
6
|
import os
|
|
6
7
|
import platform
|
|
7
|
-
import re
|
|
8
8
|
import sys
|
|
9
9
|
import warnings
|
|
10
10
|
from collections import defaultdict
|
|
@@ -17,9 +17,10 @@ from typing import TYPE_CHECKING, cast, overload
|
|
|
17
17
|
|
|
18
18
|
from platformdirs import user_data_dir
|
|
19
19
|
|
|
20
|
+
from . import _discovery
|
|
21
|
+
|
|
20
22
|
if TYPE_CHECKING:
|
|
21
23
|
from collections.abc import Iterator
|
|
22
|
-
from re import Pattern
|
|
23
24
|
from typing import Any, Callable, Literal, TypeVar
|
|
24
25
|
|
|
25
26
|
QtConnectionType = Literal["AutoConnection", "DirectConnection", "QueuedConnection"]
|
|
@@ -39,213 +40,14 @@ except ImportError:
|
|
|
39
40
|
from contextlib import nullcontext as no_stdout
|
|
40
41
|
|
|
41
42
|
|
|
42
|
-
__all__ = ["
|
|
43
|
+
__all__ = ["no_stdout", "retry", "signals_backend"]
|
|
43
44
|
|
|
44
45
|
APP_NAME = "pymmcore-plus"
|
|
45
46
|
USER_DATA_DIR = Path(user_data_dir(appname=APP_NAME))
|
|
46
47
|
USER_DATA_MM_PATH = USER_DATA_DIR / "mm"
|
|
47
48
|
CURRENT_MM_PATH = USER_DATA_MM_PATH / ".current_mm"
|
|
48
49
|
PYMMCORE_PLUS_PATH = Path(__file__).parent.parent
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
@overload
|
|
52
|
-
def find_micromanager(return_first: Literal[True] = True) -> str | None: ...
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
@overload
|
|
56
|
-
def find_micromanager(return_first: Literal[False]) -> list[str]: ...
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
def find_micromanager(return_first: bool = True) -> str | None | list[str]:
|
|
60
|
-
r"""Locate a Micro-Manager folder (for device adapters).
|
|
61
|
-
|
|
62
|
-
In order, this will look for:
|
|
63
|
-
|
|
64
|
-
1. An environment variable named `MICROMANAGER_PATH`
|
|
65
|
-
2. A path stored in the `CURRENT_MM_PATH` file (set by `use_micromanager`).
|
|
66
|
-
3. A `Micro-Manager*` folder in the `pymmcore-plus` user data directory
|
|
67
|
-
(this is the default install location when running `mmcore install`)
|
|
68
|
-
|
|
69
|
-
- **Windows**: C:\Users\\[user]\AppData\Local\pymmcore-plus\pymmcore-plus
|
|
70
|
-
- **macOS**: ~/Library/Application Support/pymmcore-plus
|
|
71
|
-
- **Linux**: ~/.local/share/pymmcore-plus
|
|
72
|
-
|
|
73
|
-
4. A `Micro-Manager*` folder in the `pymmcore_plus` package directory (this is the
|
|
74
|
-
default install location when running `python -m pymmcore_plus.install`)
|
|
75
|
-
5. The default micro-manager install location:
|
|
76
|
-
|
|
77
|
-
- **Windows**: `C:/Program Files/`
|
|
78
|
-
- **macOS**: `/Applications`
|
|
79
|
-
- **Linux**: `/usr/local/lib`
|
|
80
|
-
|
|
81
|
-
!!! note
|
|
82
|
-
|
|
83
|
-
This function is used by [`pymmcore_plus.CMMCorePlus`][] to locate the
|
|
84
|
-
micro-manager device adapters. By default, the output of this function
|
|
85
|
-
is passed to
|
|
86
|
-
[`setDeviceAdapterSearchPaths`][pymmcore_plus.CMMCorePlus.setDeviceAdapterSearchPaths]
|
|
87
|
-
when creating a new `CMMCorePlus` instance.
|
|
88
|
-
|
|
89
|
-
Parameters
|
|
90
|
-
----------
|
|
91
|
-
return_first : bool, optional
|
|
92
|
-
If True (default), return the first found path. If False, return a list of
|
|
93
|
-
all found paths.
|
|
94
|
-
"""
|
|
95
|
-
from ._logger import logger
|
|
96
|
-
|
|
97
|
-
# we use a dict here to avoid duplicates, while retaining order
|
|
98
|
-
full_list: dict[str, None] = {}
|
|
99
|
-
|
|
100
|
-
# environment variable takes precedence
|
|
101
|
-
env_path = os.getenv("MICROMANAGER_PATH")
|
|
102
|
-
if env_path and os.path.isdir(env_path):
|
|
103
|
-
if return_first:
|
|
104
|
-
logger.debug("using MM path from env var: %s", env_path)
|
|
105
|
-
return env_path
|
|
106
|
-
full_list[env_path] = None
|
|
107
|
-
|
|
108
|
-
# then check for a path in CURRENT_MM_PATH
|
|
109
|
-
if CURRENT_MM_PATH.exists():
|
|
110
|
-
path = CURRENT_MM_PATH.read_text().strip()
|
|
111
|
-
if os.path.isdir(path):
|
|
112
|
-
if return_first:
|
|
113
|
-
logger.debug("using MM path from current_mm: %s", path)
|
|
114
|
-
return path
|
|
115
|
-
full_list[path] = None
|
|
116
|
-
|
|
117
|
-
# then look for mm-device-adapters
|
|
118
|
-
with suppress(ImportError):
|
|
119
|
-
import mm_device_adapters
|
|
120
|
-
|
|
121
|
-
from . import _pymmcore
|
|
122
|
-
|
|
123
|
-
mm_dev_div = mm_device_adapters.__version__.split(".")[0]
|
|
124
|
-
pymm_div = str(_pymmcore.version_info.device_interface)
|
|
125
|
-
|
|
126
|
-
if pymm_div != mm_dev_div: # pragma: no cover
|
|
127
|
-
warnings.warn(
|
|
128
|
-
"mm-device-adapters installed, but its device interface "
|
|
129
|
-
f"version ({mm_dev_div}) "
|
|
130
|
-
f"does not match the device interface version of {_pymmcore.BACKEND}"
|
|
131
|
-
f"({pymm_div}). You may wish to run"
|
|
132
|
-
f" `pip install --force-reinstall mm-device-adapters=={pymm_div}`. "
|
|
133
|
-
"mm-device-adapters will be ignored.",
|
|
134
|
-
stacklevel=2,
|
|
135
|
-
)
|
|
136
|
-
else:
|
|
137
|
-
path = mm_device_adapters.device_adapter_path()
|
|
138
|
-
if return_first:
|
|
139
|
-
logger.debug("using MM path from mm-device-adapters: %s", path)
|
|
140
|
-
return str(path)
|
|
141
|
-
full_list[path] = None
|
|
142
|
-
|
|
143
|
-
# then look in user_data_dir
|
|
144
|
-
_folders = (p for p in USER_DATA_MM_PATH.glob("Micro-Manager*") if p.is_dir())
|
|
145
|
-
if user_install := sorted(_folders, reverse=True):
|
|
146
|
-
if return_first and (
|
|
147
|
-
first := next(
|
|
148
|
-
(x for x in user_install if _mm_path_has_compatible_div(x)), None
|
|
149
|
-
)
|
|
150
|
-
):
|
|
151
|
-
logger.debug("using MM path from user install: %s", first)
|
|
152
|
-
return str(first)
|
|
153
|
-
for x in user_install:
|
|
154
|
-
full_list[str(x)] = None
|
|
155
|
-
|
|
156
|
-
# then look for an installation in this folder (from `pymmcore_plus.install`)
|
|
157
|
-
sfx = "_win" if os.name == "nt" else "_mac"
|
|
158
|
-
local_install = [
|
|
159
|
-
p for p in PYMMCORE_PLUS_PATH.glob(f"**/Micro-Manager*{sfx}") if p.is_dir()
|
|
160
|
-
]
|
|
161
|
-
if local_install:
|
|
162
|
-
if return_first and (
|
|
163
|
-
first := next(
|
|
164
|
-
(x for x in local_install if _mm_path_has_compatible_div(x)), None
|
|
165
|
-
)
|
|
166
|
-
): # pragma: no cover
|
|
167
|
-
logger.debug("using MM path from local install: %s", first)
|
|
168
|
-
return str(first)
|
|
169
|
-
for x in local_install:
|
|
170
|
-
full_list[str(x)] = None
|
|
171
|
-
|
|
172
|
-
applications = {
|
|
173
|
-
"darwin": Path("/Applications/"),
|
|
174
|
-
"win32": Path("C:/Program Files/"),
|
|
175
|
-
"linux": Path("/usr/local/lib"),
|
|
176
|
-
}
|
|
177
|
-
if sys.platform not in applications:
|
|
178
|
-
raise NotImplementedError(
|
|
179
|
-
f"MM autodiscovery not implemented for platform: {sys.platform}"
|
|
180
|
-
)
|
|
181
|
-
app_path = applications[sys.platform]
|
|
182
|
-
pth = next(app_path.glob("[m,M]icro-[m,M]anager*"), None)
|
|
183
|
-
if return_first:
|
|
184
|
-
if pth and _mm_path_has_compatible_div(pth): # pragma: no cover
|
|
185
|
-
logger.debug("using MM path found in applications: %s", pth)
|
|
186
|
-
return str(pth)
|
|
187
|
-
from . import _pymmcore
|
|
188
|
-
|
|
189
|
-
div = _pymmcore.version_info.device_interface
|
|
190
|
-
logger.error(
|
|
191
|
-
f"could not find micromanager directory for device interface {div}. "
|
|
192
|
-
"Please run 'mmcore install'"
|
|
193
|
-
)
|
|
194
|
-
return None
|
|
195
|
-
if pth is not None:
|
|
196
|
-
full_list[str(pth)] = None
|
|
197
|
-
return list(full_list)
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
def _match_mm_pattern(pattern: str | Pattern[str]) -> Path | None:
|
|
201
|
-
"""Locate an existing Micro-Manager folder using a regex pattern."""
|
|
202
|
-
for _path in find_micromanager(return_first=False):
|
|
203
|
-
if not isinstance(pattern, re.Pattern):
|
|
204
|
-
pattern = str(pattern)
|
|
205
|
-
if re.search(pattern, _path) is not None:
|
|
206
|
-
return Path(_path)
|
|
207
|
-
return None
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
def use_micromanager(
|
|
211
|
-
path: str | Path | None = None, pattern: str | Pattern[str] | None = None
|
|
212
|
-
) -> Path | None:
|
|
213
|
-
"""Set the preferred Micro-Manager path.
|
|
214
|
-
|
|
215
|
-
This sets the preferred micromanager path, and persists across sessions.
|
|
216
|
-
This path takes precedence over everything *except* the `MICROMANAGER_PATH`
|
|
217
|
-
environment variable.
|
|
218
|
-
|
|
219
|
-
Parameters
|
|
220
|
-
----------
|
|
221
|
-
path : str | Path | None
|
|
222
|
-
Path to an existing directory. This directory should contain micro-manager
|
|
223
|
-
device adapters. If `None`, the path will be determined using `pattern`.
|
|
224
|
-
pattern : str Pattern | | None
|
|
225
|
-
A regex pattern to match against the micromanager paths found by
|
|
226
|
-
`find_micromanager`. If no match is found, a `FileNotFoundError` will be raised.
|
|
227
|
-
"""
|
|
228
|
-
if path is None:
|
|
229
|
-
if pattern is None: # pragma: no cover
|
|
230
|
-
raise ValueError("One of 'path' or 'pattern' must be provided")
|
|
231
|
-
if (path := _match_mm_pattern(pattern)) is None:
|
|
232
|
-
options = "\n".join(find_micromanager(return_first=False))
|
|
233
|
-
raise FileNotFoundError(
|
|
234
|
-
f"No micromanager path found matching: {pattern!r}. Options:\n{options}"
|
|
235
|
-
)
|
|
236
|
-
|
|
237
|
-
if not isinstance(path, Path): # pragma: no cover
|
|
238
|
-
path = Path(path)
|
|
239
|
-
|
|
240
|
-
path = path.expanduser().resolve()
|
|
241
|
-
if not path.is_dir(): # pragma: no cover
|
|
242
|
-
if not path.exists():
|
|
243
|
-
raise FileNotFoundError(f"Path not found: {path!r}")
|
|
244
|
-
raise NotADirectoryError(f"Not a directory: {path!r}")
|
|
245
|
-
|
|
246
|
-
USER_DATA_MM_PATH.mkdir(parents=True, exist_ok=True)
|
|
247
|
-
CURRENT_MM_PATH.write_text(str(path))
|
|
248
|
-
return path
|
|
50
|
+
PYMM_SIGNALS_BACKEND = "PYMM_SIGNALS_BACKEND"
|
|
249
51
|
|
|
250
52
|
|
|
251
53
|
def _imported_qt_modules() -> Iterator[str]:
|
|
@@ -266,9 +68,6 @@ def _qt_app_is_running() -> bool:
|
|
|
266
68
|
return False # pragma: no cover
|
|
267
69
|
|
|
268
70
|
|
|
269
|
-
PYMM_SIGNALS_BACKEND = "PYMM_SIGNALS_BACKEND"
|
|
270
|
-
|
|
271
|
-
|
|
272
71
|
def signals_backend() -> Literal["qt", "psygnal"]:
|
|
273
72
|
"""Return the name of the event backend to use."""
|
|
274
73
|
env_var = os.environ.get(PYMM_SIGNALS_BACKEND, "auto").lower()
|
|
@@ -302,8 +101,6 @@ def retry(
|
|
|
302
101
|
delay: float | None = ...,
|
|
303
102
|
logger: Callable[[str], Any] | None = ...,
|
|
304
103
|
) -> Callable[[Callable[P, R]], Callable[P, R]]: ...
|
|
305
|
-
|
|
306
|
-
|
|
307
104
|
@overload
|
|
308
105
|
def retry(
|
|
309
106
|
func: Callable[P, R],
|
|
@@ -312,8 +109,6 @@ def retry(
|
|
|
312
109
|
delay: float | None = ...,
|
|
313
110
|
logger: Callable[[str], Any] | None = ...,
|
|
314
111
|
) -> Callable[P, R]: ...
|
|
315
|
-
|
|
316
|
-
|
|
317
112
|
def retry(
|
|
318
113
|
func: Callable[P, R] | None = None,
|
|
319
114
|
tries: int = 3,
|
|
@@ -609,7 +404,7 @@ def system_info() -> dict[str, str]:
|
|
|
609
404
|
info["core-version-info"] = core.getVersionInfo()
|
|
610
405
|
info["api-version-info"] = core.getAPIVersionInfo()
|
|
611
406
|
|
|
612
|
-
if (mm_path := find_micromanager()) is not None:
|
|
407
|
+
if (mm_path := _discovery.find_micromanager()) is not None:
|
|
613
408
|
path = str(Path(mm_path).resolve())
|
|
614
409
|
path = path.replace(os.path.expanduser("~"), "~") # privacy
|
|
615
410
|
info["adapter-path"] = path
|
|
@@ -628,10 +423,10 @@ def system_info() -> dict[str, str]:
|
|
|
628
423
|
info[pkg] = importlib.metadata.version(pkg)
|
|
629
424
|
|
|
630
425
|
if pkg == "pymmcore-widgets":
|
|
631
|
-
with suppress(ImportError):
|
|
632
|
-
|
|
426
|
+
with suppress(ImportError, AttributeError):
|
|
427
|
+
import qtpy
|
|
633
428
|
|
|
634
|
-
info["qt"] = f"{API_NAME} {QT_VERSION}"
|
|
429
|
+
info["qt"] = f"{qtpy.API_NAME} {qtpy.QT_VERSION}"
|
|
635
430
|
|
|
636
431
|
return info
|
|
637
432
|
|
|
@@ -655,34 +450,3 @@ def timestamp() -> str:
|
|
|
655
450
|
with suppress(Exception):
|
|
656
451
|
now = now.astimezone()
|
|
657
452
|
return now.isoformat()
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
def get_device_interface_version(lib_path: str | Path) -> int:
|
|
661
|
-
"""Return the device interface version from the given library path."""
|
|
662
|
-
import ctypes
|
|
663
|
-
|
|
664
|
-
if sys.platform.startswith("win"):
|
|
665
|
-
lib = ctypes.WinDLL(str(lib_path))
|
|
666
|
-
else:
|
|
667
|
-
lib = ctypes.CDLL(str(lib_path))
|
|
668
|
-
|
|
669
|
-
try:
|
|
670
|
-
func = lib.GetDeviceInterfaceVersion
|
|
671
|
-
except AttributeError:
|
|
672
|
-
raise RuntimeError(
|
|
673
|
-
f"Function 'GetDeviceInterfaceVersion' not found in {lib_path}"
|
|
674
|
-
) from None
|
|
675
|
-
|
|
676
|
-
func.restype = ctypes.c_long
|
|
677
|
-
func.argtypes = []
|
|
678
|
-
return func() # type: ignore[no-any-return]
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
def _mm_path_has_compatible_div(folder: Path | str) -> bool:
|
|
682
|
-
from . import _pymmcore
|
|
683
|
-
|
|
684
|
-
div = _pymmcore.version_info.device_interface
|
|
685
|
-
for lib_path in Path(folder).glob("*mmgr_dal*"):
|
|
686
|
-
with suppress(Exception):
|
|
687
|
-
return get_device_interface_version(lib_path) == div
|
|
688
|
-
return False # pragma: no cover
|
pymmcore_plus/core/_device.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import weakref
|
|
3
4
|
from typing import TYPE_CHECKING, Any, Literal, overload
|
|
4
5
|
|
|
5
6
|
from ._constants import DeviceType, FocusDirection, Keyword
|
|
@@ -34,6 +35,12 @@ class Device:
|
|
|
34
35
|
Device label assigned to this device.
|
|
35
36
|
mmcore : CMMCorePlus
|
|
36
37
|
CMMCorePlus instance that owns this device.
|
|
38
|
+
device_type : DeviceType or Device subclass, optional
|
|
39
|
+
The type of device to create. If not specified, the type will be inferred
|
|
40
|
+
from the core if the device is already loaded. If the device is not loaded,
|
|
41
|
+
an error will be raised. This parameter is mainly intended for usage when
|
|
42
|
+
calling from `CMMCorePlus.getDeviceObject()`. Otherwise, prefer using
|
|
43
|
+
`[SpecificDeviceSubclass].create()`.
|
|
37
44
|
|
|
38
45
|
Examples
|
|
39
46
|
--------
|
|
@@ -56,14 +63,39 @@ class Device:
|
|
|
56
63
|
propertyChanged: PSignalInstance
|
|
57
64
|
|
|
58
65
|
@classmethod
|
|
59
|
-
def create(
|
|
60
|
-
|
|
66
|
+
def create(
|
|
67
|
+
cls,
|
|
68
|
+
device_label: str,
|
|
69
|
+
mmcore: CMMCorePlus,
|
|
70
|
+
device_type: type[Device] | DeviceType = DeviceType.Any,
|
|
71
|
+
) -> Self:
|
|
72
|
+
if device_type in {DeviceType.Any, DeviceType.Unknown}:
|
|
73
|
+
try:
|
|
74
|
+
sub_cls = cls.get_subclass(device_label, mmcore)
|
|
75
|
+
except RuntimeError as e:
|
|
76
|
+
raise RuntimeError(
|
|
77
|
+
f"Could not determine device type for {device_label}. "
|
|
78
|
+
"If you are preloading a device object, "
|
|
79
|
+
"please specify `device_type` as a `pymmcore_plus.DeviceType`."
|
|
80
|
+
) from e
|
|
81
|
+
else:
|
|
82
|
+
if isinstance(device_type, type) and issubclass(device_type, Device):
|
|
83
|
+
sub_cls = device_type
|
|
84
|
+
elif isinstance(device_type, DeviceType):
|
|
85
|
+
sub_cls = _TYPE_MAP[device_type]
|
|
86
|
+
else:
|
|
87
|
+
raise TypeError(
|
|
88
|
+
f"Invalid device_type: {device_type!r}. Must be a "
|
|
89
|
+
"pymmcore_plus `DeviceType` or `Device` subclass."
|
|
90
|
+
)
|
|
91
|
+
|
|
61
92
|
# make sure it's an error to call this class method on a subclass with
|
|
62
93
|
# a non-matching type
|
|
63
|
-
if issubclass(sub_cls, cls):
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
94
|
+
if not issubclass(sub_cls, cls):
|
|
95
|
+
dev_type = mmcore.getDeviceType(device_label).name
|
|
96
|
+
raise TypeError(f"Cannot cast {dev_type} {device_label!r} to {cls}")
|
|
97
|
+
|
|
98
|
+
return sub_cls(device_label, mmcore)
|
|
67
99
|
|
|
68
100
|
@classmethod
|
|
69
101
|
def get_subclass(cls, device_label: str, mmcore: CMMCorePlus) -> type[Device]:
|
|
@@ -82,17 +114,18 @@ class Device:
|
|
|
82
114
|
if mmcore is None:
|
|
83
115
|
from ._mmcore_plus import CMMCorePlus
|
|
84
116
|
|
|
85
|
-
|
|
117
|
+
mmcore = CMMCorePlus.instance()
|
|
86
118
|
else:
|
|
87
|
-
|
|
119
|
+
mmcore = mmcore
|
|
88
120
|
|
|
121
|
+
self._mmc_ref = weakref.ref(mmcore)
|
|
89
122
|
self._label = device_label
|
|
90
123
|
self._type = None
|
|
91
124
|
if self.isLoaded():
|
|
92
|
-
adapter_name =
|
|
93
|
-
device_name =
|
|
94
|
-
description =
|
|
95
|
-
type =
|
|
125
|
+
adapter_name = mmcore.getDeviceLibrary(device_label)
|
|
126
|
+
device_name = mmcore.getDeviceName(device_label)
|
|
127
|
+
description = mmcore.getDeviceDescription(device_label)
|
|
128
|
+
type = mmcore.getDeviceType(device_label) # noqa: A001
|
|
96
129
|
if self.type() != type:
|
|
97
130
|
raise TypeError(
|
|
98
131
|
f"Cannot create loaded device with label {device_label!r} and type "
|
|
@@ -103,7 +136,18 @@ class Device:
|
|
|
103
136
|
self._device_name = device_name
|
|
104
137
|
self._type = type
|
|
105
138
|
self._description = description
|
|
106
|
-
self.propertyChanged = _DevicePropValueSignal(device_label, None,
|
|
139
|
+
self.propertyChanged = _DevicePropValueSignal(device_label, None, mmcore)
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def _mmc(self) -> CMMCorePlus:
|
|
143
|
+
"""Return the `CMMCorePlus` instance to which this Device is bound."""
|
|
144
|
+
mmc = self._mmc_ref()
|
|
145
|
+
if mmc is None: # pragma: no cover
|
|
146
|
+
raise RuntimeError(
|
|
147
|
+
"The CMMCorePlus instance to which this Device "
|
|
148
|
+
"is bound has been garbage collected."
|
|
149
|
+
)
|
|
150
|
+
return mmc
|
|
107
151
|
|
|
108
152
|
@property
|
|
109
153
|
def label(self) -> str:
|
|
@@ -27,8 +27,9 @@ from psygnal import SignalInstance
|
|
|
27
27
|
from typing_extensions import deprecated
|
|
28
28
|
|
|
29
29
|
import pymmcore_plus._pymmcore as pymmcore
|
|
30
|
+
from pymmcore_plus._discovery import find_micromanager
|
|
30
31
|
from pymmcore_plus._logger import current_logfile, logger
|
|
31
|
-
from pymmcore_plus._util import
|
|
32
|
+
from pymmcore_plus._util import print_tabular_data
|
|
32
33
|
from pymmcore_plus.mda import MDAEngine, MDARunner, PMDAEngine
|
|
33
34
|
from pymmcore_plus.metadata.functions import summary_metadata
|
|
34
35
|
|
|
@@ -334,8 +335,10 @@ class CMMCorePlus(pymmcore.CMMCore):
|
|
|
334
335
|
def __del__(self) -> None:
|
|
335
336
|
if hasattr(self, "_weak_clean"):
|
|
336
337
|
atexit.unregister(self._weak_clean)
|
|
338
|
+
|
|
337
339
|
try:
|
|
338
340
|
super().registerCallback(None)
|
|
341
|
+
|
|
339
342
|
self.reset()
|
|
340
343
|
# clean up logging
|
|
341
344
|
self.setPrimaryLogFile("")
|
|
@@ -517,7 +520,12 @@ class CMMCorePlus(pymmcore.CMMCore):
|
|
|
517
520
|
**Why Override?** The returned [`pymmcore_plus.FocusDirection`][] enum is more
|
|
518
521
|
interpretable than the raw `int` returned by `pymmcore`
|
|
519
522
|
"""
|
|
520
|
-
|
|
523
|
+
try:
|
|
524
|
+
return FocusDirection(super().getFocusDirection(stageLabel))
|
|
525
|
+
except (ValueError, TypeError):
|
|
526
|
+
# On some platforms (notably Windows), device adapters may return
|
|
527
|
+
# invalid values that cannot be converted to FocusDirection enum.
|
|
528
|
+
return FocusDirection.Unknown
|
|
521
529
|
|
|
522
530
|
def getPropertyType(self, label: str, propName: str) -> PropertyType:
|
|
523
531
|
"""Return the intrinsic property type for a given device and property.
|
|
@@ -1260,18 +1268,7 @@ class CMMCorePlus(pymmcore.CMMCore):
|
|
|
1260
1268
|
}
|
|
1261
1269
|
}
|
|
1262
1270
|
"""
|
|
1263
|
-
|
|
1264
|
-
if (isinstance(device_type, type) and not isinstance(dev, device_type)) or (
|
|
1265
|
-
isinstance(device_type, DeviceType)
|
|
1266
|
-
and device_type not in {DeviceType.Any, DeviceType.Unknown}
|
|
1267
|
-
and dev.type() != device_type
|
|
1268
|
-
):
|
|
1269
|
-
raise TypeError(
|
|
1270
|
-
f"{device_type!r} requested but device with label "
|
|
1271
|
-
f"{device_label!r} is a {dev.type()}."
|
|
1272
|
-
)
|
|
1273
|
-
|
|
1274
|
-
return dev
|
|
1271
|
+
return _device.Device.create(device_label, mmcore=self, device_type=device_type)
|
|
1275
1272
|
|
|
1276
1273
|
def getConfigGroupObject(
|
|
1277
1274
|
self, group_name: str, allow_missing: bool = False
|
|
@@ -1622,16 +1619,16 @@ class CMMCorePlus(pymmcore.CMMCore):
|
|
|
1622
1619
|
**Why Override?** to emit the `imageSnapped` event after snapping an image.
|
|
1623
1620
|
and to emit shutter property changes if `getAutoShutter` is `True`.
|
|
1624
1621
|
"""
|
|
1625
|
-
if autoshutter := self.getAutoShutter()
|
|
1626
|
-
self.
|
|
1622
|
+
if (autoshutter := self.getAutoShutter()) and (
|
|
1623
|
+
shutter := self.getShutterDevice()
|
|
1624
|
+
):
|
|
1625
|
+
self.events.propertyChanged.emit(shutter, "State", True)
|
|
1627
1626
|
try:
|
|
1628
1627
|
self._do_snap_image()
|
|
1629
1628
|
self.events.imageSnapped.emit(self.getCameraDevice())
|
|
1630
1629
|
finally:
|
|
1631
|
-
if autoshutter:
|
|
1632
|
-
self.events.propertyChanged.emit(
|
|
1633
|
-
self.getShutterDevice(), "State", False
|
|
1634
|
-
)
|
|
1630
|
+
if autoshutter and shutter:
|
|
1631
|
+
self.events.propertyChanged.emit(shutter, "State", False)
|
|
1635
1632
|
|
|
1636
1633
|
@property
|
|
1637
1634
|
def mda(self) -> MDARunner:
|
|
@@ -2083,13 +2080,13 @@ class CMMCorePlus(pymmcore.CMMCore):
|
|
|
2083
2080
|
super().deleteConfig(*args)
|
|
2084
2081
|
self.events.configDeleted.emit(groupName, configName)
|
|
2085
2082
|
|
|
2086
|
-
def deleteConfigGroup(self,
|
|
2083
|
+
def deleteConfigGroup(self, groupName: str) -> None:
|
|
2087
2084
|
"""Deletes an entire configuration `group`.
|
|
2088
2085
|
|
|
2089
2086
|
**Why Override?** To emit a `configGroupDeleted` event.
|
|
2090
2087
|
"""
|
|
2091
|
-
super().deleteConfigGroup(
|
|
2092
|
-
self.events.configGroupDeleted.emit(
|
|
2088
|
+
super().deleteConfigGroup(groupName)
|
|
2089
|
+
self.events.configGroupDeleted.emit(groupName)
|
|
2093
2090
|
|
|
2094
2091
|
@overload
|
|
2095
2092
|
def defineConfig(self, groupName: str, configName: str) -> None: ...
|