pymmcore-plus 0.14.0__py3-none-any.whl → 0.15.2__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.
Files changed (31) hide show
  1. pymmcore_plus/__init__.py +25 -0
  2. pymmcore_plus/_ipy_completion.py +363 -0
  3. pymmcore_plus/_pymmcore.py +4 -2
  4. pymmcore_plus/core/_constants.py +25 -3
  5. pymmcore_plus/core/_mmcore_plus.py +110 -55
  6. pymmcore_plus/core/_sequencing.py +1 -1
  7. pymmcore_plus/core/events/_deprecated.py +67 -0
  8. pymmcore_plus/core/events/_protocol.py +64 -39
  9. pymmcore_plus/core/events/_psygnal.py +35 -6
  10. pymmcore_plus/core/events/_qsignals.py +34 -6
  11. pymmcore_plus/experimental/unicore/__init__.py +12 -2
  12. pymmcore_plus/experimental/unicore/_device_manager.py +1 -1
  13. pymmcore_plus/experimental/unicore/_proxy.py +20 -3
  14. pymmcore_plus/experimental/unicore/core/__init__.py +0 -0
  15. pymmcore_plus/experimental/unicore/core/_sequence_buffer.py +314 -0
  16. pymmcore_plus/experimental/unicore/core/_unicore.py +1769 -0
  17. pymmcore_plus/experimental/unicore/devices/_camera.py +201 -0
  18. pymmcore_plus/experimental/unicore/devices/{_device.py → _device_base.py} +54 -28
  19. pymmcore_plus/experimental/unicore/devices/_generic_device.py +12 -0
  20. pymmcore_plus/experimental/unicore/devices/_properties.py +9 -2
  21. pymmcore_plus/experimental/unicore/devices/_shutter.py +30 -0
  22. pymmcore_plus/experimental/unicore/devices/_slm.py +82 -0
  23. pymmcore_plus/experimental/unicore/devices/_stage.py +1 -1
  24. pymmcore_plus/experimental/unicore/devices/_state.py +152 -0
  25. pymmcore_plus/mda/events/_protocol.py +8 -8
  26. {pymmcore_plus-0.14.0.dist-info → pymmcore_plus-0.15.2.dist-info}/METADATA +2 -2
  27. {pymmcore_plus-0.14.0.dist-info → pymmcore_plus-0.15.2.dist-info}/RECORD +30 -21
  28. pymmcore_plus/experimental/unicore/_unicore.py +0 -703
  29. {pymmcore_plus-0.14.0.dist-info → pymmcore_plus-0.15.2.dist-info}/WHEEL +0 -0
  30. {pymmcore_plus-0.14.0.dist-info → pymmcore_plus-0.15.2.dist-info}/entry_points.txt +0 -0
  31. {pymmcore_plus-0.14.0.dist-info → pymmcore_plus-0.15.2.dist-info}/licenses/LICENSE +0 -0
@@ -13,9 +13,18 @@ from pathlib import Path
13
13
  from re import Pattern
14
14
  from textwrap import dedent
15
15
  from threading import Thread
16
- from typing import TYPE_CHECKING, Any, Callable, NamedTuple, TypeVar, cast, overload
16
+ from typing import (
17
+ TYPE_CHECKING,
18
+ Any,
19
+ Callable,
20
+ NamedTuple,
21
+ TypeVar,
22
+ cast,
23
+ overload,
24
+ )
17
25
 
18
26
  from psygnal import SignalInstance
27
+ from typing_extensions import deprecated
19
28
 
20
29
  import pymmcore_plus._pymmcore as pymmcore
21
30
  from pymmcore_plus._logger import current_logfile, logger
@@ -42,7 +51,7 @@ from .events import CMMCoreSignaler, PCoreSignaler, _get_auto_core_callback_clas
42
51
 
43
52
  if TYPE_CHECKING:
44
53
  from collections.abc import Iterable, Iterator, Sequence
45
- from typing import Literal, TypeAlias, TypedDict, Union, Unpack
54
+ from typing import Literal, Never, TypeAlias, TypedDict, Union, Unpack
46
55
 
47
56
  import numpy as np
