pymmcore-plus 0.12.0__py3-none-any.whl → 0.13.1__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 (48) hide show
  1. pymmcore_plus/__init__.py +3 -3
  2. pymmcore_plus/_benchmark.py +203 -0
  3. pymmcore_plus/_cli.py +78 -13
  4. pymmcore_plus/_logger.py +10 -2
  5. pymmcore_plus/_pymmcore.py +12 -0
  6. pymmcore_plus/_util.py +16 -10
  7. pymmcore_plus/core/__init__.py +3 -0
  8. pymmcore_plus/core/_config.py +1 -1
  9. pymmcore_plus/core/_config_group.py +2 -2
  10. pymmcore_plus/core/_constants.py +27 -3
  11. pymmcore_plus/core/_device.py +4 -4
  12. pymmcore_plus/core/_metadata.py +1 -1
  13. pymmcore_plus/core/_mmcore_plus.py +184 -118
  14. pymmcore_plus/core/_property.py +3 -5
  15. pymmcore_plus/core/_sequencing.py +369 -234
  16. pymmcore_plus/core/events/__init__.py +3 -3
  17. pymmcore_plus/experimental/__init__.py +0 -0
  18. pymmcore_plus/experimental/unicore/__init__.py +14 -0
  19. pymmcore_plus/experimental/unicore/_device_manager.py +173 -0
  20. pymmcore_plus/experimental/unicore/_proxy.py +127 -0
  21. pymmcore_plus/experimental/unicore/_unicore.py +703 -0
  22. pymmcore_plus/experimental/unicore/devices/__init__.py +0 -0
  23. pymmcore_plus/experimental/unicore/devices/_device.py +269 -0
  24. pymmcore_plus/experimental/unicore/devices/_properties.py +400 -0
  25. pymmcore_plus/experimental/unicore/devices/_stage.py +221 -0
  26. pymmcore_plus/install.py +10 -7
  27. pymmcore_plus/mda/__init__.py +1 -1
  28. pymmcore_plus/mda/_engine.py +152 -43
  29. pymmcore_plus/mda/_runner.py +8 -1
  30. pymmcore_plus/mda/events/__init__.py +2 -2
  31. pymmcore_plus/mda/handlers/__init__.py +1 -1
  32. pymmcore_plus/mda/handlers/_ome_zarr_writer.py +2 -2
  33. pymmcore_plus/mda/handlers/_tensorstore_handler.py +6 -2
  34. pymmcore_plus/metadata/__init__.py +3 -3
  35. pymmcore_plus/metadata/functions.py +18 -8
  36. pymmcore_plus/metadata/schema.py +6 -5
  37. pymmcore_plus/mocks.py +49 -0
  38. pymmcore_plus/model/_config_file.py +1 -1
  39. pymmcore_plus/model/_core_device.py +10 -1
  40. pymmcore_plus/model/_device.py +17 -6
  41. pymmcore_plus/model/_property.py +11 -2
  42. pymmcore_plus/seq_tester.py +1 -1
  43. {pymmcore_plus-0.12.0.dist-info → pymmcore_plus-0.13.1.dist-info}/METADATA +14 -6
  44. pymmcore_plus-0.13.1.dist-info/RECORD +71 -0
  45. {pymmcore_plus-0.12.0.dist-info → pymmcore_plus-0.13.1.dist-info}/WHEEL +1 -1
  46. pymmcore_plus-0.12.0.dist-info/RECORD +0 -59
  47. {pymmcore_plus-0.12.0.dist-info → pymmcore_plus-0.13.1.dist-info}/entry_points.txt +0 -0
  48. {pymmcore_plus-0.12.0.dist-info → pymmcore_plus-0.13.1.dist-info}/licenses/LICENSE +0 -0
@@ -12,19 +12,12 @@ from datetime import datetime
12
12
  from pathlib import Path
13
13
  from re import Pattern
14
14
  from textwrap import dedent
