pymmcore-plus 0.15.0__py3-none-any.whl → 0.15.3__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 +25 -0
- pymmcore_plus/_ipy_completion.py +363 -0
- pymmcore_plus/core/_constants.py +4 -0
- pymmcore_plus/core/_mmcore_plus.py +26 -16
- pymmcore_plus/core/_sequencing.py +1 -1
- pymmcore_plus/core/events/_deprecated.py +67 -0
- pymmcore_plus/core/events/_protocol.py +15 -5
- pymmcore_plus/core/events/_psygnal.py +33 -4
- pymmcore_plus/core/events/_qsignals.py +34 -6
- pymmcore_plus/experimental/unicore/__init__.py +7 -3
- pymmcore_plus/experimental/unicore/_device_manager.py +1 -1
- pymmcore_plus/experimental/unicore/core/_sequence_buffer.py +23 -27
- pymmcore_plus/experimental/unicore/core/_unicore.py +90 -23
- pymmcore_plus/experimental/unicore/devices/_camera.py +10 -5
- pymmcore_plus/experimental/unicore/devices/_generic_device.py +12 -0
- pymmcore_plus/experimental/unicore/devices/_properties.py +1 -1
- pymmcore_plus/experimental/unicore/devices/_shutter.py +30 -0
- pymmcore_plus/experimental/unicore/devices/_slm.py +1 -1
- pymmcore_plus/experimental/unicore/devices/_stage.py +1 -1
- pymmcore_plus/experimental/unicore/devices/_state.py +1 -1
- {pymmcore_plus-0.15.0.dist-info → pymmcore_plus-0.15.3.dist-info}/METADATA +2 -2
- {pymmcore_plus-0.15.0.dist-info → pymmcore_plus-0.15.3.dist-info}/RECORD +26 -22
- /pymmcore_plus/experimental/unicore/devices/{_device.py → _device_base.py} +0 -0
- {pymmcore_plus-0.15.0.dist-info → pymmcore_plus-0.15.3.dist-info}/WHEEL +0 -0
- {pymmcore_plus-0.15.0.dist-info → pymmcore_plus-0.15.3.dist-info}/entry_points.txt +0 -0
- {pymmcore_plus-0.15.0.dist-info → pymmcore_plus-0.15.3.dist-info}/licenses/LICENSE +0 -0
pymmcore_plus/__init__.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"""pymmcore superset providing improved APIs, event handling, and a pure python acquisition engine.""" # noqa: E501
|
|
2
2
|
|
|
3
|
+
import logging
|
|
3
4
|
from importlib.metadata import PackageNotFoundError, version
|
|
4
5
|
|
|
5
6
|
try:
|
|
@@ -12,6 +13,7 @@ from ._accumulator import AbstractChangeAccumulator
|
|
|
12
13
|
from ._logger import configure_logging
|
|
13
14
|
from ._util import find_micromanager, use_micromanager
|
|
14
15
|
from .core import (
|
|
16
|
+
ActionType,
|
|
15
17
|
CFGCommand,
|
|
16
18
|
CFGGroup,
|
|
17
19
|
CMMCorePlus,
|
|
@@ -63,3 +65,26 @@ __all__ = [
|
|
|
63
65
|
"find_micromanager",
|
|
64
66
|
"use_micromanager",
|
|
65
67
|
]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _install_ipy_completer() -> None: # pragma: no cover
|
|
71
|
+
import os
|
|
72
|
+
import sys
|
|
73
|
+
|
|
74
|
+
if os.getenv("PYMM_DISABLE_IPYTHON_COMPLETIONS", "0") == "1":
|
|
75
|
+
return
|
|
76
|
+
try:
|
|
77
|
+
if (IPython := sys.modules.get("IPython")) and (shell := IPython.get_ipython()):
|
|
78
|
+
from ._ipy_completion import install_pymmcore_ipy_completion
|
|
79
|
+
|
|
80
|
+
install_pymmcore_ipy_completion(shell)
|
|
81
|
+
except Exception as e:
|
|
82
|
+
# If we fail to install the completer, we don't want to crash the import.
|
|
83
|
+
# This is a best-effort installation.
|
|
84
|
+
logging.warning(
|
|
85
|
+
f"Failed to install pymmcore-plus IPython completer:\n {e}",
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
_install_ipy_completer()
|
|
90
|
+
del _install_ipy_completer
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import TYPE_CHECKING, Callable
|
|
5
|
+
|
|
6
|
+
from IPython import get_ipython # pyright: ignore
|
|
7
|
+
from IPython.core.completer import SimpleCompletion, context_matcher
|
|
8
|
+
|
|
9
|
+
from pymmcore_plus import CMMCorePlus, DeviceType
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from collections.abc import Sequence
|
|
13
|
+
|
|
14
|
+
from IPython.core.completer import (
|
|
15
|
+
CompletionContext,
|
|
16
|
+
SimpleMatcherResult,
|
|
17
|
+
)
|
|
18
|
+
from IPython.core.interactiveshell import InteractiveShell
|
|
19
|
+
|
|
20
|
+
# functions that only require the CMMCorePlus instance to return completions
|
|
21
|
+
CoreCompleter = Callable[[CMMCorePlus], Sequence[SimpleCompletion]]
|
|
22
|
+
# functions that require the CMMCorePlus instance and a label to return completions
|
|
23
|
+
CoreLabelCompleter = Callable[[CMMCorePlus, str], Sequence[SimpleCompletion]]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _get_argument_index(src: str, ns: dict[str, object]) -> int:
|
|
27
|
+
"""Parse argument index from method call string.
|
|
28
|
+
|
|
29
|
+
Uses a simple comma-counting approach that works reliably across
|
|
30
|
+
different backends (pymmcore, pymmcore-nano, etc.) without relying
|
|
31
|
+
on jedi's ability to understand method signatures.
|
|
32
|
+
"""
|
|
33
|
+
p0 = src.rfind("(") + 1
|
|
34
|
+
p1 = src.rfind(")") if ")" in src else None
|
|
35
|
+
if inner := src[slice(p0, p1)].strip():
|
|
36
|
+
# split on commas that are not inside quotes
|
|
37
|
+
return len(inner.split(",")) - 1 # 0-based
|
|
38
|
+
return 0
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# matches "obj.attr(" ... note trailing paren
|
|
42
|
+
OBJ_METHOD_RE = re.compile(r"(?P<obj>[A-Za-z_][\w\.]*)\s*\.\s*(?P<attr>\w+)\s*\(")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _null() -> SimpleMatcherResult:
|
|
46
|
+
return {"completions": [], "matched_fragment": "", "suppress": False}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _dev_labels(
|
|
50
|
+
core: CMMCorePlus, *dev_types: DeviceType, with_null: bool = False
|
|
51
|
+
) -> Sequence[SimpleCompletion]:
|
|
52
|
+
try:
|
|
53
|
+
if not dev_types:
|
|
54
|
+
labels = list(core.getLoadedDevices())
|
|
55
|
+
else:
|
|
56
|
+
labels = [
|
|
57
|
+
d
|
|
58
|
+
for dev_type in dev_types
|
|
59
|
+
for d in core.getLoadedDevicesOfType(dev_type)
|
|
60
|
+
]
|
|
61
|
+
completions = [SimpleCompletion(f'"{lbl}"') for lbl in labels]
|
|
62
|
+
if with_null:
|
|
63
|
+
completions.append(SimpleCompletion("''"))
|
|
64
|
+
return completions
|
|
65
|
+
except Exception: # pragma: no cover
|
|
66
|
+
return []
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _config_group_names(core: CMMCorePlus) -> Sequence[SimpleCompletion]:
|
|
70
|
+
"""Get the names of all configuration groups."""
|
|
71
|
+
try:
|
|
72
|
+
return [
|
|
73
|
+
SimpleCompletion(f'"{name}"') for name in core.getAvailableConfigGroups()
|
|
74
|
+
]
|
|
75
|
+
except Exception: # pragma: no cover
|
|
76
|
+
return []
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _config_preset_names(core: CMMCorePlus, group: str) -> Sequence[SimpleCompletion]:
|
|
80
|
+
"""Get the names of all configuration presets for a given group."""
|
|
81
|
+
try:
|
|
82
|
+
return [
|
|
83
|
+
SimpleCompletion(f'"{name}"') for name in core.getAvailableConfigs(group)
|
|
84
|
+
]
|
|
85
|
+
except Exception: # pragma: no cover
|
|
86
|
+
return []
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
# fmt: off
|
|
90
|
+
# map of (method_name, arg_index) -> function that returns possible completions
|
|
91
|
+
CORE_COMPLETERS: dict[tuple[str, int], CoreCompleter] = {
|
|
92
|
+
# ROLES -----------------------------------------------------------------------
|
|
93
|
+
("setAutoFocusDevice", 0): lambda core: _dev_labels(core, DeviceType.AutoFocus, with_null=True), # noqa
|
|
94
|
+
("setCameraDevice", 0): lambda core: _dev_labels(core, DeviceType.Camera, with_null=True), # noqa
|
|
95
|
+
("setFocusDevice", 0): lambda core: _dev_labels(core, DeviceType.Stage, with_null=True), # noqa
|
|
96
|
+
("setGalvoDevice", 0): lambda core: _dev_labels(core, DeviceType.Galvo, with_null=True), # noqa
|
|
97
|
+
("setImageProcessorDevice", 0): lambda core: _dev_labels(core, DeviceType.ImageProcessor, with_null=True), # noqa
|
|
98
|
+
("setShutterDevice", 0): lambda core: _dev_labels(core, DeviceType.Shutter, with_null=True), # noqa
|
|
99
|
+
("setSLMDevice", 0): lambda core: _dev_labels(core, DeviceType.SLM, with_null=True),
|
|
100
|
+
("setXYStageDevice", 0): lambda core: _dev_labels(core, DeviceType.XYStage, with_null=True), # noqa
|
|
101
|
+
# -----------------------
|
|
102
|
+
("getFocusDirection", 0): lambda core: _dev_labels(core, DeviceType.Stage),
|
|
103
|
+
("getPosition", 0): lambda core: _dev_labels(core, DeviceType.Stage),
|
|
104
|
+
("getStageSequenceMaxLength", 0): lambda core: _dev_labels(core, DeviceType.Stage),
|
|
105
|
+
("home", 0): lambda core: _dev_labels(core, DeviceType.Stage, DeviceType.XYStage),
|
|
106
|
+
("isContinuousFocusDrive", 0): lambda core: _dev_labels(core, DeviceType.Stage),
|
|
107
|
+
("isStageLinearSequenceable", 0): lambda core: _dev_labels(core, DeviceType.Stage),
|
|
108
|
+
("isStageSequenceable", 0): lambda core: _dev_labels(core, DeviceType.Stage),
|
|
109
|
+
("loadStageSequence", 0): lambda core: _dev_labels(core, DeviceType.Stage),
|
|
110
|
+
("setAdapterOrigin", 0): lambda core: _dev_labels(core, DeviceType.Stage),
|
|
111
|
+
("setFocusDirection", 0): lambda core: _dev_labels(core, DeviceType.Stage),
|
|
112
|
+
("setOrigin", 0): lambda core: _dev_labels(core, DeviceType.Stage),
|
|
113
|
+
("setPosition", 0): lambda core: _dev_labels(core, DeviceType.Stage),
|
|
114
|
+
("setRelativePosition", 0): lambda core: _dev_labels(core, DeviceType.Stage),
|
|
115
|
+
("setStageLinearSequence", 0): lambda core: _dev_labels(core, DeviceType.Stage),
|
|
116
|
+
("startStageSequence", 0): lambda core: _dev_labels(core, DeviceType.Stage),
|
|
117
|
+
("stop", 0): lambda core: _dev_labels(core, DeviceType.Stage, DeviceType.XYStage),
|
|
118
|
+
("stopStageSequence", 0): lambda core: _dev_labels(core, DeviceType.Stage),
|
|
119
|
+
# --------------------
|
|
120
|
+
("getXPosition", 0): lambda core: _dev_labels(core, DeviceType.XYStage),
|
|
121
|
+
("getXYPosition", 0): lambda core: _dev_labels(core, DeviceType.XYStage),
|
|
122
|
+
("getXYStageSequenceMaxLength", 0): lambda core: _dev_labels(core, DeviceType.XYStage), # noqa
|
|
123
|
+
("getYPosition", 0): lambda core: _dev_labels(core, DeviceType.XYStage),
|
|
124
|
+
("isXYStageSequenceable", 0): lambda core: _dev_labels(core, DeviceType.XYStage),
|
|
125
|
+
("loadXYStageSequence", 0): lambda core: _dev_labels(core, DeviceType.XYStage),
|
|
126
|
+
("setAdapterOriginXY", 0): lambda core: _dev_labels(core, DeviceType.XYStage),
|
|
127
|
+
("setOriginX", 0): lambda core: _dev_labels(core, DeviceType.XYStage),
|
|
128
|
+
("setOriginXY", 0): lambda core: _dev_labels(core, DeviceType.XYStage),
|
|
129
|
+
("setOriginY", 0): lambda core: _dev_labels(core, DeviceType.XYStage),
|
|
130
|
+
("setRelativeXYPosition", 0): lambda core: _dev_labels(core, DeviceType.XYStage),
|
|
131
|
+
("setXYPosition", 0): lambda core: _dev_labels(core, DeviceType.XYStage),
|
|
132
|
+
("startXYStageSequence", 0): lambda core: _dev_labels(core, DeviceType.XYStage),
|
|
133
|
+
("stopXYStageSequence", 0): lambda core: _dev_labels(core, DeviceType.XYStage),
|
|
134
|
+
# --------------------
|
|
135
|
+
("deleteConfig", 2): _dev_labels,
|
|
136
|
+
("detectDevice", 0): _dev_labels,
|
|
137
|
+
("deviceBusy", 0): _dev_labels,
|
|
138
|
+
("getAllowedPropertyValues", 0): _dev_labels,
|
|
139
|
+
("getDeviceDelayMs", 0): _dev_labels,
|
|
140
|
+
("getDeviceDescription", 0): _dev_labels,
|
|
141
|
+
("getDeviceInitializationState", 0): _dev_labels,
|
|
142
|
+
("getDeviceLibrary", 0): _dev_labels,
|
|
143
|
+
("getDeviceName", 0): _dev_labels,
|
|
144
|
+
("getDevicePropertyNames", 0): _dev_labels,
|
|
145
|
+
("getDeviceType", 0): _dev_labels,
|
|
146
|
+
("getParentLabel", 0): _dev_labels,
|
|
147
|
+
("getProperty", 0): _dev_labels,
|
|
148
|
+
("getPropertyFromCache", 0): _dev_labels,
|
|
149
|
+
("getPropertyLowerLimit", 0): _dev_labels, # ?
|
|
150
|
+
("getPropertySequenceMaxLength", 0): _dev_labels, # ?
|
|
151
|
+
("getPropertyType", 0): _dev_labels,
|
|
152
|
+
("getPropertyUpperLimit", 0): _dev_labels, # ?
|
|
153
|
+
("hasProperty", 0): _dev_labels,
|
|
154
|
+
("hasPropertyLimits", 0): _dev_labels,
|
|
155
|
+
("initializeDevice", 0): _dev_labels,
|
|
156
|
+
("isPropertyPreInit", 0): _dev_labels,
|
|
157
|
+
("isPropertyReadOnly", 0): _dev_labels,
|
|
158
|
+
("isPropertySequenceable", 0): _dev_labels,
|
|
159
|
+
("loadPropertySequence", 0): _dev_labels, # ?
|
|
160
|
+
("setDeviceDelayMs", 0): _dev_labels,
|
|
161
|
+
("setParentLabel", 0): _dev_labels,
|
|
162
|
+
("setProperty", 0): _dev_labels,
|
|
163
|
+
("startPropertySequence", 0): _dev_labels, # ?
|
|
164
|
+
("unloadDevice", 0): _dev_labels,
|
|
165
|
+
("usesDeviceDelay", 0): _dev_labels,
|
|
166
|
+
("waitForDevice", 0): _dev_labels,
|
|
167
|
+
# --------------------
|
|
168
|
+
("displaySLMImage", 0): lambda core: _dev_labels(core, DeviceType.SLM),
|
|
169
|
+
("getSLMBytesPerPixel", 0): lambda core: _dev_labels(core, DeviceType.SLM),
|
|
170
|
+
("getSLMExposure", 0): lambda core: _dev_labels(core, DeviceType.SLM),
|
|
171
|
+
("getSLMHeight", 0): lambda core: _dev_labels(core, DeviceType.SLM),
|
|
172
|
+
("getSLMNumberOfComponents", 0): lambda core: _dev_labels(core, DeviceType.SLM),
|
|
173
|
+
("getSLMSequenceMaxLength", 0): lambda core: _dev_labels(core, DeviceType.SLM),
|
|
174
|
+
("getSLMWidth", 0): lambda core: _dev_labels(core, DeviceType.SLM),
|
|
175
|
+
("loadSLMSequence", 0): lambda core: _dev_labels(core, DeviceType.SLM),
|
|
176
|
+
("setSLMExposure", 0): lambda core: _dev_labels(core, DeviceType.SLM),
|
|
177
|
+
("setSLMImage", 0): lambda core: _dev_labels(core, DeviceType.SLM),
|
|
178
|
+
("setSLMPixelsTo", 0): lambda core: _dev_labels(core, DeviceType.SLM),
|
|
179
|
+
("startSLMSequence", 0): lambda core: _dev_labels(core, DeviceType.SLM),
|
|
180
|
+
("stopSLMSequence", 0): lambda core: _dev_labels(core, DeviceType.SLM),
|
|
181
|
+
# --------------------
|
|
182
|
+
("deleteGalvoPolygons", 0): lambda core: _dev_labels(core, DeviceType.Galvo),
|
|
183
|
+
("getGalvoChannels", 0): lambda core: _dev_labels(core, DeviceType.Galvo),
|
|
184
|
+
("getGalvoPosition", 0): lambda core: _dev_labels(core, DeviceType.Galvo),
|
|
185
|
+
("getGalvoXMinimum", 0): lambda core: _dev_labels(core, DeviceType.Galvo),
|
|
186
|
+
("getGalvoXRange", 0): lambda core: _dev_labels(core, DeviceType.Galvo),
|
|
187
|
+
("getGalvoYMinimum", 0): lambda core: _dev_labels(core, DeviceType.Galvo),
|
|
188
|
+
("getGalvoYRange", 0): lambda core: _dev_labels(core, DeviceType.Galvo),
|
|
189
|
+
("loadGalvoPolygons", 0): lambda core: _dev_labels(core, DeviceType.Galvo),
|
|
190
|
+
("pointGalvoAndFire", 0): lambda core: _dev_labels(core, DeviceType.Galvo),
|
|
191
|
+
("runGalvoPolygons", 0): lambda core: _dev_labels(core, DeviceType.Galvo),
|
|
192
|
+
("runGalvoSequence", 0): lambda core: _dev_labels(core, DeviceType.Galvo),
|
|
193
|
+
("setGalvoIlluminationState", 0): lambda core: _dev_labels(core, DeviceType.Galvo),
|
|
194
|
+
("setGalvoPolygonRepetitions", 0): lambda core: _dev_labels(core, DeviceType.Galvo),
|
|
195
|
+
("setGalvoPosition", 0): lambda core: _dev_labels(core, DeviceType.Galvo),
|
|
196
|
+
("setGalvoSpotInterval", 0): lambda core: _dev_labels(core, DeviceType.Galvo),
|
|
197
|
+
# --------------------
|
|
198
|
+
("getExposure", 0): lambda core: _dev_labels(core, DeviceType.Camera),
|
|
199
|
+
("getExposureSequenceMaxLength", 0): lambda core: _dev_labels(core, DeviceType.Camera), # noqa
|
|
200
|
+
("getROI", 0): lambda core: _dev_labels(core, DeviceType.Camera),
|
|
201
|
+
("isExposureSequenceable", 0): lambda core: _dev_labels(core, DeviceType.Camera),
|
|
202
|
+
("isSequenceRunning", 0): lambda core: _dev_labels(core, DeviceType.Camera),
|
|
203
|
+
("loadExposureSequence", 0): lambda core: _dev_labels(core, DeviceType.Camera),
|
|
204
|
+
("prepareSequenceAcquisition", 0): lambda core: _dev_labels(core, DeviceType.Camera), # noqa
|
|
205
|
+
("setExposure", 0): lambda core: _dev_labels(core, DeviceType.Camera),
|
|
206
|
+
("setROI", 0): lambda core: _dev_labels(core, DeviceType.Camera),
|
|
207
|
+
("startExposureSequence", 0): lambda core: _dev_labels(core, DeviceType.Camera),
|
|
208
|
+
("startSequenceAcquisition", 0): lambda core: _dev_labels(core, DeviceType.Camera),
|
|
209
|
+
("stopExposureSequence", 0): lambda core: _dev_labels(core, DeviceType.Camera),
|
|
210
|
+
("stopSequenceAcquisition", 0): lambda core: _dev_labels(core, DeviceType.Camera),
|
|
211
|
+
# --------------------
|
|
212
|
+
("defineStateLabel", 0): lambda core: _dev_labels(core, DeviceType.State),
|
|
213
|
+
("getNumberOfStates", 0): lambda core: _dev_labels(core, DeviceType.State),
|
|
214
|
+
("getState", 0): lambda core: _dev_labels(core, DeviceType.State),
|
|
215
|
+
("getStateFromLabel", 0): lambda core: _dev_labels(core, DeviceType.State),
|
|
216
|
+
("getStateLabel", 0): lambda core: _dev_labels(core, DeviceType.State),
|
|
217
|
+
("getStateLabels", 0): lambda core: _dev_labels(core, DeviceType.State),
|
|
218
|
+
("setState", 0): lambda core: _dev_labels(core, DeviceType.State),
|
|
219
|
+
("setStateLabel", 0): lambda core: _dev_labels(core, DeviceType.State),
|
|
220
|
+
# --------------------
|
|
221
|
+
("getShutterOpen", 0): lambda core: _dev_labels(core, DeviceType.Shutter),
|
|
222
|
+
("setShutterOpen", 0): lambda core: _dev_labels(core, DeviceType.Shutter),
|
|
223
|
+
# --------------------
|
|
224
|
+
("getInstalledDeviceDescriptions", 0): lambda core: _dev_labels(core, DeviceType.Hub), # noqa
|
|
225
|
+
("getInstalledDevices", 0): lambda core: _dev_labels(core, DeviceType.Hub),
|
|
226
|
+
("getLoadedPeripheralDevices", 0): lambda core: _dev_labels(core, DeviceType.Hub),
|
|
227
|
+
("getSerialPortAnswer", 0): lambda core: _dev_labels(core, DeviceType.Serial),
|
|
228
|
+
("readFromSerialPort", 0): lambda core: _dev_labels(core, DeviceType.Serial),
|
|
229
|
+
("setParentLabel", 1): lambda core: _dev_labels(core, DeviceType.Hub),
|
|
230
|
+
|
|
231
|
+
# ----------------- Config Groups --------------------
|
|
232
|
+
("deleteConfig", 0): _config_group_names,
|
|
233
|
+
("deleteConfigGroup", 0): _config_group_names,
|
|
234
|
+
("getAvailableConfigs", 0): _config_group_names,
|
|
235
|
+
("getConfigData", 0): _config_group_names,
|
|
236
|
+
("getConfigGroupState", 0): _config_group_names,
|
|
237
|
+
("getConfigGroupStateFromCache", 0): _config_group_names,
|
|
238
|
+
("getConfigState", 0): _config_group_names,
|
|
239
|
+
("getCurrentConfig", 0): _config_group_names,
|
|
240
|
+
("getCurrentConfigFromCache", 0): _config_group_names,
|
|
241
|
+
("renameConfig", 0): _config_group_names,
|
|
242
|
+
("renameConfigGroup", 0): _config_group_names,
|
|
243
|
+
("setChannelGroup", 0): _config_group_names,
|
|
244
|
+
("setConfig", 0): _config_group_names,
|
|
245
|
+
("waitForConfig", 0): _config_group_names,
|
|
246
|
+
}
|
|
247
|
+
# fmt: on
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _get_prop_names(core: CMMCorePlus, device: str) -> Sequence[SimpleCompletion]:
|
|
251
|
+
"""Get the property names for a given device."""
|
|
252
|
+
try:
|
|
253
|
+
return [
|
|
254
|
+
SimpleCompletion(f'"{prop}"')
|
|
255
|
+
for prop in core.getDevicePropertyNames(device)
|
|
256
|
+
]
|
|
257
|
+
except Exception: # pragma: no cover
|
|
258
|
+
return []
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
# fmt: off
|
|
262
|
+
# Map of (method_name, arg_index) -> (label arg idx, function that returns completions)
|
|
263
|
+
LABEL_COMPLETERS: dict[tuple[str, int], tuple[int, CoreLabelCompleter]] = {
|
|
264
|
+
("define", 3): (2, _get_prop_names),
|
|
265
|
+
("definePixelSizeConfig", 2): (1, _get_prop_names),
|
|
266
|
+
("deleteConfig", 3): (2, _get_prop_names),
|
|
267
|
+
("getAllowedPropertyValues", 1): (0, _get_prop_names),
|
|
268
|
+
("getProperty", 1): (0, _get_prop_names),
|
|
269
|
+
("getPropertyFromCache", 1): (0, _get_prop_names),
|
|
270
|
+
("getPropertyLowerLimit", 1): (0, _get_prop_names),
|
|
271
|
+
("getPropertySequenceMaxLength", 1): (0, _get_prop_names),
|
|
272
|
+
("getPropertyType", 1): (0, _get_prop_names),
|
|
273
|
+
("getPropertyUpperLimit", 1): (0, _get_prop_names),
|
|
274
|
+
("hasProperty", 1): (0, _get_prop_names),
|
|
275
|
+
("hasPropertyLimits", 1): (0, _get_prop_names),
|
|
276
|
+
("isPropertyPreInit", 1): (0, _get_prop_names),
|
|
277
|
+
("isPropertyReadOnly", 1): (0, _get_prop_names),
|
|
278
|
+
("isPropertySequenceable", 1): (0, _get_prop_names),
|
|
279
|
+
("loadPropertySequence", 1): (0, _get_prop_names),
|
|
280
|
+
("setProperty", 1): (0, _get_prop_names),
|
|
281
|
+
("startPropertySequence", 1): (0, _get_prop_names),
|
|
282
|
+
("stopPropertySequence", 1): (0, _get_prop_names),
|
|
283
|
+
# ----------------- Config Presets --------------------
|
|
284
|
+
("deleteConfig", 1): (0, _config_preset_names),
|
|
285
|
+
("getConfigData", 1): (0, _config_preset_names),
|
|
286
|
+
("getConfigState", 1): (0, _config_preset_names),
|
|
287
|
+
("renameConfig", 1): (0, _config_preset_names),
|
|
288
|
+
("setConfig", 1): (0, _config_preset_names),
|
|
289
|
+
("waitForConfig", 1): (0, _config_preset_names),
|
|
290
|
+
}
|
|
291
|
+
# fmt: on
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
@context_matcher() # type: ignore[misc]
|
|
295
|
+
def cmmcoreplus_matcher(ctx: CompletionContext) -> SimpleMatcherResult:
|
|
296
|
+
"""
|
|
297
|
+
Offer string completions for CMMCorePlus calls such as.
|
|
298
|
+
|
|
299
|
+
core.setCameraDevice(<TAB>)
|
|
300
|
+
core.setProperty("CAM", <TAB>)
|
|
301
|
+
"""
|
|
302
|
+
if not (ip := get_ipython()):
|
|
303
|
+
return _null() # pragma: no cover
|
|
304
|
+
ns = ip.user_ns # live user namespace
|
|
305
|
+
|
|
306
|
+
# 1. Extract the object expression and the *real* attribute name syntactically.
|
|
307
|
+
# e.g.: 'core.setCameraDevice('
|
|
308
|
+
src_to_cursor = ctx.full_text[: ctx.cursor_position]
|
|
309
|
+
if not (m := OBJ_METHOD_RE.search(src_to_cursor)):
|
|
310
|
+
return _null()
|
|
311
|
+
|
|
312
|
+
# 2. Ensure we're dealing with a CMMCorePlus method
|
|
313
|
+
# e.g. ('core', 'setCameraDevice')
|
|
314
|
+
var_expr, method_name = m.group("obj"), m.group("attr")
|
|
315
|
+
try:
|
|
316
|
+
obj = eval(var_expr, ns)
|
|
317
|
+
except Exception:
|
|
318
|
+
return _null()
|
|
319
|
+
|
|
320
|
+
if not isinstance(obj, CMMCorePlus):
|
|
321
|
+
return _null()
|
|
322
|
+
|
|
323
|
+
# 3. Get the argument index for the method call.
|
|
324
|
+
arg_index = _get_argument_index(src_to_cursor, ns)
|
|
325
|
+
|
|
326
|
+
# 4. Check if we have a specific completion for this method name and arg_index.
|
|
327
|
+
key = (method_name, arg_index)
|
|
328
|
+
if (dev_getter := CORE_COMPLETERS.get(key)) and (completions := dev_getter(obj)):
|
|
329
|
+
# If we have a specific suggestion for this method and arg_index, use it.
|
|
330
|
+
return {"completions": completions, "suppress": True}
|
|
331
|
+
|
|
332
|
+
if info := LABEL_COMPLETERS.get(key):
|
|
333
|
+
dev_idx, getter = info
|
|
334
|
+
if dev_label := _get_argument(src_to_cursor, dev_idx):
|
|
335
|
+
if completions := getter(obj, dev_label):
|
|
336
|
+
return {"completions": completions, "suppress": True}
|
|
337
|
+
|
|
338
|
+
return _null()
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def _get_argument(src: str, index: int) -> str:
|
|
342
|
+
"""Parse the argument at the given index from a method call string.
|
|
343
|
+
|
|
344
|
+
For example:
|
|
345
|
+
>>> _get_argument("core.getProperty('Camera', ", 0)
|
|
346
|
+
'Camera'
|
|
347
|
+
"""
|
|
348
|
+
p0 = src.find("(") + 1
|
|
349
|
+
p1 = src.rfind(")") if ")" in src else None
|
|
350
|
+
if inner := src[slice(p0, p1)].strip():
|
|
351
|
+
args = inner.split(",")
|
|
352
|
+
if index < len(args):
|
|
353
|
+
return args[index].strip().strip("'\"")
|
|
354
|
+
return "" # pragma: no cover
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def install_pymmcore_ipy_completion(shell: InteractiveShell | None = None) -> None:
|
|
358
|
+
"""Install the CMMCorePlus completion matcher in the current IPython session."""
|
|
359
|
+
if shell is None and not (shell := get_ipython()):
|
|
360
|
+
return # pragma: no cover
|
|
361
|
+
|
|
362
|
+
if cmmcoreplus_matcher not in shell.Completer.custom_matchers:
|
|
363
|
+
shell.Completer.custom_matchers.append(cmmcoreplus_matcher)
|
pymmcore_plus/core/_constants.py
CHANGED
|
@@ -142,6 +142,8 @@ class DeviceType(IntEnum):
|
|
|
142
142
|
SLMDevice = pymmcore.SLMDevice
|
|
143
143
|
HubDevice = pymmcore.HubDevice
|
|
144
144
|
GalvoDevice = pymmcore.GalvoDevice
|
|
145
|
+
PressurePumpDevice = pymmcore.PressurePumpDevice
|
|
146
|
+
VolumetricPumpDevice = pymmcore.VolumetricPumpDevice
|
|
145
147
|
# aliases for clearer naming (e.g. `DeviceType.Camera`)
|
|
146
148
|
Unknown = UnknownType
|
|
147
149
|
Any = AnyType
|
|
@@ -160,6 +162,8 @@ class DeviceType(IntEnum):
|
|
|
160
162
|
SLM = SLMDevice
|
|
161
163
|
Hub = HubDevice
|
|
162
164
|
Galvo = GalvoDevice
|
|
165
|
+
PressurePump = PressurePumpDevice
|
|
166
|
+
VolumetricPump = VolumetricPumpDevice
|
|
163
167
|
|
|
164
168
|
def __str__(self) -> str:
|
|
165
169
|
return str(self.name).replace("Type", "").replace("Device", "")
|
|
@@ -335,7 +335,7 @@ class CMMCorePlus(pymmcore.CMMCore):
|
|
|
335
335
|
if hasattr(self, "_weak_clean"):
|
|
336
336
|
atexit.unregister(self._weak_clean)
|
|
337
337
|
try:
|
|
338
|
-
super().registerCallback(None)
|
|
338
|
+
super().registerCallback(None)
|
|
339
339
|
self.reset()
|
|
340
340
|
# clean up logging
|
|
341
341
|
self.setPrimaryLogFile("")
|
|
@@ -1626,7 +1626,7 @@ class CMMCorePlus(pymmcore.CMMCore):
|
|
|
1626
1626
|
self.events.propertyChanged.emit(self.getShutterDevice(), "State", True)
|
|
1627
1627
|
try:
|
|
1628
1628
|
self._do_snap_image()
|
|
1629
|
-
self.events.imageSnapped.emit()
|
|
1629
|
+
self.events.imageSnapped.emit(self.getCameraDevice())
|
|
1630
1630
|
finally:
|
|
1631
1631
|
if autoshutter:
|
|
1632
1632
|
self.events.propertyChanged.emit(
|
|
@@ -2037,26 +2037,27 @@ class CMMCorePlus(pymmcore.CMMCore):
|
|
|
2037
2037
|
self.events.autoShutterSet.emit(state)
|
|
2038
2038
|
|
|
2039
2039
|
@overload
|
|
2040
|
-
def setShutterOpen(self, state: bool) -> None: ...
|
|
2041
|
-
|
|
2040
|
+
def setShutterOpen(self, state: bool, /) -> None: ...
|
|
2042
2041
|
@overload
|
|
2043
|
-
def setShutterOpen(self, shutterLabel: str, state: bool) -> None: ...
|
|
2044
|
-
|
|
2045
|
-
def setShutterOpen(self, *args: Any, **kwargs: Any) -> None:
|
|
2042
|
+
def setShutterOpen(self, shutterLabel: str, state: bool, /) -> None: ...
|
|
2043
|
+
def setShutterOpen(self, *args: Any) -> None:
|
|
2046
2044
|
"""Open or close the currently selected or `shutterLabel` shutter.
|
|
2047
2045
|
|
|
2048
2046
|
**Why Override?** To emit a `propertyChanged` event.
|
|
2049
2047
|
"""
|
|
2050
|
-
super().setShutterOpen(*args, **kwargs)
|
|
2051
|
-
shutterLabel, state = kwargs.get("shutterLabel"), kwargs.get("state")
|
|
2052
2048
|
if len(args) == 2:
|
|
2053
2049
|
shutterLabel, state = args
|
|
2054
2050
|
elif len(args) == 1:
|
|
2055
2051
|
shutterLabel = super().getShutterDevice()
|
|
2056
2052
|
state = args[0]
|
|
2053
|
+
self._do_shutter_open(shutterLabel, state)
|
|
2057
2054
|
state = str(int(bool(state)))
|
|
2058
2055
|
self.events.propertyChanged.emit(shutterLabel, "State", state)
|
|
2059
2056
|
|
|
2057
|
+
def _do_shutter_open(self, shutterLabel: str, state: bool, /) -> None:
|
|
2058
|
+
"""Open or close the shutter."""
|
|
2059
|
+
super().setShutterOpen(shutterLabel, state)
|
|
2060
|
+
|
|
2060
2061
|
@overload
|
|
2061
2062
|
def deleteConfig(self, groupName: str, configName: str) -> None: ...
|
|
2062
2063
|
|
|
@@ -2384,8 +2385,9 @@ class CMMCorePlus(pymmcore.CMMCore):
|
|
|
2384
2385
|
try:
|
|
2385
2386
|
before = [self.getProperty(device, p) for p in properties]
|
|
2386
2387
|
except Exception as e:
|
|
2387
|
-
logger.
|
|
2388
|
-
"Error getting properties %s on %s: %s.
|
|
2388
|
+
logger.warning(
|
|
2389
|
+
"Error getting properties %s on %s: %s. "
|
|
2390
|
+
"Cannot ensure propertyChanged signal emission",
|
|
2389
2391
|
properties,
|
|
2390
2392
|
device,
|
|
2391
2393
|
e,
|
|
@@ -2539,6 +2541,13 @@ for name in (
|
|
|
2539
2541
|
).strip()
|
|
2540
2542
|
)
|
|
2541
2543
|
|
|
2544
|
+
MMCORE_SIGNAL_NAMES = {n for n in dir(pymmcore.MMEventCallback) if n.startswith("on")}
|
|
2545
|
+
_SKIP = {
|
|
2546
|
+
"onImageSnapped",
|
|
2547
|
+
"onSequenceAcquisitionStopped",
|
|
2548
|
+
"onSequenceAcquisitionStarted",
|
|
2549
|
+
}
|
|
2550
|
+
|
|
2542
2551
|
|
|
2543
2552
|
class _MMCallbackRelay(pymmcore.MMEventCallback):
|
|
2544
2553
|
"""Relays MMEventCallback methods to CMMCorePlus.signal."""
|
|
@@ -2549,6 +2558,11 @@ class _MMCallbackRelay(pymmcore.MMEventCallback):
|
|
|
2549
2558
|
|
|
2550
2559
|
@staticmethod
|
|
2551
2560
|
def make_reemitter(name: str) -> Callable[..., None]:
|
|
2561
|
+
# until we debug issues passing signals emitted on other threads into Qt,
|
|
2562
|
+
# we skip these signals (which already had their own pymmcore-plus emitters)
|
|
2563
|
+
if name in _SKIP:
|
|
2564
|
+
return lambda self, *args: None
|
|
2565
|
+
|
|
2552
2566
|
sig_name = name[2].lower() + name[3:]
|
|
2553
2567
|
|
|
2554
2568
|
def reemit(self: _MMCallbackRelay, *args: Any) -> None:
|
|
@@ -2565,9 +2579,5 @@ class _MMCallbackRelay(pymmcore.MMEventCallback):
|
|
|
2565
2579
|
MMCallbackRelay = type(
|
|
2566
2580
|
"MMCallbackRelay",
|
|
2567
2581
|
(_MMCallbackRelay,),
|
|
2568
|
-
{
|
|
2569
|
-
n: _MMCallbackRelay.make_reemitter(n)
|
|
2570
|
-
for n in dir(pymmcore.MMEventCallback)
|
|
2571
|
-
if n.startswith("on")
|
|
2572
|
-
},
|
|
2582
|
+
{n: _MMCallbackRelay.make_reemitter(n) for n in MMCORE_SIGNAL_NAMES},
|
|
2573
2583
|
)
|
|
@@ -66,7 +66,7 @@ class SequencedEvent(MDAEvent):
|
|
|
66
66
|
slm_sequence: tuple[bytes, ...] = Field(default_factory=tuple)
|
|
67
67
|
|
|
68
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:
|
|
69
|
+
sequence: Optional[MDASequence] = Field(default=None, repr=False) # noqa: UP045
|
|
70
70
|
|
|
71
71
|
# all other property sequences
|
|
72
72
|
property_sequences: dict[tuple[str, str], list[str]] = Field(default_factory=dict)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import inspect
|
|
4
|
+
import warnings
|
|
5
|
+
from typing import TYPE_CHECKING, Any, Callable
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from ._protocol import PSignalInstance
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class DeprecatedSignalProxy:
|
|
12
|
+
def __init__(
|
|
13
|
+
self,
|
|
14
|
+
signal: PSignalInstance,
|
|
15
|
+
current_n_args: int,
|
|
16
|
+
deprecated_posargs: tuple[Any, ...],
|
|
17
|
+
) -> None:
|
|
18
|
+
self._signal = signal
|
|
19
|
+
self._current_n_args = current_n_args
|
|
20
|
+
self._deprecated_posargs = deprecated_posargs
|
|
21
|
+
self._shims: dict[Callable, Callable] = {}
|
|
22
|
+
|
|
23
|
+
def connect(self, slot: Callable) -> Any:
|
|
24
|
+
"""Connect slot to this signal."""
|
|
25
|
+
min_pos_args = sum(
|
|
26
|
+
1
|
|
27
|
+
for p in inspect.signature(slot).parameters.values()
|
|
28
|
+
if p.kind
|
|
29
|
+
in {
|
|
30
|
+
inspect.Parameter.POSITIONAL_OR_KEYWORD,
|
|
31
|
+
inspect.Parameter.POSITIONAL_ONLY,
|
|
32
|
+
}
|
|
33
|
+
and p.default is inspect.Parameter.empty
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
num_extra_pos_args = min_pos_args - self._current_n_args
|
|
37
|
+
if num_extra_pos_args > 0:
|
|
38
|
+
extra_args = self._deprecated_posargs[:num_extra_pos_args]
|
|
39
|
+
warnings.warn(
|
|
40
|
+
f"Callback {slot.__name__!r} requires {min_pos_args} positional "
|
|
41
|
+
f"arguments, but this signal only supports {self._current_n_args}. "
|
|
42
|
+
"Fake arguments will be added, but this will be an exception in the "
|
|
43
|
+
"future. Please update your callback.",
|
|
44
|
+
FutureWarning,
|
|
45
|
+
stacklevel=2,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def _shim(*args: Any) -> Any:
|
|
49
|
+
slot(*args[: self._current_n_args], *extra_args)
|
|
50
|
+
|
|
51
|
+
self._shims[slot] = _shim
|
|
52
|
+
self._signal.connect(_shim)
|
|
53
|
+
else:
|
|
54
|
+
self._signal.connect(slot)
|
|
55
|
+
|
|
56
|
+
def disconnect(self, slot: Callable | None = None) -> Any:
|
|
57
|
+
"""Disconnect slot from this signal.
|
|
58
|
+
|
|
59
|
+
If `None`, all slots should be disconnected.
|
|
60
|
+
"""
|
|
61
|
+
if slot in self._shims:
|
|
62
|
+
slot = self._shims.pop(slot)
|
|
63
|
+
return self._signal.disconnect(slot)
|
|
64
|
+
|
|
65
|
+
def emit(self, *args: Any) -> Any:
|
|
66
|
+
"""Emits the signal with the given arguments."""
|
|
67
|
+
return self._signal.emit(*args[: self._current_n_args])
|
|
@@ -109,7 +109,7 @@ class PCoreSignaler(Protocol):
|
|
|
109
109
|
propertiesChanged: ClassVar[PSignal]
|
|
110
110
|
"""Emits with no arguments when properties have changed."""
|
|
111
111
|
propertyChanged: ClassVar[PSignal]
|
|
112
|
-
"""Emits `(name: str,
|
|
112
|
+
"""Emits `(name: str, propName: str, propValue: str)` when a specific property has changed.""" # noqa: E501
|
|
113
113
|
channelGroupChanged: ClassVar[PSignal]
|
|
114
114
|
"""Emits `(newChannelGroupName: str)` when a channel group has changed."""
|
|
115
115
|
configGroupChanged: ClassVar[PSignal]
|
|
@@ -159,15 +159,25 @@ class PCoreSignaler(Protocol):
|
|
|
159
159
|
> :sparkles: This signal is unique to `pymmcore-plus`.
|
|
160
160
|
"""
|
|
161
161
|
sequenceAcquisitionStarting: ClassVar[PSignal]
|
|
162
|
-
"""Emits `(str,
|
|
162
|
+
"""Emits `(str,)` *before* sequence acquisition is started.
|
|
163
163
|
|
|
164
|
-
|
|
164
|
+
!!! critical "Breaking Change"
|
|
165
|
+
In `pymmcore-plus` version 0.16.0, the `sequenceAcquisitionStarting` signal
|
|
166
|
+
was *reduced* from `(str, int, float, bool)` to `(str,)` to match the signature
|
|
167
|
+
emitted by the C++ MMCore library.
|
|
168
|
+
|
|
169
|
+
`(cameraLabel,)`
|
|
165
170
|
> :sparkles: This signal is unique to `pymmcore-plus`.
|
|
166
171
|
"""
|
|
167
172
|
sequenceAcquisitionStarted: ClassVar[PSignal]
|
|
168
|
-
"""Emits `(str
|
|
173
|
+
"""Emits `(str)` *after* sequence acquisition has started.
|
|
174
|
+
|
|
175
|
+
!!! critical "Breaking Change"
|
|
176
|
+
In `pymmcore-plus` version 0.16.0, the `sequenceAcquisitionStarted` signal
|
|
177
|
+
was *reduced* from `(str, int, float, bool)` to `(str,)` to match the signature
|
|
178
|
+
emitted by the C++ MMCore library.
|
|
169
179
|
|
|
170
|
-
(cameraLabel,
|
|
180
|
+
(cameraLabel,)
|
|
171
181
|
> :sparkles: This signal is unique to `pymmcore-plus`.
|
|
172
182
|
"""
|
|
173
183
|
sequenceAcquisitionStopped: ClassVar[PSignal]
|