48
57
  from pymmcore import DeviceLabel
@@ -274,7 +283,7 @@ class CMMCorePlus(pymmcore.CMMCore):
274
283
 
275
284
  self._events = _get_auto_core_callback_class()()
276
285
  self._callback_relay = MMCallbackRelay(self.events)
277
- self.registerCallback(self._callback_relay)
286
+ super().registerCallback(self._callback_relay)
278
287
 
279
288
  self._mda_runner = MDARunner()
280
289
  self._mda_runner.set_engine(MDAEngine(self))
@@ -287,6 +296,24 @@ class CMMCorePlus(pymmcore.CMMCore):
287
296
  self._weak_clean = weakref.WeakMethod(self.unloadAllDevices)
288
297
  atexit.register(self._weak_clean)
289
298
 
299
+ @deprecated(
300
+ "registerCallback is disallowed in pymmcore-plus. Use .events instead."
301
+ )
302
+ def registerCallback(self, *_: Never) -> Never: # type: ignore[override]
303
+ """*registerCallback is disallowed in pymmcore-plus!*
304
+
305
+ If you want to connect callbacks to events, use the
306
+ [`CMMCorePlus.events`][pymmcore_plus.CMMCorePlus.events] property instead.
307
+ """ # noqa
308
+ raise RuntimeError(
309
+ dedent("""
310
+ This method is disallowed in pymmcore-plus.
311
+
312
+ If you want to connect callbacks to events, use the
313
+ `CMMCorePlus.events` property instead.
314
+ """)
315
+ )
316
+
290
317
  @property
291
318
  def events(self) -> PCoreSignaler:
292
319
  """Signaler for core events.
@@ -300,14 +327,20 @@ class CMMCorePlus(pymmcore.CMMCore):
300
327
  return self._events
301
328
 
302
329
  def __repr__(self) -> str:
303
- return f"<{type(self).__name__} at {hex(id(self))}>"
330
+ """Return a string representation of the core object."""
331
+ ndevices = len(self.getLoadedDevices()) - 1
332
+ return f"<{type(self).__name__} at {hex(id(self))} with {ndevices} devices>"
304
333
 
305
334
  def __del__(self) -> None:
306
335
  if hasattr(self, "_weak_clean"):
307
336
  atexit.unregister(self._weak_clean)
308
- self.unloadAllDevices()
309
- # clean up logging
310
- self.setPrimaryLogFile("")
337
+ try:
338
+ super().registerCallback(None)
339
+ self.reset()
340
+ # clean up logging
341
+ self.setPrimaryLogFile("")
342
+ except Exception as e:
343
+ logger.exception("Error during CMMCorePlus.__del__(): %s", e)
311
344
 
312
345
  # Re-implemented methods from the CMMCore API
313
346
 
@@ -691,9 +724,9 @@ class CMMCorePlus(pymmcore.CMMCore):
691
724
  """
692
725
  md = Metadata()
693
726
  if channel is not None and slice is not None:
694
- img = super().getLastImageMD(channel, slice, md)
727
+ img = self.getLastImageMD(channel, slice, md)
695
728
  else:
696
- img = super().getLastImageMD(md)
729
+ img = self.getLastImageMD(md)
697
730
  return (self.fixImage(img) if fix and not pymmcore.NANO else img, md)
698
731
 
699
732
  @overload
@@ -735,7 +768,7 @@ class CMMCorePlus(pymmcore.CMMCore):
735
768
  Image and metadata
736
769
  """
737
770
  md = Metadata()
738
- img = super().popNextImageMD(channel, slice, md)
771
+ img = self.popNextImageMD(channel, slice, md)
739
772
  return (self.fixImage(img) if fix and not pymmcore.NANO else img, md)
740
773
 
741
774
  def popNextImage(self, *, fix: bool = True) -> np.ndarray:
@@ -779,7 +812,7 @@ class CMMCorePlus(pymmcore.CMMCore):
779
812
  will be reshaped to (w, h, n_components) using `fixImage`.
780
813
  """
781
814
  md = Metadata()