15
- from threading import RLock, Thread
16
- from typing import (
17
- TYPE_CHECKING,
18
- Any,
19
- Callable,
20
- NamedTuple,
21
- TypeVar,
22
- overload,
23
- )
15
+ from threading import Thread
16
+ from typing import TYPE_CHECKING, Any, Callable, NamedTuple, TypeVar, overload
24
17
 
25
- import pymmcore
26
18
  from psygnal import SignalInstance
27
19
 
20
+ import pymmcore_plus._pymmcore as pymmcore
28
21
  from pymmcore_plus._logger import current_logfile, logger
29
22
  from pymmcore_plus._util import find_micromanager, print_tabular_data
30
23
  from pymmcore_plus.mda import MDAEngine, MDARunner, PMDAEngine
@@ -44,12 +37,11 @@ from ._constants import (
44
37
  from ._device import Device
45
38
  from ._metadata import Metadata
46
39
  from ._property import DeviceProperty
47
- from ._sequencing import can_sequence_events
48
40
  from .events import CMMCoreSignaler, PCoreSignaler, _get_auto_core_callback_class
49
41
 
50
42
  if TYPE_CHECKING:
51
43
  from collections.abc import Iterable, Iterator, Sequence
52
- from typing import Literal, TypedDict
44
+ from typing import Literal, TypedDict, Unpack
53
45
 
54
46
  import numpy as np
55
47
  from useq import MDAEvent
@@ -58,7 +50,6 @@ if TYPE_CHECKING:
58
50
  from pymmcore_plus.metadata.schema import SummaryMetaV1
59
51
 
60
52
  _T = TypeVar("_T")
61
- _F = TypeVar("_F", bound=Callable[..., Any])
62
53
  ListOrTuple = list[_T] | tuple[_T, ...]
63
54
 
64
55
  class PropertySchema(TypedDict, total=False):
@@ -82,10 +73,43 @@ if TYPE_CHECKING:
82
73
  type: str
83
74
  properties: dict[str, PropertySchema]
84
75
 
85
- def synchronized(lock: RLock) -> Callable[[_F], _F]: ...
76
+ class SetContextKwargs(TypedDict, total=False):
77
+ """All the valid keywords and their types for the `setContext` method."""
78
+
79
+ autoFocusDevice: str
80
+ autoFocusOffset: float
81
+ autoShutter: bool
82
+ cameraDevice: str
83
+ channelGroup: str
84
+ circularBufferMemoryFootprint: int
85
+ deviceAdapterSearchPaths: list[str]
86
+ deviceDelayMs: tuple[str, float]
87
+ exposure: float | tuple[str, float]
88
+ focusDevice: str
89
+ focusDirection: str
90
+ galvoDevice: str
91
+ galvoPosition: tuple[str, float, float]
92
+ imageProcessorDevice: str
93
+ multiROI: tuple[list[int], list[int], list[int], list[int]]
94
+ parentLabel: tuple[str, str]
95
+ pixelSizeAffine: tuple[str, list[float]]
96
+ pixelSizeUm: tuple[str, float]
97
+ position: float | tuple[str, float]
98
+ primaryLogFile: str | tuple[str, bool]
99
+ property: tuple[str, str, bool | float | int | str]
100
+ ROI: tuple[int, int, int, int] | tuple[str, int, int, int, int]
101
+ SLMDevice: str
102
+ SLMExposure: tuple[str, float]
103
+ shutterDevice: str
104
+ shutterOpen: bool | tuple[str, bool]
105
+ state: tuple[str, int]
106
+ stateLabel: tuple[str, str]
107
+ systemState: pymmcore.Configuration
108
+ timeoutMs: int
109
+ XYPosition: tuple[float, float] | tuple[str, float, float]
110
+ XYStageDevice: str
111
+ ZPosition: float | tuple[str, float]
86
112
 
87
- else:
88
- from wrapt import synchronized
89
113
 
90
114
  _OBJDEV_REGEX = re.compile("(.+)?(nosepiece|obj(ective)?)(turret)?s?", re.IGNORECASE)
91
115
  _CHANNEL_REGEX = re.compile("(chan{1,2}(el)?|filt(er)?)s?", re.IGNORECASE)
@@ -151,8 +175,6 @@ class CMMCorePlus(pymmcore.CMMCore):
151
175
  Paths to search for device adapters, by default ()
152
176
  """
153
177
 
154
- _lock = RLock()
155
-
156
178
  @classmethod
157
179
  def instance(cls) -> CMMCorePlus:
158
180
  """Return the global singleton instance of `CMMCorePlus`.
@@ -240,10 +262,11 @@ class CMMCorePlus(pymmcore.CMMCore):
240
262
  if hasattr(self, "_weak_clean"):
241
263
  atexit.unregister(self._weak_clean)
242
264
  self.unloadAllDevices()
265
+ # clean up logging
266
+ self.setPrimaryLogFile("")
243
267
 
244
268
  # Re-implemented methods from the CMMCore API
245
269
 
246
- @synchronized(_lock)
247
270
  def setProperty(
248
271
  self, label: str, propName: str, propValue: bool | float | int | str
249
272
  ) -> None:
@@ -258,7 +281,6 @@ class CMMCorePlus(pymmcore.CMMCore):
258
281
  with self._property_change_emission_ensured(label, (propName,)):
259
282
  super().setProperty(label, propName, propValue)
260
283
 
261
- @synchronized(_lock)
262
284
  def setState(self, stateDeviceLabel: str, state: int) -> None:
263
285
  """Set state (by position) on `stateDeviceLabel`, with reliable event emission.
264
286
 
@@ -271,7 +293,6 @@ class CMMCorePlus(pymmcore.CMMCore):
271
293
  with self._property_change_emission_ensured(stateDeviceLabel, STATE_PROPS):
272
294
  super().setState(stateDeviceLabel, state)
273
295
 
274
- @synchronized(_lock)
275
296
  def setStateLabel(self, stateDeviceLabel: str, stateLabel: str) -> None:
276
297
  """Set state (by label) on `stateDeviceLabel`, with reliable event emission.
277
298
 
@@ -328,33 +349,37 @@ class CMMCorePlus(pymmcore.CMMCore):
328
349
  try:
329
350
  super().loadDevice(label, moduleName, deviceName)
330
351
  except RuntimeError as e:
331
- msg = str(e)
332
- if label in self.getLoadedDevices():
333
- lib = super().getDeviceLibrary(label)
334
- name = super().getDeviceName(label)
335
- if moduleName == lib and deviceName == name:
336
- msg += f". Device {label!r} appears to be loaded already."
337
- warnings.warn(msg, stacklevel=2)
338
- return
339
-
340
- msg += f". Device {label!r} is already taken by {lib}::{name}"
352
+ if exc := self._load_error_with_info(label, moduleName, deviceName, str(e)):
353
+ raise exc from e
354
+
355
+ def _load_error_with_info(
356
+ self, label: str, moduleName: str, deviceName: str, msg: str = ""
357
+ ) -> RuntimeError | None:
358
+ if label in self.getLoadedDevices():
359
+ lib = super().getDeviceLibrary(label)
360
+ name = super().getDeviceName(label)
361
+ if moduleName == lib and deviceName == name:
362
+ msg += f". Device {label!r} appears to be loaded already."
363
+ warnings.warn(msg, stacklevel=2)
364
+ return None
365
+
366
+ msg += f". Device {label!r} is already taken by {lib}::{name}"
367
+ else:
368
+ adapters = super().getDeviceAdapterNames()
369
+ if moduleName not in adapters:
370
+ msg += (
371
+ f". Adapter name {moduleName!r} not in list of known adapter "
372
+ f"names: {adapters}."
373
+ )
341
374
  else:
342
- adapters = super().getDeviceAdapterNames()
343
- if moduleName not in adapters:
375
+ devices = super().getAvailableDevices(moduleName)
376
+ if deviceName not in devices:
344
377
  msg += (
345
- f". Adapter name {moduleName!r} not in list of known adapter "
346
- f"names: {adapters}."
378
+ f". Device name {deviceName!r} not in devices provided by "
379
+ f"adapter {moduleName!r}: {devices}"
347
380
  )
348
- else:
349
- devices = super().getAvailableDevices(moduleName)
350
- if deviceName not in devices:
351
- msg += (
352
- f". Device name {deviceName!r} not in devices provided by "
353
- f"adapter {moduleName!r}: {devices}"
354
- )
355
- raise RuntimeError(msg) from e
356
-
357
- @synchronized(_lock)
381
+ return RuntimeError(msg)
382
+
358
383
  def loadSystemConfiguration(
359
384
  self, fileName: str | Path = "MMConfig_demo.cfg"
360
385
  ) -> None:
@@ -582,7 +607,6 @@ class CMMCorePlus(pymmcore.CMMCore):
582
607
  @overload
583
608
  def getLastImageAndMD(self, *, fix: bool = True) -> tuple[np.ndarray, Metadata]: ...
584
609
 
585
- @synchronized(_lock)
586
610
  def getLastImageAndMD(
587
611
  self, channel: int | None = None, slice: int | None = None, *, fix: bool = True
588
612
  ) -> tuple[np.ndarray, Metadata]:
@@ -618,7 +642,10 @@ class CMMCorePlus(pymmcore.CMMCore):
618
642
  img = super().getLastImageMD(channel, slice, md)
619
643
  else:
620
644
  img = super().getLastImageMD(md)
621
- return (self.fixImage(img) if fix else img, md)
645
+ return (
646
+ self.fixImage(img) if fix and pymmcore.BACKEND == "pymmcore" else img,
647
+ md,
648
+ )
622
649
 
623
650
  @overload
624
651
  def popNextImageAndMD(
@@ -628,7 +655,6 @@ class CMMCorePlus(pymmcore.CMMCore):
628
655
  @overload
629
656
  def popNextImageAndMD(self, *, fix: bool = True) -> tuple[np.ndarray, Metadata]: ...
630
657
 
631
- @synchronized(_lock)
632
658
  def popNextImageAndMD(
633
659
  self, channel: int = 0, slice: int = 0, *, fix: bool = True
634
660
  ) -> tuple[np.ndarray, Metadata]:
@@ -661,9 +687,12 @@ class CMMCorePlus(pymmcore.CMMCore):
661
687
  """
662
688
  md = Metadata()
663
689
  img = super().popNextImageMD(channel, slice, md)
664
- return (self.fixImage(img) if fix else img, md)
690
+ md = Metadata(md)
691
+ return (
692
+ self.fixImage(img) if fix and pymmcore.BACKEND == "pymmcore" else img,
693
+ md,
694
+ )
665
695
 
666
- @synchronized(_lock)
667
696
  def popNextImage(self, *, fix: bool = True) -> np.ndarray:
668
697
  """Gets and removes the next image from the circular buffer.
669
698
 
@@ -678,9 +707,8 @@ class CMMCorePlus(pymmcore.CMMCore):
678
707
  will be reshaped to (w, h, n_components) using `fixImage`.
679
708
  """
680
709
  img: np.ndarray = super().popNextImage()
681
- return self.fixImage(img) if fix else img
710
+ return self.fixImage(img) if fix and pymmcore.BACKEND == "pymmcore" else img
682
711
 
683
- @synchronized(_lock)
684
712
  def getNBeforeLastImageAndMD(
685
713
  self, n: int, *, fix: bool = True
686
714
  ) -> tuple[np.ndarray, Metadata]:
@@ -707,7 +735,7 @@ class CMMCorePlus(pymmcore.CMMCore):
707
735
  """
708
736
  md = Metadata()
709
737
  img = super().getNBeforeLastImageMD(n, md)
710
- return self.fixImage(img) if fix else img, md
738
+ return self.fixImage(img) if fix and pymmcore.BACKEND == "pymmcore" else img, md
711
739
 
712
740
  def setConfig(self, groupName: str, configName: str) -> None:
713
741
  """Applies a configuration to a group.
@@ -1344,35 +1372,6 @@ class CMMCorePlus(pymmcore.CMMCore):
1344
1372
  """
1345
1373
  return self.setPosition(val)
1346
1374
 
1347
- @overload
1348
- def setPosition(self, position: float) -> None: ...
1349
-
1350
- @overload
1351
- def setPosition(self, stageLabel: str, position: float) -> None: ...
1352
-
1353
- @synchronized(_lock)
1354
- def setPosition(self, *args: Any, **kwargs: Any) -> None:
1355
- """Set position of the stage in microns.
1356
-
1357
- **Why Override?** To add a lock to prevent concurrent calls across threads.
1358
- """
1359
- return super().setPosition(*args, **kwargs)
1360
-
1361
- @overload
1362
- def setXYPosition(self, x: float, y: float) -> None: ...
1363
-
1364
- @overload
1365
- def setXYPosition(self, xyStageLabel: str, x: float, y: float) -> None: ...
1366
-
1367
- @synchronized(_lock)
1368
- def setXYPosition(self, *args: Any, **kwargs: Any) -> None:
1369
- """Sets the position of the XY stage in microns.
1370
-
1371
- **Why Override?** To add a lock to prevent concurrent calls across threads.
1372
- """
1373
- return super().setXYPosition(*args, **kwargs)
1374
-
1375
- @synchronized(_lock)
1376
1375
  def getCameraChannelNames(self) -> tuple[str, ...]:
1377
1376
  """Convenience method to call `getCameraChannelName` for all camera channels.
1378
1377
 
@@ -1383,11 +1382,11 @@ class CMMCorePlus(pymmcore.CMMCore):
1383
1382
  for i in range(self.getNumberOfCameraChannels())
1384
1383
  )
1385
1384
 
1386
- @synchronized(_lock)
1387
1385
  def snapImage(self) -> None:
1388
1386
  """Acquires a single image with current settings.
1389
1387
 
1390
- **Why Override?** To add a lock to prevent concurrent calls across threads.
1388
+ **Why Override?** to emit the `imageSnapped` event after snapping an image.
1389
+ and to emit shutter property changes if `getAutoShutter` is `True`.
1391
1390
  """
1392
1391
  if autoshutter := self.getAutoShutter():
1393
1392
  self.events.propertyChanged.emit(self.getShutterDevice(), "State", True)
@@ -1502,9 +1501,9 @@ class CMMCorePlus(pymmcore.CMMCore):
1502
1501
  """
1503
1502
  if ncomponents is None:
1504
1503
  ncomponents = self.getNumberOfComponents()
1505
- if ncomponents == 4:
1504
+ if ncomponents == 4 and img.ndim != 3:
1506
1505
  new_shape = (*img.shape, 4)
1507
- img = img.view(dtype=f"u{img.dtype.itemsize//4}").reshape(new_shape)
1506
+ img = img.view(dtype=f"u{img.dtype.itemsize // 4}").reshape(new_shape)
1508
1507
  img = img[..., [2, 1, 0]] # Convert from BGRA to RGB
1509
1508
  return img
1510
1509
 
@@ -1519,7 +1518,7 @@ class CMMCorePlus(pymmcore.CMMCore):
1519
1518
  # best as I can tell, this is a hard-coded string in Utilities/MultiCamera.cpp
1520
1519
  # (it also appears in ArduinoCounter.cpp). This appears to be "the way"
1521
1520
  # to get at the original camera when using the multi-camera utility.
1522
- prop_name = f"Physical Camera {channel_index+1}"
1521
+ prop_name = f"Physical Camera {channel_index + 1}"
1523
1522
  if self.hasProperty(cam_dev, prop_name):
1524
1523
  return self.getProperty(cam_dev, prop_name)
1525
1524
  if channel_index > 0:
@@ -1669,7 +1668,7 @@ class CMMCorePlus(pymmcore.CMMCore):
1669
1668
  if numChannel is not None
1670
1669
  else super().getImage()
1671
1670
  )
1672
- return self.fixImage(img) if fix else img
1671
+ return self.fixImage(img) if fix and pymmcore.BACKEND == "pymmcore" else img
1673
1672
 
1674
1673
  def startContinuousSequenceAcquisition(self, intervalMs: float = 0) -> None:
1675
1674
  """Start a ContinuousSequenceAcquisition.
@@ -1913,30 +1912,33 @@ class CMMCorePlus(pymmcore.CMMCore):
1913
1912
  super().definePixelSizeConfig(*args, **kwargs)
1914
1913
  self.events.pixelSizeChanged.emit(0.0)
1915
1914
 
1916
- def getMultiROI( # type: ignore [override]
1917
- self, *_: Any
1918
- ) -> tuple[list[int], list[int], list[int], list[int]]:
1919
- """Get multiple ROIs from the current camera device.
1915
+ # pymmcore-SWIG needs this, but pymmcore-nano doesn't
1916
+ if hasattr(pymmcore, "UnsignedVector"):
1920
1917
 
1921
- Will fail if the camera does not support multiple ROIs. Will return empty
1922
- vectors if multiple ROIs are not currently being used.
1918
+ def getMultiROI( # type: ignore [override]
1919
+ self, *_: Any
1920
+ ) -> tuple[list[int], list[int], list[int], list[int]]:
1921
+ """Get multiple ROIs from the current camera device.
1923
1922
 
1924
- **Why Override?** So that the user doesn't need to pass in four empty
1925
- pymmcore.UnsignedVector() objects.
1926
- """
1927
- if _:
1928
- warnings.warn( # pragma: no cover
1929
- "Unlike pymmcore, CMMCorePlus.getMultiROI does not require arguments."
1930
- "Arguments are ignored.",
1931
- stacklevel=2,
1932
- )
1923
+ Will fail if the camera does not support multiple ROIs. Will return empty
1924
+ vectors if multiple ROIs are not currently being used.
1933
1925
 
1934
- xs = pymmcore.UnsignedVector() # type: ignore [attr-defined]
1935
- ys = pymmcore.UnsignedVector() # type: ignore [attr-defined]
1936
- ws = pymmcore.UnsignedVector() # type: ignore [attr-defined]
1937
- hs = pymmcore.UnsignedVector() # type: ignore [attr-defined]
1938
- super().getMultiROI(xs, ys, ws, hs)
1939
- return list(xs), list(ys), list(ws), list(hs)
1926
+ **Why Override?** So that the user doesn't need to pass in four empty
1927
+ pymmcore.UnsignedVector() objects.
1928
+ """
1929
+ if _:
1930
+ warnings.warn( # pragma: no cover
1931
+ "Unlike pymmcore, CMMCorePlus.getMultiROI does not require "
1932
+ "arguments. Arguments are ignored.",
1933
+ stacklevel=2,
1934
+ )
1935
+
1936
+ xs = pymmcore.UnsignedVector() # type: ignore [attr-defined]
1937
+ ys = pymmcore.UnsignedVector() # type: ignore [attr-defined]
1938
+ ws = pymmcore.UnsignedVector() # type: ignore [attr-defined]
1939
+ hs = pymmcore.UnsignedVector() # type: ignore [attr-defined]
1940
+ super().getMultiROI(xs, ys, ws, hs)
1941
+ return list(xs), list(ys), list(ws), list(hs)
1940
1942
 
1941
1943
  @overload
1942
1944
  def setROI(self, x: int, y: int, width: int, height: int) -> None: ...
@@ -2000,7 +2002,12 @@ class CMMCorePlus(pymmcore.CMMCore):
2000
2002
  with open(filename, "a") as f:
2001
2003
  f.write("\n".join(cfg))
2002
2004
 
2003
- def describe(self, sort: str | None = None) -> None:
2005
+ def describe(
2006
+ self,
2007
+ sort: str | None = None,
2008
+ show_config_groups: bool = False,
2009
+ show_available: bool = False,
2010
+ ) -> None:
2004
2011
  """Print information table with the current configuration.
2005
2012
 
2006
2013
  Intended to provide a quick overview of the microscope configuration during
@@ -2032,8 +2039,50 @@ class CMMCorePlus(pymmcore.CMMCore):
2032
2039
 
2033
2040
  print(f"{self.getVersionInfo()}, {self.getAPIVersionInfo()}")
2034
2041
  print("Adapter path:", ",".join(self.getDeviceAdapterSearchPaths()))
2042
+ print("\nLoaded Devices:")
2035
2043
  print_tabular_data(data, sort=sort)
2036
2044
 
2045
+ state = self.state(cached=False)
2046
+ if show_config_groups:
2047
+ group_data: defaultdict[str, list[str]] = defaultdict(list)
2048
+ groups = state["config_groups"]
2049
+ for group in groups:
2050
+ for pi, preset in enumerate(group["presets"]):
2051
+ for si, stng in enumerate(preset["settings"]):
2052
+ dev, prop, val = stng["dev"], stng["prop"], stng["val"]
2053
+ group_name = group["name"] if (pi == 0 and si == 0) else ""
2054
+ preset_name = preset["name"] if si == 0 else ""
2055
+ group_data["Group"].append(group_name)
2056
+ group_data["Preset"].append(preset_name)
2057
+ group_data["Device"].append(dev)
2058
+ group_data["Property"].append(prop)
2059
+ group_data["Value"].append(val)
2060
+ # add break between presets
2061
+ group_data["Group"].append("")
2062
+ group_data["Preset"].append("")
2063
+ group_data["Device"].append("")
2064
+ group_data["Property"].append("")
2065
+ group_data["Value"].append("")
2066
+
2067
+ print("\nConfig Groups:")
2068
+ print_tabular_data(group_data, sort=sort)
2069
+
2070
+ if show_available:
2071
+ avail_data: defaultdict[str, list[str]] = defaultdict(list)
2072
+ avail_adapters = self.getDeviceAdapterNames()
2073
+ for adapt in avail_adapters:
2074
+ with suppress(Exception):
2075
+ devices = self.getAvailableDevices(adapt)
2076
+ descriptions = self.getAvailableDeviceDescriptions(adapt)
2077
+ types = self.getAvailableDeviceTypes(adapt)
2078
+ for dev, desc, type_ in zip(devices, descriptions, types):
2079
+ avail_data["Library, DeviceName"].append(f"{adapt!r}, {dev!r}")
2080
+ avail_data["Type"].append(str(DeviceType(type_)))
2081
+ avail_data["Description"].append(desc)
2082
+
2083
+ print("\nAvailable Devices:")
2084
+ print_tabular_data(avail_data, sort=sort)
2085
+
2037
2086
  def state(
2038
2087
  self, *, cached: bool = True, include_time: bool = False, **_kwargs: Any
2039
2088
  ) -> SummaryMetaV1:
@@ -2089,7 +2138,7 @@ class CMMCorePlus(pymmcore.CMMCore):
2089
2138
  self.events.propertyChanged.emit(device, properties[i], val)
2090
2139
 
2091
2140
  @contextmanager
2092
- def setContext(self, **kwargs: Any) -> Iterator[None]:
2141
+ def setContext(self, **kwargs: Unpack[SetContextKwargs]) -> Iterator[None]:
2093
2142
  """Set core properties in a context restoring the initial values on exit.
2094
2143
 
2095
2144
  :sparkles: *This method is new in `CMMCorePlus`.*
@@ -2098,9 +2147,12 @@ class CMMCorePlus(pymmcore.CMMCore):
2098
2147
  ----------
2099
2148
  **kwargs : Any
2100
2149
  Keyword arguments may be any `Name` for which `get<Name>` and `set<Name>`
2101
- methods exist. For example, `setContext(exposure=10)` will call
2150
+ methods exist (where the first letter in `<Name>` may be either lower or
2151
+ upper case). For example, `setContext(exposure=10)` will call
2102
2152
  `setExposure(10)` when entering the context and `setExposure(<initial>)`
2103
- when exiting the context.
2153
+ when exiting the context. If the property is not found, a warning is logged
2154
+ and the property is skipped. If the value is a tuple, it is unpacked and
2155
+ passed to the `set<Name>` method (but lists are not unpacked).
2104
2156
 
2105
2157
  Examples
2106
2158
  --------
@@ -2120,11 +2172,16 @@ class CMMCorePlus(pymmcore.CMMCore):
2120
2172
  try:
2121
2173
  for name, v in kwargs.items():
2122
2174
  name = name[0].upper() + name[1:]
2123
- try:
2124
- orig_values[name] = getattr(self, f"get{name}")()
2125
- getattr(self, f"set{name}")(v)
2126
- except AttributeError:
2175
+ get_name, set_name = f"get{name}", f"set{name}"
2176
+ if not hasattr(self, get_name) or not hasattr(self, set_name):
2127
2177
  logger.warning("%s is not a valid property, skipping.", name)
2178
+ continue
2179
+
2180
+ orig_values[name] = getattr(self, get_name)()
2181
+ if isinstance(v, tuple):
2182
+ getattr(self, set_name)(*v)
2183
+ else:
2184
+ getattr(self, set_name)(v)
2128
2185
  yield
2129
2186
  finally:
2130
2187
  for k, v in orig_values.items():
@@ -2186,7 +2243,16 @@ class CMMCorePlus(pymmcore.CMMCore):
2186
2243
  False
2187
2244
  ```
2188
2245
  """
2189
- return can_sequence_events(self, e1, e2, cur_length)
2246
+ warnings.warn(
2247
+ "canSequenceEvents is deprecated.\nPlease use "
2248
+ "`list(pymmcore_plus.core.iter_sequenced_events(core, [e1, e2]))` "
2249
+ "to see how this core will combine MDAEvents into SequencedEvents.",
2250
+ DeprecationWarning,
2251
+ stacklevel=2,
2252
+ )
2253
+ from ._sequencing import can_sequence_events
2254
+
2255
+ return can_sequence_events(self, e1, e2)
2190
2256
 
2191
2257
 
2192
2258
  for name in (
@@ -3,9 +3,7 @@ from __future__ import annotations
3
3
  from functools import cached_property
4
4
  from typing import TYPE_CHECKING, Any, TypedDict
5
5
 
6
- from pymmcore import g_Keyword_Label, g_Keyword_State
7
-
8
- from ._constants import DeviceType, PropertyType
6
+ from ._constants import DeviceType, Keyword, PropertyType
9
7
  from .events._device_signal_view import _DevicePropValueSignal
10
8
 
11
9
  if TYPE_CHECKING:
@@ -149,10 +147,10 @@ class DeviceProperty:
149
147
  # https://github.com/micro-manager/mmCoreAndDevices/issues/172
150
148
  allowed = self._mmc.getAllowedPropertyValues(self.device, self.name)
151
149
  if not allowed and self.deviceType() is DeviceType.StateDevice:
152
- if self.name == g_Keyword_State:
150
+ if self.name == Keyword.State:
153
151
  n_states = self._mmc.getNumberOfStates(self.device)
154
152
  allowed = tuple(str(i) for i in range(n_states))
155
- elif self.name == g_Keyword_Label:
153
+ elif self.name == Keyword.Label:
156
154
  allowed = self._mmc.getStateLabels(self.device)
157
155
  return allowed
158
156