pymmcore-plus 0.14.0__tar.gz → 0.15.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (109) hide show
  1. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/PKG-INFO +1 -1
  2. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/pyproject.toml +3 -4
  3. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/_pymmcore.py +4 -2
  4. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/core/_constants.py +21 -3
  5. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/core/_mmcore_plus.py +96 -40
  6. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/core/events/_protocol.py +49 -34
  7. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/core/events/_psygnal.py +2 -2
  8. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/experimental/unicore/__init__.py +7 -1
  9. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/experimental/unicore/_proxy.py +20 -3
  10. pymmcore_plus-0.15.0/src/pymmcore_plus/experimental/unicore/core/_sequence_buffer.py +318 -0
  11. pymmcore_plus-0.15.0/src/pymmcore_plus/experimental/unicore/core/_unicore.py +1702 -0
  12. pymmcore_plus-0.15.0/src/pymmcore_plus/experimental/unicore/devices/_camera.py +196 -0
  13. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/experimental/unicore/devices/_device.py +54 -28
  14. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/experimental/unicore/devices/_properties.py +8 -1
  15. pymmcore_plus-0.15.0/src/pymmcore_plus/experimental/unicore/devices/_slm.py +82 -0
  16. pymmcore_plus-0.15.0/src/pymmcore_plus/experimental/unicore/devices/_state.py +152 -0
  17. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/mda/events/_protocol.py +8 -8
  18. pymmcore_plus-0.15.0/src/pymmcore_plus/py.typed +0 -0
  19. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/tests/conftest.py +11 -8
  20. pymmcore_plus-0.15.0/tests/test_bench.py +191 -0
  21. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/tests/test_cli.py +4 -2
  22. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/tests/test_events.py +5 -4
  23. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/tests/test_sequencing.py +8 -1
  24. pymmcore_plus-0.15.0/tests/unicore/conftest.py +13 -0
  25. pymmcore_plus-0.15.0/tests/unicore/test_camera.py +266 -0
  26. pymmcore_plus-0.15.0/tests/unicore/test_sequence_buffer.py +577 -0
  27. pymmcore_plus-0.15.0/tests/unicore/test_slm.py +337 -0
  28. pymmcore_plus-0.15.0/tests/unicore/test_state.py +362 -0
  29. pymmcore_plus-0.14.0/src/pymmcore_plus/experimental/unicore/_unicore.py +0 -703
  30. pymmcore_plus-0.14.0/tests/test_bench.py +0 -89
  31. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/.gitignore +0 -0
  32. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/LICENSE +0 -0
  33. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/README.md +0 -0
  34. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/__init__.py +0 -0
  35. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/_accumulator.py +0 -0
  36. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/_benchmark.py +0 -0
  37. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/_build.py +0 -0
  38. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/_cli.py +0 -0
  39. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/_logger.py +0 -0
  40. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/_util.py +0 -0
  41. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/core/__init__.py +0 -0
  42. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/core/_adapter.py +0 -0
  43. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/core/_config.py +0 -0
  44. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/core/_config_group.py +0 -0
  45. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/core/_device.py +0 -0
  46. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/core/_metadata.py +0 -0
  47. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/core/_property.py +0 -0
  48. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/core/_sequencing.py +0 -0
  49. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/core/events/__init__.py +0 -0
  50. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/core/events/_device_signal_view.py +0 -0
  51. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/core/events/_norm_slot.py +0 -0
  52. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/core/events/_prop_event_mixin.py +0 -0
  53. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/core/events/_qsignals.py +0 -0
  54. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/experimental/__init__.py +0 -0
  55. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/experimental/unicore/_device_manager.py +0 -0
  56. {pymmcore_plus-0.14.0/src/pymmcore_plus/experimental/unicore/devices → pymmcore_plus-0.15.0/src/pymmcore_plus/experimental/unicore/core}/__init__.py +0 -0
  57. /pymmcore_plus-0.14.0/src/pymmcore_plus/py.typed → /pymmcore_plus-0.15.0/src/pymmcore_plus/experimental/unicore/devices/__init__.py +0 -0
  58. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/experimental/unicore/devices/_stage.py +0 -0
  59. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/install.py +0 -0
  60. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/mda/__init__.py +0 -0
  61. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/mda/_engine.py +0 -0
  62. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/mda/_protocol.py +0 -0
  63. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/mda/_runner.py +0 -0
  64. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/mda/_thread_relay.py +0 -0
  65. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/mda/events/__init__.py +0 -0
  66. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/mda/events/_psygnal.py +0 -0
  67. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/mda/events/_qsignals.py +0 -0
  68. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/mda/handlers/_5d_writer_base.py +0 -0
  69. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/mda/handlers/__init__.py +0 -0
  70. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/mda/handlers/_img_sequence_writer.py +0 -0
  71. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/mda/handlers/_ome_tiff_writer.py +0 -0
  72. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/mda/handlers/_ome_zarr_writer.py +0 -0
  73. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/mda/handlers/_tensorstore_handler.py +0 -0
  74. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/mda/handlers/_util.py +0 -0
  75. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/metadata/__init__.py +0 -0
  76. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/metadata/functions.py +0 -0
  77. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/metadata/schema.py +0 -0
  78. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/metadata/serialize.py +0 -0
  79. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/mocks.py +0 -0
  80. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/model/__init__.py +0 -0
  81. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/model/_config_file.py +0 -0
  82. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/model/_config_group.py +0 -0
  83. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/model/_core_device.py +0 -0
  84. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/model/_core_link.py +0 -0
  85. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/model/_device.py +0 -0
  86. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/model/_microscope.py +0 -0
  87. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/model/_pixel_size_config.py +0 -0
  88. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/model/_property.py +0 -0
  89. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/src/pymmcore_plus/seq_tester.py +0 -0
  90. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/tests/__init__.py +0 -0
  91. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/tests/io/test_image_sequence_writer.py +0 -0
  92. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/tests/io/test_ome_tiff.py +0 -0
  93. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/tests/io/test_zarr_writers.py +0 -0
  94. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/tests/local_config.cfg +0 -0
  95. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/tests/test_accumulators.py +0 -0
  96. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/tests/test_adapter_class.py +0 -0
  97. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/tests/test_config_group_class.py +0 -0
  98. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/tests/test_core.py +0 -0
  99. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/tests/test_device_class.py +0 -0
  100. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/tests/test_mda.py +0 -0
  101. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/tests/test_metadata.py +0 -0
  102. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/tests/test_misc.py +0 -0
  103. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/tests/test_model.py +0 -0
  104. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/tests/test_pixel_config_class.py +0 -0
  105. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/tests/test_property_class.py +0 -0
  106. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/tests/test_slm_image.py +0 -0
  107. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/tests/test_thread_relay.py +0 -0
  108. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/tests/unicore/test_unicore.py +0 -0
  109. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.0}/tests/unicore/test_xy_stage.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pymmcore-plus