782
- img = super().getNBeforeLastImageMD(n, md)
815
+ img = self.getNBeforeLastImageMD(n, md)
783
816
  return self.fixImage(img) if fix and not pymmcore.NANO else img, md
784
817
 
785
818
  def setConfig(self, groupName: str, configName: str) -> None:
@@ -1534,7 +1567,7 @@ class CMMCorePlus(pymmcore.CMMCore):
1534
1567
  def setXYPosition(self, x: float, y: float, /) -> None: ...
1535
1568
  @overload
1536
1569
  def setXYPosition(self, xyStageLabel: str, x: float, y: float, /) -> None: ...
1537
- def setXYPosition(self, *args: str | float) -> None:
1570
+ def setXYPosition(self, *args: Any) -> None:
1538
1571
  """Sets the position of the XY stage in microns.
1539
1572
 
1540
1573
  **Why Override?** to store the last commanded stage position internally.
@@ -1543,10 +1576,10 @@ class CMMCorePlus(pymmcore.CMMCore):
1543
1576
  label: str | None = None
1544
1577
  x, y = cast("tuple[float, float]", args)
1545
1578
  elif len(args) == 3:
1546
- label, x, y = args # type: ignore
1579
+ label, x, y = args
1547
1580
  else:
1548
1581
  raise ValueError("Invalid number of arguments. Expected 2 or 3.")
1549
- super().setXYPosition(*args) # type: ignore
1582
+ super().setXYPosition(*args)
1550
1583
  self._last_xy_position[label] = (x, y)
1551
1584
 
1552
1585
  def getZPosition(self) -> float:
@@ -1592,8 +1625,8 @@ class CMMCorePlus(pymmcore.CMMCore):
1592
1625
  if autoshutter := self.getAutoShutter():
1593
1626
  self.events.propertyChanged.emit(self.getShutterDevice(), "State", True)
1594
1627
  try:
1595
- super().snapImage()
1596
- self.events.imageSnapped.emit()
1628
+ self._do_snap_image()
1629
+ self.events.imageSnapped.emit(self.getCameraDevice())
1597
1630
  finally:
1598
1631
  if autoshutter:
1599
1632
  self.events.propertyChanged.emit(
@@ -1877,15 +1910,12 @@ class CMMCorePlus(pymmcore.CMMCore):
1877
1910
  **Why Override?** To emit a `startContinuousSequenceAcquisition` event.
1878
1911
  """
1879
1912
  self.events.continuousSequenceAcquisitionStarting.emit()
1880
- super().startContinuousSequenceAcquisition(intervalMs)
1913
+ self._do_start_continuous_sequence_acquisition(intervalMs)
1881
1914
  self.events.continuousSequenceAcquisitionStarted.emit()
1882
1915
 
1883
1916
  @overload
1884
1917
  def startSequenceAcquisition(
1885
- self,
1886
- numImages: int,
1887
- intervalMs: float,
1888
- stopOnOverflow: bool,
1918
+ self, numImages: int, intervalMs: float, stopOnOverflow: bool, /
1889
1919
  ) -> None: ...
1890
1920
 
1891
1921
  @overload
@@ -1895,9 +1925,10 @@ class CMMCorePlus(pymmcore.CMMCore):
1895
1925
  numImages: int,
1896
1926
  intervalMs: float,
1897
1927
  stopOnOverflow: bool,
1928
+ /,
1898
1929
  ) -> None: ...
1899
1930
 
1900
- def startSequenceAcquisition(self, *args: Any, **kwargs: Any) -> None:
1931
+ def startSequenceAcquisition(self, *args: Any) -> None:
1901
1932
  """Starts streaming camera sequence acquisition.
1902
1933
 
1903
1934
  This command does not block the calling thread for the duration of the
@@ -1906,18 +1937,16 @@ class CMMCorePlus(pymmcore.CMMCore):
1906
1937
  **Why Override?** To emit a `startSequenceAcquisition` event.
1907
1938
  """
1908
1939
  if len(args) == 3:
