pymmcore-plus 0.13.5__tar.gz → 0.13.7__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.13.5 → pymmcore_plus-0.13.7}/PKG-INFO +4 -3
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/pyproject.toml +11 -3
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/_cli.py +13 -4
- pymmcore_plus-0.13.7/src/pymmcore_plus/_pymmcore.py +30 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/_util.py +88 -20
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/core/_constants.py +8 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/core/_mmcore_plus.py +32 -6
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/core/_sequencing.py +5 -2
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/install.py +68 -7
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/metadata/functions.py +9 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/metadata/schema.py +19 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/model/_config_file.py +16 -3
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/model/_device.py +6 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/model/_pixel_size_config.py +9 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/tests/test_cli.py +9 -3
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/tests/test_core.py +41 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/tests/test_events.py +2 -2
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/tests/test_model.py +12 -8
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/tests/unicore/test_unicore.py +7 -2
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/tests/unicore/test_xy_stage.py +2 -0
- pymmcore_plus-0.13.5/src/pymmcore_plus/_pymmcore.py +0 -14
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/.gitignore +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/LICENSE +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/README.md +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/__init__.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/_benchmark.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/_build.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/_logger.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/core/__init__.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/core/_adapter.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/core/_config.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/core/_config_group.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/core/_device.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/core/_metadata.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/core/_property.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/core/events/__init__.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/core/events/_device_signal_view.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/core/events/_norm_slot.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/core/events/_prop_event_mixin.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/core/events/_protocol.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/core/events/_psygnal.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/core/events/_qsignals.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/experimental/__init__.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/experimental/unicore/__init__.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/experimental/unicore/_device_manager.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/experimental/unicore/_proxy.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/experimental/unicore/_unicore.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/experimental/unicore/devices/__init__.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/experimental/unicore/devices/_device.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/experimental/unicore/devices/_properties.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/experimental/unicore/devices/_stage.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/mda/__init__.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/mda/_engine.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/mda/_protocol.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/mda/_runner.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/mda/_thread_relay.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/mda/events/__init__.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/mda/events/_protocol.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/mda/events/_psygnal.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/mda/events/_qsignals.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/mda/handlers/_5d_writer_base.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/mda/handlers/__init__.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/mda/handlers/_img_sequence_writer.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/mda/handlers/_ome_tiff_writer.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/mda/handlers/_ome_zarr_writer.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/mda/handlers/_tensorstore_handler.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/mda/handlers/_util.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/metadata/__init__.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/metadata/serialize.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/mocks.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/model/__init__.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/model/_config_group.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/model/_core_device.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/model/_core_link.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/model/_microscope.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/model/_property.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/py.typed +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/seq_tester.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/tests/__init__.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/tests/conftest.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/tests/io/test_image_sequence_writer.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/tests/io/test_ome_tiff.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/tests/io/test_zarr_writers.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/tests/local_config.cfg +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/tests/test_adapter_class.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/tests/test_bench.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/tests/test_config_group_class.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/tests/test_device_class.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/tests/test_mda.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/tests/test_metadata.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/tests/test_misc.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/tests/test_pixel_config_class.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/tests/test_property_class.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/tests/test_sequencing.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/tests/test_slm_image.py +0 -0
- {pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/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.13.
|
|
3
|
+
Version: 0.13.7
|
|
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
|
|
@@ -40,11 +40,12 @@ Requires-Dist: typer>=0.4.2; extra == 'cli'
|
|
|
40
40
|
Provides-Extra: dev
|
|
41
41
|
Requires-Dist: ipython; extra == 'dev'
|
|
42
42
|
Requires-Dist: mypy; extra == 'dev'
|
|
43
|
-
Requires-Dist: pdbpp; extra == 'dev'
|
|
43
|
+
Requires-Dist: pdbpp; (sys_platform != 'win32') and extra == 'dev'
|
|
44
44
|
Requires-Dist: pre-commit; extra == 'dev'
|
|
45
45
|
Requires-Dist: ruff; extra == 'dev'
|
|
46
46
|
Requires-Dist: tensorstore-stubs; extra == 'dev'
|
|
47
47
|
Provides-Extra: docs
|
|
48
|
+
Requires-Dist: mkdocs-autorefs==1.3.1; extra == 'docs'
|
|
48
49
|
Requires-Dist: mkdocs-material; extra == 'docs'
|
|
49
50
|
Requires-Dist: mkdocs-typer==0.0.3; extra == 'docs'
|
|
50
51
|
Requires-Dist: mkdocs>=1.4; extra == 'docs'
|
|
@@ -63,7 +64,7 @@ Provides-Extra: pyside6
|
|
|
63
64
|
Requires-Dist: pyside6<6.8,>=6.4.0; extra == 'pyside6'
|
|
64
65
|
Provides-Extra: test
|
|
65
66
|
Requires-Dist: msgpack; extra == 'test'
|
|
66
|
-
Requires-Dist: msgspec;
|
|
67
|
+
Requires-Dist: msgspec; extra == 'test'
|
|
67
68
|
Requires-Dist: pytest-cov>=4; extra == 'test'
|
|
68
69
|
Requires-Dist: pytest-qt>=4; extra == 'test'
|
|
69
70
|
Requires-Dist: pytest>=7.3.2; extra == 'test'
|
|
@@ -58,7 +58,7 @@ PySide6 = ["PySide6 >=6.4.0,<6.8"]
|
|
|
58
58
|
PyQt5 = ["PyQt5 >=5.15.4"]
|
|
59
59
|
PyQt6 = ["PyQt6 >=6.4.2,<6.8"]
|
|
60
60
|
test = [
|
|
61
|
-
"msgspec
|
|
61
|
+
"msgspec",
|
|
62
62
|
"msgpack",
|
|
63
63
|
"pytest-cov >=4",
|
|
64
64
|
"pytest-qt >=4",
|
|
@@ -70,11 +70,19 @@ test = [
|
|
|
70
70
|
"zarr >=2.2,<3",
|
|
71
71
|
"xarray",
|
|
72
72
|
]
|
|
73
|
-
dev = [
|
|
73
|
+
dev = [
|
|
74
|
+
"ipython",
|
|
75
|
+
"mypy",
|
|
76
|
+
"pdbpp; sys_platform != 'win32'",
|
|
77
|
+
"pre-commit",
|
|
78
|
+
"ruff",
|
|
79
|
+
"tensorstore-stubs",
|
|
80
|
+
]
|
|
74
81
|
docs = [
|
|
75
82
|
"mkdocs >=1.4",
|
|
76
83
|
"mkdocs-material",
|
|
77
84
|
"mkdocstrings ==0.22.0",
|
|
85
|
+
"mkdocs-autorefs ==1.3.1",
|
|
78
86
|
"mkdocstrings-python ==1.1.2",
|
|
79
87
|
"mkdocs-typer ==0.0.3",
|
|
80
88
|
# "griffe @ git+https://github.com/tlambert03/griffe@recursion"
|
|
@@ -194,4 +202,4 @@ ignore = [
|
|
|
194
202
|
]
|
|
195
203
|
|
|
196
204
|
[tool.typos.default]
|
|
197
|
-
extend-ignore-identifiers-re = ["(?i)nd2?.*", "(?i)ome", "anager"]
|
|
205
|
+
extend-ignore-identifiers-re = ["(?i)nd2?.*", "(?i)ome", "anager", "ba"]
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# do NOT use __future__.annotations here. It breaks typer.
|
|
2
|
+
import contextlib
|
|
2
3
|
import os
|
|
3
4
|
import shutil
|
|
4
5
|
import subprocess
|
|
@@ -8,6 +9,7 @@ from contextlib import suppress
|
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
from typing import Optional, Union, cast
|
|
10
11
|
|
|
12
|
+
from pymmcore_plus._util import get_device_interface_version
|
|
11
13
|
from pymmcore_plus.core._device import Device
|
|
12
14
|
from pymmcore_plus.core._mmcore_plus import CMMCorePlus
|
|
13
15
|
|
|
@@ -114,9 +116,15 @@ def _list() -> None:
|
|
|
114
116
|
for parent, items in found.items():
|
|
115
117
|
print(f":file_folder:[bold green] {parent}")
|
|
116
118
|
for item in items:
|
|
119
|
+
version = ""
|
|
120
|
+
for _lib in (parent / item).glob("*_dal_*"):
|
|
121
|
+
with suppress(Exception):
|
|
122
|
+
div = get_device_interface_version(_lib)
|
|
123
|
+
version = f" (Dev. Interface {div})"
|
|
124
|
+
break
|
|
117
125
|
bullet = " [bold yellow]*" if first else " •"
|
|
118
126
|
using = " [bold blue](active)" if first else ""
|
|
119
|
-
print(f"{bullet} [cyan]{item}{using}")
|
|
127
|
+
print(f"{bullet} [cyan]{item}{version}{using}")
|
|
120
128
|
first = False
|
|
121
129
|
else:
|
|
122
130
|
print(":x: [bold red]There are no pymmcore-plus Micro-Manager files.")
|
|
@@ -136,12 +144,13 @@ def mmstudio() -> None: # pragma: no cover
|
|
|
136
144
|
if mm
|
|
137
145
|
else None
|
|
138
146
|
)
|
|
139
|
-
if not app: # pragma: no cover
|
|
147
|
+
if not mm or not app: # pragma: no cover
|
|
140
148
|
print(f":x: [bold red]No MMStudio application found in {mm!r}")
|
|
141
149
|
print("[magenta]run `mmcore install` to install a version of Micro-Manager")
|
|
142
150
|
raise typer.Exit(1)
|
|
143
151
|
cmd = ["open", "-a", str(app)] if PLATFORM == "Darwin" else [str(app)]
|
|
144
|
-
|
|
152
|
+
with contextlib.chdir(mm):
|
|
153
|
+
raise typer.Exit(subprocess.run(cmd).returncode)
|
|
145
154
|
|
|
146
155
|
|
|
147
156
|
@app.command()
|
|
@@ -157,7 +166,7 @@ def install(
|
|
|
157
166
|
help="Installation directory.",
|
|
158
167
|
),
|
|
159
168
|
release: str = typer.Option(
|
|
160
|
-
"latest", "-r", "--release", help="Release date. e.g. 20210201"
|
|
169
|
+
"latest-compatible", "-r", "--release", help="Release date. e.g. 20210201"
|
|
161
170
|
),
|
|
162
171
|
plain_output: bool = typer.Option(
|
|
163
172
|
False,
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Internal module to choose between pymmcore and pymmcore-nano."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import NamedTuple
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
from pymmcore_nano import * # noqa F403
|
|
8
|
+
from pymmcore_nano import __version__
|
|
9
|
+
|
|
10
|
+
BACKEND = "pymmcore-nano"
|
|
11
|
+
NANO = True
|
|
12
|
+
except ImportError:
|
|
13
|
+
from pymmcore import * # noqa F403
|
|
14
|
+
from pymmcore import __version__
|
|
15
|
+
|
|
16
|
+
BACKEND = "pymmcore"
|
|
17
|
+
NANO = False
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class VersionInfo(NamedTuple):
|
|
21
|
+
"""Version info for the backend."""
|
|
22
|
+
|
|
23
|
+
major: int
|
|
24
|
+
minor: int
|
|
25
|
+
micro: int
|
|
26
|
+
device_interface: int
|
|
27
|
+
build: int
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
version_info = VersionInfo(*(int(x) for x in re.findall(r"\d+", __version__)))
|
|
@@ -94,7 +94,7 @@ def find_micromanager(return_first: bool = True) -> str | None | list[str]:
|
|
|
94
94
|
"""
|
|
95
95
|
from ._logger import logger
|
|
96
96
|
|
|
97
|
-
# we use a dict here to avoid duplicates
|
|
97
|
+
# we use a dict here to avoid duplicates, while retaining order
|
|
98
98
|
full_list: dict[str, None] = {}
|
|
99
99
|
|
|
100
100
|
# environment variable takes precedence
|
|
@@ -114,13 +114,42 @@ def find_micromanager(return_first: bool = True) -> str | None | list[str]:
|
|
|
114
114
|
return path
|
|
115
115
|
full_list[path] = None
|
|
116
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
|
+
|
|
117
143
|
# then look in user_data_dir
|
|
118
144
|
_folders = (p for p in USER_DATA_MM_PATH.glob("Micro-Manager*") if p.is_dir())
|
|
119
|
-
user_install
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
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)
|
|
124
153
|
for x in user_install:
|
|
125
154
|
full_list[str(x)] = None
|
|
126
155
|
|
|
@@ -130,9 +159,13 @@ def find_micromanager(return_first: bool = True) -> str | None | list[str]:
|
|
|
130
159
|
p for p in PYMMCORE_PLUS_PATH.glob(f"**/Micro-Manager*{sfx}") if p.is_dir()
|
|
131
160
|
]
|
|
132
161
|
if local_install:
|
|
133
|
-
if return_first
|
|
134
|
-
|
|
135
|
-
|
|
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)
|
|
136
169
|
for x in local_install:
|
|
137
170
|
full_list[str(x)] = None
|
|
138
171
|
|
|
@@ -148,13 +181,17 @@ def find_micromanager(return_first: bool = True) -> str | None | list[str]:
|
|
|
148
181
|
app_path = applications[sys.platform]
|
|
149
182
|
pth = next(app_path.glob("[m,M]icro-[m,M]anager*"), None)
|
|
150
183
|
if return_first:
|
|
151
|
-
if pth
|
|
152
|
-
logger.
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
|
158
195
|
if pth is not None:
|
|
159
196
|
full_list[str(pth)] = None
|
|
160
197
|
return list(full_list)
|
|
@@ -229,15 +266,15 @@ def _qt_app_is_running() -> bool:
|
|
|
229
266
|
return False # pragma: no cover
|
|
230
267
|
|
|
231
268
|
|
|
232
|
-
|
|
269
|
+
PYMM_SIGNALS_BACKEND = "PYMM_SIGNALS_BACKEND"
|
|
233
270
|
|
|
234
271
|
|
|
235
272
|
def signals_backend() -> Literal["qt", "psygnal"]:
|
|
236
273
|
"""Return the name of the event backend to use."""
|
|
237
|
-
env_var = os.environ.get(
|
|
274
|
+
env_var = os.environ.get(PYMM_SIGNALS_BACKEND, "auto").lower()
|
|
238
275
|
if env_var not in {"qt", "psygnal", "auto"}:
|
|
239
276
|
warnings.warn(
|
|
240
|
-
f"{
|
|
277
|
+
f"{PYMM_SIGNALS_BACKEND} must be one of ['qt', 'psygnal', 'auto']. "
|
|
241
278
|
f"not: {env_var!r}. Using 'auto'.",
|
|
242
279
|
stacklevel=1,
|
|
243
280
|
)
|
|
@@ -250,7 +287,7 @@ def signals_backend() -> Literal["qt", "psygnal"]:
|
|
|
250
287
|
if qt_app_running or list(_imported_qt_modules()):
|
|
251
288
|
return "qt"
|
|
252
289
|
warnings.warn(
|
|
253
|
-
f"{
|
|
290
|
+
f"{PYMM_SIGNALS_BACKEND} set to 'qt', but no Qt app is running. "
|
|
254
291
|
"Falling back to 'psygnal'.",
|
|
255
292
|
stacklevel=1,
|
|
256
293
|
)
|
|
@@ -618,3 +655,34 @@ def timestamp() -> str:
|
|
|
618
655
|
with suppress(Exception):
|
|
619
656
|
now = now.astimezone()
|
|
620
657
|
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
|
|
@@ -94,6 +94,14 @@ class CFGCommand(str, Enum):
|
|
|
94
94
|
PixelSizeAffine = pymmcore.g_CFGCommand_PixelSizeAffine
|
|
95
95
|
ParentID = pymmcore.g_CFGCommand_ParentID
|
|
96
96
|
FocusDirection = pymmcore.g_CFGCommand_FocusDirection
|
|
97
|
+
|
|
98
|
+
if hasattr(pymmcore, "g_CFGCommand_PixelSizedxdz"):
|
|
99
|
+
PixelSize_dxdz = pymmcore.g_CFGCommand_PixelSizedxdz
|
|
100
|
+
if hasattr(pymmcore, "g_CFGCommand_PixelSizedydz"):
|
|
101
|
+
PixelSize_dydz = pymmcore.g_CFGCommand_PixelSizedydz
|
|
102
|
+
if hasattr(pymmcore, "g_CFGCommand_PixelSizeOptimalZUm"):
|
|
103
|
+
PixelSize_OptimalZUm = pymmcore.g_CFGCommand_PixelSizeOptimalZUm
|
|
104
|
+
|
|
97
105
|
#
|
|
98
106
|
FieldDelimiters = pymmcore.g_FieldDelimiters
|
|
99
107
|
|
|
@@ -207,14 +207,39 @@ class CMMCorePlus(pymmcore.CMMCore):
|
|
|
207
207
|
|
|
208
208
|
def __init__(self, mm_path: str | None = None, adapter_paths: Sequence[str] = ()):
|
|
209
209
|
super().__init__()
|
|
210
|
+
if os.getenv("PYMM_DEBUG_LOG", "0").lower() in ("1", "true"):
|
|
211
|
+
self.enableDebugLog(True)
|
|
212
|
+
if os.getenv("PYMM_STDERR_LOG", "0").lower() in ("1", "true"):
|
|
213
|
+
self.enableStderrLog(True)
|
|
214
|
+
if buf_size := os.getenv("PYMM_BUFFER_SIZE_MB", ""):
|
|
215
|
+
try:
|
|
216
|
+
buf_size_int = int(buf_size)
|
|
217
|
+
if buf_size_int:
|
|
218
|
+
self.setCircularBufferMemoryFootprint(buf_size_int)
|
|
219
|
+
except (ValueError, TypeError):
|
|
220
|
+
warnings.warn("PYMM_BUFFER_SIZE_MB must be an integer", stacklevel=2)
|
|
210
221
|
|
|
211
222
|
# Set the first instance of this class as the global singleton
|
|
212
223
|
global _instance
|
|
213
224
|
if _instance is None:
|
|
214
225
|
_instance = self
|
|
215
226
|
|
|
216
|
-
if hasattr(
|
|
217
|
-
|
|
227
|
+
if hasattr(self, "enableFeature"):
|
|
228
|
+
strict = True
|
|
229
|
+
if env_strict := os.getenv("PYMM_STRICT_INIT_CHECKS", "").lower():
|
|
230
|
+
if env_strict in ("1", "true"):
|
|
231
|
+
strict = True
|
|
232
|
+
elif env_strict in ("0", "false"):
|
|
233
|
+
strict = False
|
|
234
|
+
self.enableFeature("StrictInitializationChecks", strict)
|
|
235
|
+
|
|
236
|
+
parallel = True
|
|
237
|
+
if env_parallel := os.getenv("PYMM_PARALLEL_INIT", "").lower():
|
|
238
|
+
if env_parallel in ("1", "true"):
|
|
239
|
+
parallel = True
|
|
240
|
+
elif env_parallel in ("0", "false"):
|
|
241
|
+
parallel = False
|
|
242
|
+
self.enableFeature("ParallelDeviceInitialization", parallel)
|
|
218
243
|
|
|
219
244
|
# TODO: test this on windows ... writing to the same file may be an issue there
|
|
220
245
|
if logfile := current_logfile(logger):
|
|
@@ -354,7 +379,7 @@ class CMMCorePlus(pymmcore.CMMCore):
|
|
|
354
379
|
"""
|
|
355
380
|
try:
|
|
356
381
|
super().loadDevice(label, moduleName, deviceName)
|
|
357
|
-
except RuntimeError as e:
|
|
382
|
+
except (RuntimeError, ValueError) as e:
|
|
358
383
|
if exc := self._load_error_with_info(label, moduleName, deviceName, str(e)):
|
|
359
384
|
raise exc from e
|
|
360
385
|
|
|
@@ -2001,9 +2026,10 @@ class CMMCorePlus(pymmcore.CMMCore):
|
|
|
2001
2026
|
**Why Override?** To also save pixel size configurations.
|
|
2002
2027
|
"""
|
|
2003
2028
|
super().saveSystemConfiguration(filename)
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2029
|
+
if pymmcore.version_info < (11, 5):
|
|
2030
|
+
# saveSystemConfiguration does not save the pixel size config so hereq
|
|
2031
|
+
# we add to the saved file also any pixel size config.
|
|
2032
|
+
self._save_pixel_configurations(filename)
|
|
2007
2033
|
|
|
2008
2034
|
def _save_pixel_configurations(self, filename: str) -> None:
|
|
2009
2035
|
px_configs = self.getAvailablePixelSizeConfigs()
|
|
@@ -3,10 +3,10 @@ from __future__ import annotations
|
|
|
3
3
|
from collections import defaultdict
|
|
4
4
|
from collections.abc import Iterable
|
|
5
5
|
from contextlib import suppress
|
|
6
|
-
from typing import TYPE_CHECKING, Any, TypeVar
|
|
6
|
+
from typing import TYPE_CHECKING, Any, Optional, TypeVar
|
|
7
7
|
|
|
8
8
|
from pydantic import Field, model_validator
|
|
9
|
-
from useq import AcquireImage, MDAEvent
|
|
9
|
+
from useq import AcquireImage, MDAEvent, MDASequence
|
|
10
10
|
|
|
11
11
|
from pymmcore_plus.core._constants import DeviceType, Keyword
|
|
12
12
|
|
|
@@ -65,6 +65,9 @@ class SequencedEvent(MDAEvent):
|
|
|
65
65
|
z_sequence: tuple[float, ...] = Field(default_factory=tuple)
|
|
66
66
|
slm_sequence: tuple[bytes, ...] = Field(default_factory=tuple)
|
|
67
67
|
|
|
68
|
+
# re-defining this from MDAEvent to circumvent a strange issue with pydantic 2.11
|
|
69
|
+
sequence: Optional[MDASequence] = Field(default=None, repr=False) # noqa: UP007
|
|
70
|
+
|
|
68
71
|
# all other property sequences
|
|
69
72
|
property_sequences: dict[tuple[str, str], list[str]] = Field(default_factory=dict)
|
|
70
73
|
# static properties should be added to MDAEvent.properties as usual
|
|
@@ -8,8 +8,9 @@ import subprocess
|
|
|
8
8
|
import sys
|
|
9
9
|
import tempfile
|
|
10
10
|
from contextlib import contextmanager, nullcontext
|
|
11
|
+
from functools import cache
|
|
11
12
|
from pathlib import Path
|
|
12
|
-
from platform import system
|
|
13
|
+
from platform import machine, system
|
|
13
14
|
from typing import TYPE_CHECKING, Callable, Protocol
|
|
14
15
|
from urllib.request import urlopen, urlretrieve
|
|
15
16
|
|
|
@@ -57,7 +58,35 @@ except ImportError: # pragma: no cover
|
|
|
57
58
|
|
|
58
59
|
|
|
59
60
|
PLATFORM = system()
|
|
61
|
+
MACH = machine()
|
|
60
62
|
BASE_URL = "https://download.micro-manager.org"
|
|
63
|
+
plat = {"Darwin": "Mac", "Windows": "Windows", "Linux": "Linux"}.get(PLATFORM)
|
|
64
|
+
DOWNLOADS_URL = f"{BASE_URL}/nightly/2.0/{plat}/"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# Dates of release for each interface version.
|
|
68
|
+
# generally running `mmcore install -r <some_date>` will bring in devices with
|
|
69
|
+
# the NEW interface.
|
|
70
|
+
INTERFACES: dict[int, str] = {
|
|
71
|
+
73: "20250318",
|
|
72
|
+
72: "20250318",
|
|
73
|
+
71: "20221031",
|
|
74
|
+
70: "20210219",
|
|
75
|
+
69: "20180712",
|
|
76
|
+
68: "20171107",
|
|
77
|
+
67: "20160609",
|
|
78
|
+
66: "20160608",
|
|
79
|
+
65: "20150528",
|
|
80
|
+
64: "20150515",
|
|
81
|
+
63: "20150505",
|
|
82
|
+
62: "20150501",
|
|
83
|
+
61: "20140801",
|
|
84
|
+
60: "20140618",
|
|
85
|
+
59: "20140515",
|
|
86
|
+
58: "20140514",
|
|
87
|
+
57: "20140125",
|
|
88
|
+
56: "20140120",
|
|
89
|
+
}
|
|
61
90
|
|
|
62
91
|
|
|
63
92
|
def _get_download_name(url: str) -> str:
|
|
@@ -141,10 +170,10 @@ def _mac_install(dmg: Path, dest: Path, log_msg: _MsgLogger) -> None:
|
|
|
141
170
|
os.rename(_tmp / "ImageJ.app", install_path / "ImageJ.app")
|
|
142
171
|
|
|
143
172
|
|
|
173
|
+
@cache
|
|
144
174
|
def available_versions() -> dict[str, str]:
|
|
145
175
|
"""Return a map of version -> url available for download."""
|
|
146
|
-
|
|
147
|
-
with urlopen(f"{BASE_URL}/nightly/2.0/{plat}/") as resp:
|
|
176
|
+
with urlopen(DOWNLOADS_URL) as resp:
|
|
148
177
|
html = resp.read().decode("utf-8")
|
|
149
178
|
|
|
150
179
|
all_links = re.findall(r"href=\"([^\"]+)\"", html)
|
|
@@ -187,7 +216,7 @@ def _download_url(url: str, output_path: Path, show_progress: bool = True) -> No
|
|
|
187
216
|
|
|
188
217
|
def install(
|
|
189
218
|
dest: Path | str = USER_DATA_MM_PATH,
|
|
190
|
-
release: str = "latest",
|
|
219
|
+
release: str = "latest-compatible",
|
|
191
220
|
log_msg: _MsgLogger = _pretty_print,
|
|
192
221
|
) -> None:
|
|
193
222
|
"""Install Micro-Manager to `dest`.
|
|
@@ -199,14 +228,20 @@ def install(
|
|
|
199
228
|
folder in the user's data directory: `appdirs.user_data_dir()`.
|
|
200
229
|
release : str, optional
|
|
201
230
|
Which release to install, by default "latest". Should be a date in the form
|
|
202
|
-
YYYYMMDD,
|
|
231
|
+
YYYYMMDD, "latest" to install the latest nightly release, or "latest-compatible"
|
|
232
|
+
to install the latest nightly release that is compatible with the
|
|
233
|
+
device interface version of the current pymmcore version.
|
|
203
234
|
log_msg : _MsgLogger, optional
|
|
204
235
|
Callback to log messages, must have signature:
|
|
205
236
|
`def logger(text: str, color: str = "", emoji: str = ""): ...`
|
|
206
237
|
May ignore color and emoji.
|
|
207
238
|
"""
|
|
208
|
-
if PLATFORM not in ("Darwin", "Windows")
|
|
209
|
-
|
|
239
|
+
if PLATFORM not in ("Darwin", "Windows") or (
|
|
240
|
+
PLATFORM == "Darwin" and MACH == "arm64"
|
|
241
|
+
): # pragma: no cover
|
|
242
|
+
log_msg(
|
|
243
|
+
f"Unsupported platform/architecture: {PLATFORM}/{MACH}", "bold red", ":x:"
|
|
244
|
+
)
|
|
210
245
|
log_msg(
|
|
211
246
|
"Consider building from source (mmcore build-dev).",
|
|
212
247
|
"bold yellow",
|
|
@@ -214,6 +249,32 @@ def install(
|
|
|
214
249
|
)
|
|
215
250
|
raise sys.exit(1)
|
|
216
251
|
|
|
252
|
+
if release == "latest-compatible":
|
|
253
|
+
from pymmcore_plus import _pymmcore
|
|
254
|
+
|
|
255
|
+
div = _pymmcore.version_info.device_interface
|
|
256
|
+
# date when the device interface version FOLLOWING the version that this
|
|
257
|
+
# pymmcore supports was released.
|
|
258
|
+
next_div_date = INTERFACES.get(div + 1, None)
|
|
259
|
+
|
|
260
|
+
# if div is equal to the greatest known interface version, use latest
|
|
261
|
+
if div == max(INTERFACES.keys()) or next_div_date is None:
|
|
262
|
+
release = "latest"
|
|
263
|
+
else: # pragma: no cover
|
|
264
|
+
# otherwise, find the date of the release in available_versions() that
|
|
265
|
+
# is less than the next_div date.
|
|
266
|
+
available = available_versions()
|
|
267
|
+
release = max(
|
|
268
|
+
(date for date in available if date < next_div_date),
|
|
269
|
+
default="unavailable",
|
|
270
|
+
)
|
|
271
|
+
if release == "unavailable":
|
|
272
|
+
# fallback to latest if no compatible versions found
|
|
273
|
+
raise ValueError(
|
|
274
|
+
"Unable to find a compatible release for device interface"
|
|
275
|
+
f"{div} at {DOWNLOADS_URL} "
|
|
276
|
+
)
|
|
277
|
+
|
|
217
278
|
if release == "latest":
|
|
218
279
|
plat = {
|
|
219
280
|
"Darwin": "macos/Micro-Manager-x86_64-latest.dmg",
|
|
@@ -289,6 +289,15 @@ def pixel_size_config(core: CMMCorePlus, *, config_name: str) -> PixelSizeConfig
|
|
|
289
289
|
affine = core.getPixelSizeAffineByID(config_name)
|
|
290
290
|
if affine != (1.0, 0.0, 0.0, 0.0, 1.0, 0.0):
|
|
291
291
|
info["pixel_size_affine"] = affine
|
|
292
|
+
# added in v11.5
|
|
293
|
+
if hasattr(core, "getPixelSizedxdz") and (px := core.getPixelSizedxdz(config_name)):
|
|
294
|
+
info["pixel_size_dxdz"] = px
|
|
295
|
+
if hasattr(core, "getPixelSizedydz") and (px := core.getPixelSizedydz(config_name)):
|
|
296
|
+
info["pixel_size_dydz"] = px
|
|
297
|
+
if hasattr(core, "getPixelSizeOptimalZUm") and (
|
|
298
|
+
z := core.getPixelSizeOptimalZUm(config_name)
|
|
299
|
+
):
|
|
300
|
+
info["pixel_size_optimal_z_um"] = z
|
|
292
301
|
return info
|
|
293
302
|
|
|
294
303
|
|
|
@@ -331,11 +331,30 @@ class PixelSizeConfigPreset(ConfigPreset):
|
|
|
331
331
|
corrected for binning and known magnification devices. The affine transform
|
|
332
332
|
consists of the first two rows of a 3x3 matrix, the third row is always assumed
|
|
333
333
|
to be 0.0 0.0 1.0.
|
|
334
|
+
|
|
335
|
+
pixel_size_dxdz : float
|
|
336
|
+
*Not Required*. The angle between the camera's x axis and the axis (direction)
|
|
337
|
+
of the z drive for the given pixel size configuration. This angle is
|
|
338
|
+
dimensionless (i.e. the ratio of the translation in x caused by a translation
|
|
339
|
+
in z, i.e. dx / dz). If missing, assume 0.0.
|
|
340
|
+
pixel_size_dydz : float
|
|
341
|
+
*Not Required*. The angle between the camera's y axis and the axis (direction)
|
|
342
|
+
of the z drive for the given pixel size configuration. This angle is
|
|
343
|
+
dimensionless (i.e. the ratio of the translation in y caused by a translation
|
|
344
|
+
in z, i.e. dy / dz). If missing, assume 0.0.
|
|
345
|
+
pixel_size_optimal_z_um : float
|
|
346
|
+
*Not Required*. User-defined optimal Z step size is for this pixel size config.
|
|
347
|
+
If missing, assume 0.0.
|
|
334
348
|
"""
|
|
335
349
|
|
|
336
350
|
pixel_size_um: float
|
|
337
351
|
pixel_size_affine: NotRequired[AffineTuple]
|
|
338
352
|
|
|
353
|
+
# added in MMCore v 11.5
|
|
354
|
+
pixel_size_dxdz: NotRequired[float] # default 0.0
|
|
355
|
+
pixel_size_dydz: NotRequired[float] # default 0.0
|
|
356
|
+
pixel_size_optimal_z_um: NotRequired[float] # default 0.0
|
|
357
|
+
|
|
339
358
|
|
|
340
359
|
class ConfigGroup(TypedDict):
|
|
341
360
|
"""A group of configuration presets.
|
|
@@ -5,7 +5,7 @@ from __future__ import annotations
|
|
|
5
5
|
import warnings
|
|
6
6
|
from typing import TYPE_CHECKING, Any, Callable
|
|
7
7
|
|
|
8
|
-
from pymmcore_plus import CFGCommand, DeviceType, FocusDirection, Keyword
|
|
8
|
+
from pymmcore_plus import CFGCommand, DeviceType, FocusDirection, Keyword, _pymmcore
|
|
9
9
|
from pymmcore_plus._util import timestamp
|
|
10
10
|
|
|
11
11
|
from ._config_group import ConfigGroup, ConfigPreset, Setting
|
|
@@ -162,6 +162,12 @@ def iter_pixel_size_presets(scope: Microscope) -> Iterable[str]:
|
|
|
162
162
|
yield _serialize(CFGCommand.PixelSize_um, p.name, p.pixel_size_um)
|
|
163
163
|
if p.affine != DEFAULT_AFFINE:
|
|
164
164
|
yield _serialize(CFGCommand.PixelSizeAffine, p.name, *p.affine)
|
|
165
|
+
if p.angle_dxdz and (cmd := getattr(CFGCommand, "PixelSizeAngleDxdz", None)):
|
|
166
|
+
yield _serialize(cmd, p.name, p.angle_dxdz)
|
|
167
|
+
if p.angle_dydz and (cmd := getattr(CFGCommand, "PixelSizeAngleDydz", None)):
|
|
168
|
+
yield _serialize(cmd, p.name, p.angle_dydz)
|
|
169
|
+
if p.optimalz_um and (cmd := getattr(CFGCommand, "PixelSize_OptimalZUm", None)):
|
|
170
|
+
yield _serialize(cmd, p.name, p.optimalz_um)
|
|
165
171
|
|
|
166
172
|
|
|
167
173
|
# Order will determine the order of the sections in the file
|
|
@@ -180,10 +186,17 @@ CONFIG_SECTIONS: dict[str, Callable[[Microscope], Iterable[str]]] = {
|
|
|
180
186
|
"Camera-synchronized devices": lambda _: [],
|
|
181
187
|
"Labels": iter_labels,
|
|
182
188
|
"Configuration presets": iter_config_presets,
|
|
183
|
-
"Roles": iter_roles, # MMStudio puts this above Cam-Synched devices, MMCore here.
|
|
184
|
-
"PixelSize settings": iter_pixel_size_presets,
|
|
185
189
|
}
|
|
186
190
|
|
|
191
|
+
|
|
192
|
+
if _pymmcore.version_info >= (11, 5):
|
|
193
|
+
CONFIG_SECTIONS["PixelSize settings"] = iter_pixel_size_presets
|
|
194
|
+
CONFIG_SECTIONS["Roles"] = iter_roles
|
|
195
|
+
else:
|
|
196
|
+
CONFIG_SECTIONS["Roles"] = iter_roles
|
|
197
|
+
CONFIG_SECTIONS["PixelSize settings"] = iter_pixel_size_presets
|
|
198
|
+
|
|
199
|
+
|
|
187
200
|
# ------------------ Deserialization ------------------
|
|
188
201
|
|
|
189
202
|
# TODO: ... I think the command subclasses are probably overkill here.
|
|
@@ -7,6 +7,7 @@ from typing import TYPE_CHECKING
|
|
|
7
7
|
|
|
8
8
|
from pymmcore_plus import CMMCorePlus, DeviceType, FocusDirection, Keyword
|
|
9
9
|
from pymmcore_plus._util import no_stdout
|
|
10
|
+
from pymmcore_plus.core._constants import DeviceInitializationState
|
|
10
11
|
|
|
11
12
|
from ._core_link import CoreObject
|
|
12
13
|
from ._property import Property
|
|
@@ -410,6 +411,11 @@ def get_available_devices(core: CMMCorePlus) -> list[AvailableDevice]:
|
|
|
410
411
|
for hub in core.getLoadedDevicesOfType(DeviceType.Hub):
|
|
411
412
|
lib_name = core.getDeviceLibrary(hub)
|
|
412
413
|
hub_dev = library_to_hub.get((lib_name, hub))
|
|
414
|
+
if (
|
|
415
|
+
core.getDeviceInitializationState(hub)
|
|
416
|
+
!= DeviceInitializationState.InitializedSuccessfully
|
|
417
|
+
):
|
|
418
|
+
continue
|
|
413
419
|
for child in core.getInstalledDevices(hub):
|
|
414
420
|
dev = AvailableDevice(
|
|
415
421
|
library=lib_name, adapter_name=child, library_hub=hub_dev
|
|
@@ -31,6 +31,9 @@ class PixelSizePreset(ConfigPreset):
|
|
|
31
31
|
|
|
32
32
|
pixel_size_um: float = 0.0
|
|
33
33
|
affine: AffineTuple = DEFAULT_AFFINE
|
|
34
|
+
angle_dxdz: float = 0.0
|
|
35
|
+
angle_dydz: float = 0.0
|
|
36
|
+
optimalz_um: float = 0.0
|
|
34
37
|
|
|
35
38
|
@classmethod
|
|
36
39
|
def from_metadata(cls, meta: PixelSizeConfigPreset) -> Self: # type: ignore [override]
|
|
@@ -38,6 +41,12 @@ class PixelSizePreset(ConfigPreset):
|
|
|
38
41
|
obj.pixel_size_um = meta["pixel_size_um"]
|
|
39
42
|
if "pixel_size_affine" in meta:
|
|
40
43
|
obj.affine = meta["pixel_size_affine"]
|
|
44
|
+
if "pixel_size_dxdz" in meta:
|
|
45
|
+
obj.angle_dxdz = meta["pixel_size_dxdz"]
|
|
46
|
+
if "pixel_size_dydz" in meta:
|
|
47
|
+
obj.angle_dydz = meta["pixel_size_dydz"]
|
|
48
|
+
if "pixel_size_optimal_z_um" in meta:
|
|
49
|
+
obj.optimalz_um = meta["pixel_size_optimal_z_um"]
|
|
41
50
|
return obj
|
|
42
51
|
|
|
43
52
|
def __rich_repr__(self, *, defaults: bool = False) -> Iterable[tuple[str, Any]]:
|
|
@@ -29,6 +29,12 @@ from pymmcore_plus import CMMCorePlus, __version__, _cli, _logger, _util, instal
|
|
|
29
29
|
runner = CliRunner()
|
|
30
30
|
subrun = subprocess.run
|
|
31
31
|
|
|
32
|
+
skipif_no_drivers_available = pytest.mark.skipif(
|
|
33
|
+
platform.system() == "Linux"
|
|
34
|
+
or (platform.system() == "Darwin" and platform.machine() == "arm64"),
|
|
35
|
+
reason="Drivers not available on Linux or macOS ARM64",
|
|
36
|
+
)
|
|
37
|
+
|
|
32
38
|
|
|
33
39
|
def _mock_urlretrieve(url: str, filename: str, reporthook=None) -> None:
|
|
34
40
|
"""fake urlretrieve that writes a fake file."""
|
|
@@ -68,7 +74,7 @@ def _mock_run(dest: Path) -> Callable:
|
|
|
68
74
|
return runner
|
|
69
75
|
|
|
70
76
|
|
|
71
|
-
@
|
|
77
|
+
@skipif_no_drivers_available
|
|
72
78
|
def test_install_app(tmp_path: Path) -> None:
|
|
73
79
|
patch_download = patch.object(install, "urlretrieve", _mock_urlretrieve)
|
|
74
80
|
patch_run = patch.object(subprocess, "run", _mock_run(tmp_path))
|
|
@@ -79,7 +85,7 @@ def test_install_app(tmp_path: Path) -> None:
|
|
|
79
85
|
assert result.exit_code == 0
|
|
80
86
|
|
|
81
87
|
|
|
82
|
-
@
|
|
88
|
+
@skipif_no_drivers_available
|
|
83
89
|
def test_basic_install(tmp_path: Path) -> None:
|
|
84
90
|
patch_download = patch.object(install, "urlretrieve", _mock_urlretrieve)
|
|
85
91
|
patch_run = patch.object(subprocess, "run", _mock_run(tmp_path))
|
|
@@ -91,7 +97,7 @@ def test_basic_install(tmp_path: Path) -> None:
|
|
|
91
97
|
assert mock.call_args_list[-1][0][0].startswith("Installed")
|
|
92
98
|
|
|
93
99
|
|
|
94
|
-
@
|
|
100
|
+
@skipif_no_drivers_available
|
|
95
101
|
def test_available_versions() -> None:
|
|
96
102
|
"""installing with an erroneous version should fail and show available versions."""
|
|
97
103
|
result = runner.invoke(app, ["install", "-r", "xxxx"])
|
|
@@ -607,3 +607,44 @@ def test_snap_rgb(core: CMMCorePlus) -> None:
|
|
|
607
607
|
dtype="uint8",
|
|
608
608
|
)
|
|
609
609
|
np.testing.assert_equal(img[::64, -1], expect)
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
def test_env_vars(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
|
613
|
+
core = CMMCorePlus()
|
|
614
|
+
assert not core.debugLogEnabled()
|
|
615
|
+
assert not core.stderrLogEnabled()
|
|
616
|
+
assert core.getCircularBufferMemoryFootprint() != 64
|
|
617
|
+
assert core.isFeatureEnabled("ParallelDeviceInitialization")
|
|
618
|
+
assert core.isFeatureEnabled("StrictInitializationChecks")
|
|
619
|
+
|
|
620
|
+
with monkeypatch.context() as m:
|
|
621
|
+
m.setenv("PYMM_DEBUG_LOG", "1")
|
|
622
|
+
core = CMMCorePlus()
|
|
623
|
+
assert core.debugLogEnabled()
|
|
624
|
+
|
|
625
|
+
with monkeypatch.context() as m:
|
|
626
|
+
m.setenv("PYMM_STDERR_LOG", "1")
|
|
627
|
+
core = CMMCorePlus()
|
|
628
|
+
assert core.stderrLogEnabled()
|
|
629
|
+
|
|
630
|
+
with monkeypatch.context() as m:
|
|
631
|
+
m.setenv("PYMM_BUFFER_SIZE_MB", "64")
|
|
632
|
+
core = CMMCorePlus()
|
|
633
|
+
assert core.getCircularBufferMemoryFootprint() == 64
|
|
634
|
+
|
|
635
|
+
with monkeypatch.context() as m:
|
|
636
|
+
m.setenv("PYMM_STRICT_INIT_CHECKS", "0")
|
|
637
|
+
core = CMMCorePlus()
|
|
638
|
+
assert not core.isFeatureEnabled("StrictInitializationChecks")
|
|
639
|
+
|
|
640
|
+
with monkeypatch.context() as m:
|
|
641
|
+
# test with an invalid value
|
|
642
|
+
m.setenv("PYMM_PARALLEL_INIT", "0")
|
|
643
|
+
core = CMMCorePlus()
|
|
644
|
+
assert not core.isFeatureEnabled("ParallelDeviceInitialization")
|
|
645
|
+
|
|
646
|
+
with monkeypatch.context() as m:
|
|
647
|
+
m.setenv("MICROMANAGER_PATH", str(tmp_path))
|
|
648
|
+
core = CMMCorePlus()
|
|
649
|
+
paths = core.getDeviceAdapterSearchPaths()
|
|
650
|
+
assert str(tmp_path) in paths
|
|
@@ -7,7 +7,7 @@ from unittest.mock import Mock, call
|
|
|
7
7
|
import pytest
|
|
8
8
|
|
|
9
9
|
from pymmcore_plus import CMMCorePlus, Keyword
|
|
10
|
-
from pymmcore_plus._util import
|
|
10
|
+
from pymmcore_plus._util import PYMM_SIGNALS_BACKEND
|
|
11
11
|
from pymmcore_plus.core.events import CMMCoreSignaler, PCoreSignaler
|
|
12
12
|
|
|
13
13
|
Signalers = [CMMCoreSignaler]
|
|
@@ -38,7 +38,7 @@ def test_signal_backend_selection(
|
|
|
38
38
|
|
|
39
39
|
_ = QApplication.instance() or QApplication([])
|
|
40
40
|
|
|
41
|
-
monkeypatch.setenv(
|
|
41
|
+
monkeypatch.setenv(PYMM_SIGNALS_BACKEND, env_var)
|
|
42
42
|
ctx = (
|
|
43
43
|
pytest.warns(UserWarning)
|
|
44
44
|
if (env_var == "nonsense" or (env_var == "qt" and QCoreSignaler is None))
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import re
|
|
3
4
|
from pathlib import Path
|
|
4
5
|
|
|
5
6
|
import pytest
|
|
@@ -56,7 +57,7 @@ def test_model_from_summary_metadata(tmp_path: Path) -> None:
|
|
|
56
57
|
|
|
57
58
|
def non_empty_lines(path: Path) -> list[str]:
|
|
58
59
|
return [
|
|
59
|
-
ln
|
|
60
|
+
ln.replace("1.0", "1").replace("0.0", "0") # normalize floats
|
|
60
61
|
for line in path.read_text().splitlines()
|
|
61
62
|
if (ln := line.strip()) and not ln.startswith("#")
|
|
62
63
|
]
|
|
@@ -95,16 +96,19 @@ def _assert_cfg_matches_core_save(
|
|
|
95
96
|
model.save(model_out)
|
|
96
97
|
core.saveSystemConfiguration(str(core_out))
|
|
97
98
|
|
|
98
|
-
# MMCore DOES write out default affine transforms
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
99
|
+
# MMCore DOES write out default affine transforms and angles...
|
|
100
|
+
# MMStudio doesn't and we don't
|
|
101
|
+
pattern = re.compile(
|
|
102
|
+
r"(PixelSizeAffine,.+,1(?:\.0)?,0(?:\.0)?,0(?:\.0)?,0(?:\.0)?,1(?:\.0)?,0(?:\.0)?"
|
|
103
|
+
r"|PixelSizeAngle_dxdz,.+,0|"
|
|
104
|
+
r"PixelSizeAngle_dydz,.+,0|PixelSizeOptimalZ_Um,.+,0)"
|
|
105
|
+
)
|
|
106
|
+
core_lines = [x for x in non_empty_lines(core_out) if not pattern.match(x)]
|
|
107
|
+
# MMCore doesn't write out AutoShutter prefs, but we do
|
|
103
108
|
model_lines = [
|
|
104
109
|
x
|
|
105
110
|
for x in non_empty_lines(model_out)
|
|
106
|
-
if not x.startswith("Property,Core,AutoShutter")
|
|
107
|
-
and "1.0,0.0,0.0,0.0,1.0,0.0" not in x
|
|
111
|
+
if not x.startswith("Property,Core,AutoShutter") and "1,0,0,0,1,0" not in x
|
|
108
112
|
]
|
|
109
113
|
assert core_lines == model_lines
|
|
110
114
|
|
|
@@ -5,7 +5,7 @@ from unittest.mock import MagicMock
|
|
|
5
5
|
|
|
6
6
|
import pytest
|
|
7
7
|
|
|
8
|
-
from pymmcore_plus import DeviceInitializationState, DeviceType, PropertyType
|
|
8
|
+
from pymmcore_plus import DeviceInitializationState, DeviceType, PropertyType, _pymmcore
|
|
9
9
|
from pymmcore_plus.experimental.unicore import Device, UniMMCore, pymm_property
|
|
10
10
|
|
|
11
11
|
DOC = """Example generic device."""
|
|
@@ -153,7 +153,12 @@ def test_device_load_from_module():
|
|
|
153
153
|
|
|
154
154
|
# If we gave a proper library name, but a bad device name...
|
|
155
155
|
# it should raise the usual error
|
|
156
|
-
|
|
156
|
+
msg = (
|
|
157
|
+
"failed to instantiate device"
|
|
158
|
+
if _pymmcore.version_info >= (11, 5)
|
|
159
|
+
else "Failed to load device"
|
|
160
|
+
)
|
|
161
|
+
with pytest.raises(RuntimeError, match=msg):
|
|
157
162
|
core.loadDevice("newdev", "DemoCamera", "NoSuchDevice")
|
|
158
163
|
|
|
159
164
|
# Then we fallback to checking python modules
|
|
@@ -52,6 +52,7 @@ def test_unicore_xy_stage():
|
|
|
52
52
|
# print status of C-side XY stage device
|
|
53
53
|
assert core.getXYStageDevice() == "XY"
|
|
54
54
|
core.setXYPosition(100, 200)
|
|
55
|
+
core.waitForDevice("XY")
|
|
55
56
|
x, y = core.getXYPosition()
|
|
56
57
|
assert (round(x), round(y)) == (100, 200)
|
|
57
58
|
|
|
@@ -75,6 +76,7 @@ def test_unicore_xy_stage():
|
|
|
75
76
|
# can still set and query the C-side device directly
|
|
76
77
|
core.waitForDevice("XY")
|
|
77
78
|
core.setXYPosition("XY", 1.5, 3.7)
|
|
79
|
+
core.waitForDevice("XY")
|
|
78
80
|
x, y = core.getXYPosition("XY")
|
|
79
81
|
assert (round(x, 1), round(y, 1)) == (1.5, 3.7)
|
|
80
82
|
assert core.getXYPosition(XYDEV) == NEW_POS
|
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
"""Internal module to choose between pymmcore and pymmcore-nano."""
|
|
2
|
-
|
|
3
|
-
try:
|
|
4
|
-
from pymmcore_nano import * # noqa F403
|
|
5
|
-
from pymmcore_nano import __version__
|
|
6
|
-
|
|
7
|
-
BACKEND = "pymmcore-nano"
|
|
8
|
-
NANO = True
|
|
9
|
-
except ImportError:
|
|
10
|
-
from pymmcore import * # noqa F403
|
|
11
|
-
from pymmcore import __version__ # noqa F401
|
|
12
|
-
|
|
13
|
-
BACKEND = "pymmcore"
|
|
14
|
-
NANO = False
|
|
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
|
{pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/core/events/_device_signal_view.py
RENAMED
|
File without changes
|
|
File without changes
|
{pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/core/events/_prop_event_mixin.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/experimental/unicore/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/experimental/unicore/_proxy.py
RENAMED
|
File without changes
|
{pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/experimental/unicore/_unicore.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/mda/handlers/_5d_writer_base.py
RENAMED
|
File without changes
|
|
File without changes
|
{pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/mda/handlers/_img_sequence_writer.py
RENAMED
|
File without changes
|
{pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/mda/handlers/_ome_tiff_writer.py
RENAMED
|
File without changes
|
{pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/mda/handlers/_ome_zarr_writer.py
RENAMED
|
File without changes
|
{pymmcore_plus-0.13.5 → pymmcore_plus-0.13.7}/src/pymmcore_plus/mda/handlers/_tensorstore_handler.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
|
|
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
|
|
File without changes
|