3
- Version: 0.14.0
3
+ Version: 0.15.0
4
4
  Summary: pymmcore superset providing improved APIs, event handling, and a pure python acquisition engine
5
5
  Project-URL: Source, https://github.com/pymmcore-plus/pymmcore-plus
6
6
  Project-URL: Tracker, https://github.com/pymmcore-plus/pymmcore-plus/issues
@@ -79,15 +79,14 @@ test = [
79
79
  "pymmcore >=11.5.0.73",
80
80
  ]
81
81
  test-codspeed = [{ include-group = "test" }, "pytest-codspeed >=3.2.0"]
82
- test-qt = ["pytest-qt >=4.4", "qtpy >=2"]
82
+ test-qt = [{ include-group = 'test' }, "pytest-qt >=4.4", "qtpy >=2"]
83
83
  PyQt6 = [{ include-group = 'test-qt' }, "pymmcore-plus[PyQt6]"]
84
84
  PySide6 = [{ include-group = 'test-qt' }, "pymmcore-plus[PySide6]"]
85
85
  PyQt5 = [{ include-group = 'test-qt' }, "pymmcore-plus[PyQt5]"]
86
86
  PySide2 = [{ include-group = 'test-qt' }, "pymmcore-plus[PySide2]"]
87
87
  dev = [
88
88
  # { include-group = "docs" },
89
- { include-group = "test" },
90
- "PyQt6 >=6.4.2",
89
+ { include-group = "PyQt6" },
91
90
  "ipython>=8.18.1",
92
91
  "pdbpp>=0.11.6 ; sys_platform != 'win32'",
93
92
  "mypy>=1.14.1",
@@ -158,7 +157,7 @@ ignore = [
158
157
  ]
159
158
 
160
159
  [tool.ruff.lint.per-file-ignores]
161
- "tests/*.py" = ["D", "SLF"]
160
+ "tests/**/*.py" = ["D", "SLF"]
162
161
  "examples/*.py" = ["D"]
163
162
  "_cli.py" = ["B008"]
164
163
  "docs/*.py" = ["A", "D"]
@@ -24,7 +24,9 @@ class VersionInfo(NamedTuple):
24
24
  minor: int
25
25
  micro: int
26
26
  device_interface: int
27
- build: int
27
+ build: int = 0
28
28
 
29
29
 
30
- version_info = VersionInfo(*(int(x) for x in re.findall(r"\d+", __version__)))
30
+ # pass no more than 5 parts to VersionInfo
31
+ numbers = re.findall(r"\d+", __version__)[:5]
32
+ version_info = VersionInfo(*(int(i) for i in numbers))
@@ -13,6 +13,7 @@ import pymmcore_plus._pymmcore as pymmcore
13
13
  class Keyword(str, Enum):
14
14
  Name = pymmcore.g_Keyword_Name
15
15
  Description = pymmcore.g_Keyword_Description
16
+
16
17
  CameraName = pymmcore.g_Keyword_CameraName
17
18
  CameraID = pymmcore.g_Keyword_CameraID
18
19
  CameraChannelName = pymmcore.g_Keyword_CameraChannelName
@@ -31,6 +32,7 @@ class Keyword(str, Enum):
31
32
  Offset = pymmcore.g_Keyword_Offset
32
33
  CCDTemperature = pymmcore.g_Keyword_CCDTemperature
33
34
  CCDTemperatureSetPoint = pymmcore.g_Keyword_CCDTemperatureSetPoint
35
+
34
36
  State = pymmcore.g_Keyword_State
35
37
  Label = pymmcore.g_Keyword_Label
36
38
  Position = pymmcore.g_Keyword_Position
@@ -69,12 +71,15 @@ class Keyword(str, Enum):
69
71
  HubID = pymmcore.g_Keyword_HubID
70
72
 
71
73
  # image annotations
72
- Meatdata_Exposure = pymmcore.g_Keyword_Meatdata_Exposure
73
- Metadata_Score = pymmcore.g_Keyword_Metadata_Score
74
+ Metadata_CameraLabel = pymmcore.g_Keyword_Metadata_CameraLabel
75
+ Metadata_Exposure = pymmcore.g_Keyword_Metadata_Exposure
76
+ Metadata_Height = pymmcore.g_Keyword_Metadata_Height
74
77
  Metadata_ImageNumber = pymmcore.g_Keyword_Metadata_ImageNumber
75
78
  Metadata_ROI_X = pymmcore.g_Keyword_Metadata_ROI_X
76
79
  Metadata_ROI_Y = pymmcore.g_Keyword_Metadata_ROI_Y
80
+ Metadata_Score = pymmcore.g_Keyword_Metadata_Score
77
81
  Metadata_TimeInCore = pymmcore.g_Keyword_Metadata_TimeInCore
82
+ Metadata_Width = pymmcore.g_Keyword_Metadata_Width
78
83
 
79
84
  def __str__(self) -> str:
80
85
  return str(self.value)
@@ -165,7 +170,9 @@ class PropertyType(IntEnum):
165
170
  String = pymmcore.String
166
171
  Float = pymmcore.Float
167
172
  Integer = pymmcore.Integer
173
+
168
174
  Boolean = auto() # not supported in pymmcore
175
+ Enum = auto() # not supported in pymmcore
169
176
 
170
177
  def to_python(self) -> type | None:
171
178
  return {0: None, 1: str, 2: float, 3: int}[self]
@@ -183,7 +190,16 @@ class PropertyType(IntEnum):
183
190
  if value is None:
184
191
  return PropertyType.Undef
185
192
  if isinstance(value, str):
186
- return PropertyType[value.lower().capitalize()]
193
+ if value.lower() in ("int", "integer"):
194
+ return PropertyType.Integer
195
+ if value.lower() in ("float", "double"):
196
+ return PropertyType.Float
197
+ if value.lower() in ("bool", "boolean"):
198
+ return PropertyType.Boolean
199
+ if value.lower() in ("string", "str"):
200
+ return PropertyType.String
201
+ if value.lower() in ("enum", "enumeration"):
202
+ return PropertyType.Enum
187
203
  if isinstance(value, type):
188
204
  if value is float:
189
205
  return PropertyType.Float
@@ -193,6 +209,8 @@ class PropertyType(IntEnum):
193
209
  return PropertyType.String
194
210
  elif value is bool:
195
211
  return PropertyType.Boolean
212
+ elif issubclass(value, Enum):
213
+ return PropertyType.Enum
196
214
 
197
215
  raise TypeError(
198
216
  f"Property type must be a PropertyType enum member, "
@@ -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) # type: ignore
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,7 +1625,7 @@ 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()
1628
+ self._do_snap_image()
1596
1629
  self.events.imageSnapped.emit()
1597
1630
  finally:
1598
1631
  if autoshutter:
@@ -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
 
@@ -2147,21 +2195,29 @@ class CMMCorePlus(pymmcore.CMMCore):
2147
2195
  return list(xs), list(ys), list(ws), list(hs)
2148
2196
 
2149
2197
  @overload
2150
- def setROI(self, x: int, y: int, width: int, height: int) -> None: ...
2198
+ def setROI(self, x: int, y: int, width: int, height: int, /) -> None: ...
2151
2199
 
2152
2200
  @overload
2153
- def setROI(self, label: str, x: int, y: int, width: int, height: int) -> None: ...
2201
+ def setROI(
2202
+ self, label: str, x: int, y: int, width: int, height: int, /
2203
+ ) -> None: ...
2154
2204
 
2155
- def setROI(self, *args: Any, **kwargs: Any) -> None:
2205
+ def setROI(self, *args: Any) -> None:
2156
2206
  """Set the camera Region of Interest (ROI).
2157
2207
 
2158
2208
  **Why Override?** To emit a `roiSet` event.
2159
2209
  """
2160
- super().setROI(*args, **kwargs)
2161
2210
  if len(args) == 4:
2162
2211
  args = (super().getCameraDevice(), *args)
2212
+ self._do_set_roi(*args)
2163
2213
  self.events.roiSet.emit(*args)
2164
2214
 
2215
+ # here for ease of overriding in Unicore
2216
+
2217
+ def _do_set_roi(self, label: str, x: int, y: int, width: int, height: int) -> None:
2218
+ """Internal method to set the ROI for a specific camera device."""
2219
+ super().setROI(label, x, y, width, height)
2220
+
2165
2221
  def setChannelGroup(self, channelGroup: str) -> None:
2166
2222
  """Specifies the group determining the channel selection.
2167
2223
 
@@ -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,102 @@ 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
111
+ propertyChanged: ClassVar[PSignal]
97
112
  """Emits `(name: str, : propName: str, propValue: str)` when a specific property has changed.""" # noqa: E501
98
- channelGroupChanged: PSignal
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
161
+ sequenceAcquisitionStarting: ClassVar[PSignal]
147
162
  """Emits `(str, int, float, bool)` *before* sequence acquisition is started.
148
163
 
149
164
  (cameraLabel, numImages, intervalMs, stopOnOverflow)
150
165
  > :sparkles: This signal is unique to `pymmcore-plus`.
151
166
  """
152
- sequenceAcquisitionStarted: PSignal
167
+ sequenceAcquisitionStarted: ClassVar[PSignal]
153
168
  """Emits `(str, int, float, bool)` *after* sequence acquisition has started.
154
169
 
155
170
  (cameraLabel, numImages, intervalMs, stopOnOverflow)
156
171
  > :sparkles: This signal is unique to `pymmcore-plus`.
157
172
  """
158
- sequenceAcquisitionStopped: PSignal
173
+ sequenceAcquisitionStopped: ClassVar[PSignal]
159
174
  """Emits `(str)` when sequence acquisition is stopped.
160
175
 
161
176
  > :sparkles: This signal is unique to `pymmcore-plus`.
162
177
  """
163
- autoShutterSet: PSignal
178
+ autoShutterSet: ClassVar[PSignal]
164
179
  """Emits `(bool)` when the auto shutter setting is changed.
165
180
 
166
181
  """
167
- configGroupDeleted: PSignal
182
+ configGroupDeleted: ClassVar[PSignal]
168
183
  """Emits `(str)` when a config group is deleted.
169
184
 
170
185
  > :sparkles: This signal is unique to `pymmcore-plus`.
171
186
  """
172
- configDeleted: PSignal
187
+ configDeleted: ClassVar[PSignal]
173
188
  """Emits `(str, str)` when a config is deleted.
174
189
 
175
190
  > :sparkles: This signal is unique to `pymmcore-plus`.
176
191
  """
177
- configDefined: PSignal
192
+ configDefined: ClassVar[PSignal]
178
193
  """Emits `(str, str, str, str, str)` when a config is defined.
179
194
 
180
195
  > :sparkles: This signal is unique to `pymmcore-plus`.
181
196
  """
182
- roiSet: PSignal
197
+ roiSet: ClassVar[PSignal]
183
198
  """Emits `(str, int, int, int, int)` when an ROI is set.
184
199
 
185
200
  > :sparkles: This signal is unique to `pymmcore-plus`.
186
201
  """
187
202
 
188
203
  def devicePropertyChanged(
189
- self, device: str, property: Optional[str] = None
204
+ self, device: str, property: str | None = None
190
205
  ) -> PSignalInstance:
191
206
  """Return object to connect/disconnect to device/property-specific changes.
192
207
 
@@ -38,9 +38,9 @@ class CMMCoreSignaler(SignalGroup, _DevicePropertyEventMixin):
38
38
 
39
39
  # aliases for lower casing
40
40
  @property
41
- def xYStagePositionChanged(self) -> SignalInstance: # type: ignore
41
+ def xYStagePositionChanged(self) -> SignalInstance:
42
42
  return self.XYStagePositionChanged
43
43
 
44
44
  @property
45
- def sLMExposureChanged(self) -> SignalInstance: # type: ignore
45
+ def sLMExposureChanged(self) -> SignalInstance:
46
46
  return self.SLMExposureChanged
@@ -1,12 +1,18 @@
1
- from ._unicore import UniMMCore
1
+ from .core._unicore import UniMMCore
2
+ from .devices._camera import Camera
2
3
  from .devices._device import Device
3
4
  from .devices._properties import PropertyInfo, pymm_property
5
+ from .devices._slm import SLMDevice
4
6
  from .devices._stage import StageDevice, XYStageDevice, XYStepperStageDevice
7
+ from .devices._state import StateDevice
5
8
 
6
9
  __all__ = [
10
+ "Camera",
7
11
  "Device",
8
12
  "PropertyInfo",
13
+ "SLMDevice",
9
14
  "StageDevice",
15
+ "StateDevice",
10
16
  "UniMMCore",
11
17
  "XYStageDevice",
12
18
  "XYStepperStageDevice",
@@ -106,22 +106,39 @@ def create_proxy(
106
106
  ```
107
107
  """
108
108
  sub_proxies = sub_proxies or {}
109
+
110
+ # Get all public attribute names from the protocol (both from dir() and type hints)
109
111
  allowed_names = {
110
112
  x
111
113
  for x in chain(dir(protocol), get_type_hints(protocol))
112
- if not x.startswith("_")
114
+ if not x.startswith("_") # Exclude private/dunder attributes
113
115
  }
116
+
117
+ # Create an immutable module to act as our proxy object
114
118
  proxy = _ImmutableModule(protocol.__name__)
119
+
120
+ # Iterate through each allowed attribute name
115
121
  for attr_name in allowed_names:
122
+ # Get the actual attribute from the source object
116
123
  attr = getattr(obj, attr_name)
124
+
125
+ # Check if this attribute should be sub-proxied
117
126
  if subprotocol := sub_proxies.get(attr_name):
118
- # look for nested sub-proxies on attr_name, e.g. `attr_name.sub_attr`
127
+ # Look for nested sub-proxies on attr_name, e.g. `attr_name.sub_attr`
128
+ # Filter sub_proxies for keys that start with "attr_name."
119
129
  sub = {
120
- k.split(".", 1)[1]: v
130
+ k.split(".", 1)[1]: v # Remove the "attr_name." prefix
121
131
  for k, v in sub_proxies.items()
122
132
  if k.startswith(f"{attr_name}.")
123
133
  }
134
+ # Recursively create a proxy for this attribute
124
135
  attr = create_proxy(attr, subprotocol, sub)
136
+
137
+ # Set the attribute on our proxy object
125
138
  setattr(proxy, attr_name, attr)
139
+
140
+ # Freeze the proxy to prevent further modifications
126
141
  proxy.__frozen__ = True
142
+
143
+ # Return the proxy cast to the expected protocol type
127
144
  return cast("T", proxy)