1909
- numImages, intervalMs, stopOnOverflow = args
1910
- cameraLabel = super().getCameraDevice()
1911
- else:
1912
- cameraLabel, numImages, intervalMs, stopOnOverflow = args
1940
+ args = (self.getCameraDevice(), *args)
1941
+ elif len(args) != 4:
1942
+ raise ValueError(
1943
+ "startSequenceAcquisition requires either 3 or 4 arguments, "
1944
+ f"got {len(args)}."
1945
+ )
1913
1946
 
1914
- self.events.sequenceAcquisitionStarting.emit(
1915
- cameraLabel, numImages, intervalMs, stopOnOverflow
1916
- )
1917
- super().startSequenceAcquisition(*args, **kwargs)
1918
- self.events.sequenceAcquisitionStarted.emit(
1919
- cameraLabel, numImages, intervalMs, stopOnOverflow
1920
- )
1947
+ self.events.sequenceAcquisitionStarting.emit(*args)
1948
+ self._do_start_sequence_acquisition(*args)
1949
+ self.events.sequenceAcquisitionStarted.emit(*args)
1921
1950
 
1922
1951
  def stopSequenceAcquisition(self, cameraLabel: str | None = None) -> None:
1923
1952
  """Stops streaming camera sequence acquisition.
@@ -1926,13 +1955,32 @@ class CMMCorePlus(pymmcore.CMMCore):
1926
1955
 
1927
1956
  **Why Override?** To emit a `stopSequenceAcquisition` event.
1928
1957
  """
1929
- if cameraLabel is None:
1930
- super().stopSequenceAcquisition()
1931
- else:
1932
- super().stopSequenceAcquisition(cameraLabel)
1933
1958
  cameraLabel = cameraLabel or super().getCameraDevice()
1959
+ self._do_stop_sequence_acquisition(cameraLabel)
1934
1960
  self.events.sequenceAcquisitionStopped.emit(cameraLabel)
1935
1961
 
1962
+ # here for ease of overriding in Unicore ---------------------
1963
+
1964
+ def _do_snap_image(self) -> None:
1965
+ super().snapImage()
1966
+
1967
+ def _do_start_sequence_acquisition(
1968
+ self, cameraLabel: str, numImages: int, intervalMs: float, stopOnOverflow: bool
1969
+ ) -> None:
1970
+ super().startSequenceAcquisition(
1971
+ cameraLabel, numImages, intervalMs, stopOnOverflow
1972
+ )
1973
+
1974
+ def _do_start_continuous_sequence_acquisition(self, intervalMs: float) -> None:
1975
+ """Starts the actual continuous sequence acquisition process."""
1976
+ super().startContinuousSequenceAcquisition(intervalMs)
1977
+
1978
+ def _do_stop_sequence_acquisition(self, cameraLabel: str) -> None:
1979
+ """Stops the actual sequence acquisition process."""
1980
+ super().stopSequenceAcquisition(cameraLabel)
1981
+
1982
+ # end of Unicore helpers ---------------------
1983
+
1936
1984
  def setAutoFocusOffset(self, offset: float) -> None:
1937
1985
  """Applies offset the one-shot focusing device.
1938
1986
 
@@ -1989,26 +2037,27 @@ class CMMCorePlus(pymmcore.CMMCore):
1989
2037
  self.events.autoShutterSet.emit(state)
1990
2038
 
1991
2039
  @overload
1992
- def setShutterOpen(self, state: bool) -> None: ...
1993
-
2040
+ def setShutterOpen(self, state: bool, /) -> None: ...
1994
2041
  @overload
1995
- def setShutterOpen(self, shutterLabel: str, state: bool) -> None: ...
1996
-
1997
- def setShutterOpen(self, *args: Any, **kwargs: Any) -> None:
2042
+ def setShutterOpen(self, shutterLabel: str, state: bool, /) -> None: ...
2043
+ def setShutterOpen(self, *args: Any) -> None:
1998
2044
  """Open or close the currently selected or `shutterLabel` shutter.
1999
2045
 
2000
2046
  **Why Override?** To emit a `propertyChanged` event.
2001
2047
  """
2002
- super().setShutterOpen(*args, **kwargs)
2003
- shutterLabel, state = kwargs.get("shutterLabel"), kwargs.get("state")
2004
2048
  if len(args) == 2:
2005
2049
  shutterLabel, state = args
2006
2050
  elif len(args) == 1:
2007
2051
  shutterLabel = super().getShutterDevice()
2008
2052
  state = args[0]
2053
+ self._do_shutter_open(shutterLabel, state)
2009
2054
  state = str(int(bool(state)))
2010
2055
  self.events.propertyChanged.emit(shutterLabel, "State", state)
2011
2056
 
2057
+ def _do_shutter_open(self, shutterLabel: str, state: bool, /) -> None:
2058
+ """Open or close the shutter."""
2059
+ super().setShutterOpen(shutterLabel, state)
2060
+
2012
2061
  @overload
2013
2062
  def deleteConfig(self, groupName: str, configName: str) -> None: ...
2014
2063
 
@@ -2147,21 +2196,29 @@ class CMMCorePlus(pymmcore.CMMCore):
2147
2196
  return list(xs), list(ys), list(ws), list(hs)
2148
2197
 
2149
2198
  @overload
2150
- def setROI(self, x: int, y: int, width: int, height: int) -> None: ...
2199
+ def setROI(self, x: int, y: int, width: int, height: int, /) -> None: ...
2151
2200
 
2152
2201
  @overload
2153
- def setROI(self, label: str, x: int, y: int, width: int, height: int) -> None: ...
2202
+ def setROI(
2203
+ self, label: str, x: int, y: int, width: int, height: int, /
2204
+ ) -> None: ...
2154
2205
 
2155
- def setROI(self, *args: Any, **kwargs: Any) -> None:
2206
+ def setROI(self, *args: Any) -> None:
2156
2207
  """Set the camera Region of Interest (ROI).
2157
2208
 
2158
2209
  **Why Override?** To emit a `roiSet` event.
2159
2210
  """
2160
- super().setROI(*args, **kwargs)
2161
2211
  if len(args) == 4:
2162
2212
  args = (super().getCameraDevice(), *args)
2213
+ self._do_set_roi(*args)
2163
2214
  self.events.roiSet.emit(*args)
2164
2215
 
2216
+ # here for ease of overriding in Unicore
2217
+
2218
+ def _do_set_roi(self, label: str, x: int, y: int, width: int, height: int) -> None:
2219
+ """Internal method to set the ROI for a specific camera device."""
2220
+ super().setROI(label, x, y, width, height)
2221
+
2165
2222
  def setChannelGroup(self, channelGroup: str) -> None:
2166
2223
  """Specifies the group determining the channel selection.
2167
2224
 
@@ -2328,8 +2385,9 @@ class CMMCorePlus(pymmcore.CMMCore):
2328
2385
  try:
2329
2386
  before = [self.getProperty(device, p) for p in properties]
2330
2387
  except Exception as e:
2331
- logger.error(
2332
- "Error getting properties %s on %s: %s. Cannot ensure signal emission",
2388
+ logger.warning(
2389
+ "Error getting properties %s on %s: %s. "
2390
+ "Cannot ensure propertyChanged signal emission",
2333
2391
  properties,
2334
2392
  device,
2335
2393
  e,
@@ -2506,12 +2564,9 @@ class _MMCallbackRelay(pymmcore.MMEventCallback):
2506
2564
  return reemit
2507
2565
 
2508
2566
 
2567
+ MMCORE_SIGNAL_NAMES = {n for n in dir(pymmcore.MMEventCallback) if n.startswith("on")}
2509
2568
  MMCallbackRelay = type(
2510
2569
  "MMCallbackRelay",
2511
2570
  (_MMCallbackRelay,),
2512
- {
2513
- n: _MMCallbackRelay.make_reemitter(n)
2514
- for n in dir(pymmcore.MMEventCallback)
2515
- if n.startswith("on")
2516
- },
2571
+ {n: _MMCallbackRelay.make_reemitter(n) for n in MMCORE_SIGNAL_NAMES},
2517
2572
  )
@@ -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: UP007
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])
@@ -1,4 +1,15 @@
1
- from typing import Any, Callable, Optional, Protocol, Union, runtime_checkable
1
+ from __future__ import annotations
2
+
3
+ from typing import (
4
+ Any,
5
+ Callable,
6
+ ClassVar,
7
+ Protocol,
8
+ overload,
9
+ runtime_checkable,
10
+ )
11
+
12
+ from typing_extensions import Self
2
13
 
3
14
 
4
15
  @runtime_checkable
@@ -12,7 +23,7 @@ class PSignalInstance(Protocol):
12
23
  def connect(self, slot: Callable) -> Any:
13
24
  """Connect slot to this signal."""
14
25
 
15
- def disconnect(self, slot: Optional[Callable] = None) -> Any:
26
+ def disconnect(self, slot: Callable | None = None) -> Any:
16
27
  """Disconnect slot from this signal.
17
28
 
18
29
  If `None`, all slots should be disconnected.
@@ -23,14 +34,18 @@ class PSignalInstance(Protocol):
23
34
 
24
35
 
25
36
  @runtime_checkable
26
- class PSignalDescriptor(Protocol):
37
+ class PSignal(Protocol):
27
38
  """Descriptor that returns a signal instance."""
28
39
 
29
- def __get__(self, instance: Optional[Any], owner: Any) -> PSignalInstance:
40
+ @overload
41
+ def __get__(self, instance: None, owner: type, /) -> Self: ...
42
+ @overload
43
+ def __get__(self, instance: Any, owner: type | None = None, /) -> Any: ...
44
+ def __get__(
45
+ self, instance: Any, owner: type | None = ..., /
46
+ ) -> PSignalInstance | PSignal:
30
47
  """Returns the signal instance for this descriptor."""
31
-
32
-
33
- PSignal = Union[PSignalDescriptor, PSignalInstance]
48
+ ...
34
49
 
35
50
 
36
51
  @runtime_checkable
@@ -91,102 +106,112 @@ class PCoreSignaler(Protocol):
91
106
  """
92
107
 
93
108
  # native MMCore callback events
94
- propertiesChanged: PSignal
109
+ propertiesChanged: ClassVar[PSignal]
95
110
  """Emits with no arguments when properties have changed."""
96
- propertyChanged: PSignal
97
- """Emits `(name: str, : propName: str, propValue: str)` when a specific property has changed.""" # noqa: E501
98
- channelGroupChanged: PSignal
111
+ propertyChanged: ClassVar[PSignal]
112
+ """Emits `(name: str, propName: str, propValue: str)` when a specific property has changed.""" # noqa: E501
113
+ channelGroupChanged: ClassVar[PSignal]
99
114
  """Emits `(newChannelGroupName: str)` when a channel group has changed."""
100
- configGroupChanged: PSignal
115
+ configGroupChanged: ClassVar[PSignal]
101
116
  """Emits `(groupName: str, newConfigName: str)` when a config group has changed."""
102
- systemConfigurationLoaded: PSignal
117
+ systemConfigurationLoaded: ClassVar[PSignal]
103
118
  """Emits with no arguments when the system configuration has been loaded."""
104
- pixelSizeChanged: PSignal
119
+ pixelSizeChanged: ClassVar[PSignal]
105
120
  """Emits `(newPixelSizeUm: float)` when the pixel size has changed."""
106
- pixelSizeAffineChanged: PSignal
121
+ pixelSizeAffineChanged: ClassVar[PSignal]
107
122
  """Emits `(float, float, float, float, float, float)` when the pixel size affine has changed.""" # noqa: E501
108
- stagePositionChanged: PSignal
123
+ stagePositionChanged: ClassVar[PSignal]
109
124
  """Emits `(name: str, pos: float)` when a stage position has changed."""
110
- XYStagePositionChanged: PSignal
125
+ XYStagePositionChanged: ClassVar[PSignal]
111
126
  """Emits `(name: str, xpos: float, ypos: float)` when an XY stage position has changed.""" # noqa: E501
112
- xYStagePositionChanged: PSignal # alias
113
- exposureChanged: PSignal
127
+ xYStagePositionChanged: ClassVar[PSignal] # alias
128
+ exposureChanged: ClassVar[PSignal]
114
129
  """Emits `(name: str, newExposure: float)` when an exposure has changed."""
115
- SLMExposureChanged: PSignal
130
+ SLMExposureChanged: ClassVar[PSignal]
116
131
  """Emits `(name: str, newExposure: float)` when the exposure of the SLM device changes.""" # noqa: E501
117
- sLMExposureChanged: PSignal # alias
132
+ sLMExposureChanged: ClassVar[PSignal] # alias
118
133
 
119
134
  # added for CMMCorePlus
120
- configSet: PSignal
135
+ configSet: ClassVar[PSignal]
121
136
  """Emits `(str, str)` when a config has been set.
122
137
 
123
138
  > :sparkles: This signal is unique to `pymmcore-plus`.
124
139
  """
125
- imageSnapped: PSignal
140
+ imageSnapped: ClassVar[PSignal]
126
141
  """Emits with no arguments whenever snap is called.
127
142
 
128
143
  > :sparkles: This signal is unique to `pymmcore-plus`.
129
144
  """
130
- mdaEngineRegistered: PSignal
145
+ mdaEngineRegistered: ClassVar[PSignal]
131
146
  """Emits `(MDAEngine, MDAEngine)` when an MDAEngine is registered.
132
147
 
133
148
  > :sparkles: This signal is unique to `pymmcore-plus`.
134
149
  """
135
150
 
136
- continuousSequenceAcquisitionStarting: PSignal
151
+ continuousSequenceAcquisitionStarting: ClassVar[PSignal]
137
152
  """Emits with no arguments *before* continuous sequence acquisition is started.
138
153
 
139
154
  > :sparkles: This signal is unique to `pymmcore-plus`.
140
155
  """
141
- continuousSequenceAcquisitionStarted: PSignal
156
+ continuousSequenceAcquisitionStarted: ClassVar[PSignal]
142
157
  """Emits with no arguments *after* continuous sequence acquisition has started.
143
158
 
144
159
  > :sparkles: This signal is unique to `pymmcore-plus`.
145
160
  """
146
- sequenceAcquisitionStarting: PSignal
147
- """Emits `(str, int, float, bool)` *before* sequence acquisition is started.
161
+ sequenceAcquisitionStarting: ClassVar[PSignal]
162
+ """Emits `(str,)` *before* sequence acquisition is started.
163
+
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.
148
168
 
149
- (cameraLabel, numImages, intervalMs, stopOnOverflow)
169
+ `(cameraLabel,)`
150
170
  > :sparkles: This signal is unique to `pymmcore-plus`.
151
171
  """
152
- sequenceAcquisitionStarted: PSignal
153
- """Emits `(str, int, float, bool)` *after* sequence acquisition has started.
172
+ sequenceAcquisitionStarted: ClassVar[PSignal]
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.
154
179
 
155
- (cameraLabel, numImages, intervalMs, stopOnOverflow)
180
+ (cameraLabel,)
156
181
  > :sparkles: This signal is unique to `pymmcore-plus`.
157
182
  """
158
- sequenceAcquisitionStopped: PSignal
183
+ sequenceAcquisitionStopped: ClassVar[PSignal]
159
184
  """Emits `(str)` when sequence acquisition is stopped.
160
185
 
161
186
  > :sparkles: This signal is unique to `pymmcore-plus`.
162
187
  """
163
- autoShutterSet: PSignal
188
+ autoShutterSet: ClassVar[PSignal]
164
189
  """Emits `(bool)` when the auto shutter setting is changed.
165
190
 
166
191
  """
167
- configGroupDeleted: PSignal
192
+ configGroupDeleted: ClassVar[PSignal]
168
193
  """Emits `(str)` when a config group is deleted.
169
194
 
170
195
  > :sparkles: This signal is unique to `pymmcore-plus`.
171
196
  """
172
- configDeleted: PSignal
197
+ configDeleted: ClassVar[PSignal]
173
198
  """Emits `(str, str)` when a config is deleted.
174
199
 
175
200
  > :sparkles: This signal is unique to `pymmcore-plus`.
176
201
  """
177
- configDefined: PSignal
202
+ configDefined: ClassVar[PSignal]
178
203
  """Emits `(str, str, str, str, str)` when a config is defined.
179
204
 
180
205
  > :sparkles: This signal is unique to `pymmcore-plus`.
181
206
  """
182
- roiSet: PSignal
207
+ roiSet: ClassVar[PSignal]
183
208
  """Emits `(str, int, int, int, int)` when an ROI is set.
184
209
 
185
210
  > :sparkles: This signal is unique to `pymmcore-plus`.
186
211
  """
187
212
 
188
213
  def devicePropertyChanged(
189
- self, device: str, property: Optional[str] = None
214
+ self, device: str, property: str | None = None
190
215
  ) -> PSignalInstance:
191
216
  """Return object to connect/disconnect to device/property-specific changes.
192
217
 
@@ -1,7 +1,10 @@
1
+ from typing import TYPE_CHECKING
2
+
1
3
  from psygnal import Signal, SignalGroup, SignalInstance
2
4
 
3
5
  from pymmcore_plus.mda import MDAEngine
4
6
 
7
+ from ._deprecated import DeprecatedSignalProxy
5
8
  from ._prop_event_mixin import _DevicePropertyEventMixin
6
9
 
7
10
 
@@ -22,14 +25,19 @@ class CMMCoreSignaler(SignalGroup, _DevicePropertyEventMixin):
22
25
  exposureChanged = Signal(str, float)
23
26
  SLMExposureChanged = Signal(str, float)
24
27
 
28
+ # https://github.com/micro-manager/mmCoreAndDevices/pull/659
29
+ imageSnapped = Signal(str) # on snapImage()
30
+ # when (Continuous)SequenceAcquisition is stopped
31
+ sequenceAcquisitionStopped = Signal(str)
32
+ if TYPE_CHECKING: # see deprecated impl below
33
+ sequenceAcquisitionStarted = Signal(str)
34
+
25
35
  # added for CMMCorePlus
26
- imageSnapped = Signal() # whenever snapImage is called
27
36
  mdaEngineRegistered = Signal(MDAEngine, MDAEngine)
28
37
  continuousSequenceAcquisitionStarting = Signal()
29
38
  continuousSequenceAcquisitionStarted = Signal()
30
- sequenceAcquisitionStarting = Signal(str, int, float, bool)
31
- sequenceAcquisitionStarted = Signal(str, int, float, bool)
32
- sequenceAcquisitionStopped = Signal(str)
39
+ if TYPE_CHECKING:
40
+ sequenceAcquisitionStarting = Signal(str)
33
41
  autoShutterSet = Signal(bool)
34
42
  configGroupDeleted = Signal(str)
35
43
  configDeleted = Signal(str, str)
@@ -38,9 +46,30 @@ class CMMCoreSignaler(SignalGroup, _DevicePropertyEventMixin):
38
46
 
39
47
  # aliases for lower casing
40
48
  @property
41
- def xYStagePositionChanged(self) -> SignalInstance: # type: ignore
49
+ def xYStagePositionChanged(self) -> SignalInstance:
42
50
  return self.XYStagePositionChanged
43
51
 
44
52
  @property
45
- def sLMExposureChanged(self) -> SignalInstance: # type: ignore
53
+ def sLMExposureChanged(self) -> SignalInstance:
46
54
  return self.SLMExposureChanged
55
+
56
+ if not TYPE_CHECKING:
57
+ _sequenceAcquisitionStarting = Signal(str)
58
+ _sequenceAcquisitionStarted = Signal(str)
59
+
60
+ # Deprecated signal wrappers for backwards compatibility
61
+ @property
62
+ def sequenceAcquisitionStarting(self) -> SignalInstance:
63
+ return DeprecatedSignalProxy(
64
+ self._sequenceAcquisitionStarting,
65
+ current_n_args=1,
66
+ deprecated_posargs=(-1, 0, False),
67
+ )
68
+
69
+ @property
70
+ def sequenceAcquisitionStarted(self) -> SignalInstance:
71
+ return DeprecatedSignalProxy(
72
+ self._sequenceAcquisitionStarted,
73
+ current_n_args=1,
74
+ deprecated_posargs=(-1, 0, False),
75
+ )