pymmcore-plus 0.12.0__tar.gz → 0.13.1__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 (96) hide show
  1. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/.gitignore +1 -0
  2. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/PKG-INFO +14 -6
  3. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/pyproject.toml +16 -8
  4. pymmcore_plus-0.13.1/src/pymmcore_plus/_benchmark.py +203 -0
  5. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/_cli.py +78 -13
  6. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/_logger.py +10 -2
  7. pymmcore_plus-0.13.1/src/pymmcore_plus/_pymmcore.py +12 -0
  8. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/_util.py +16 -10
  9. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/core/__init__.py +3 -0
  10. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/core/_config.py +1 -1
  11. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/core/_config_group.py +2 -2
  12. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/core/_constants.py +27 -3
  13. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/core/_device.py +4 -4
  14. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/core/_metadata.py +1 -1
  15. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/core/_mmcore_plus.py +184 -118
  16. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/core/_property.py +3 -5
  17. pymmcore_plus-0.13.1/src/pymmcore_plus/core/_sequencing.py +434 -0
  18. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/core/events/__init__.py +3 -3
  19. pymmcore_plus-0.13.1/src/pymmcore_plus/experimental/unicore/__init__.py +14 -0
  20. pymmcore_plus-0.13.1/src/pymmcore_plus/experimental/unicore/_device_manager.py +173 -0
  21. pymmcore_plus-0.13.1/src/pymmcore_plus/experimental/unicore/_proxy.py +127 -0
  22. pymmcore_plus-0.13.1/src/pymmcore_plus/experimental/unicore/_unicore.py +703 -0
  23. pymmcore_plus-0.13.1/src/pymmcore_plus/experimental/unicore/devices/__init__.py +0 -0
  24. pymmcore_plus-0.13.1/src/pymmcore_plus/experimental/unicore/devices/_device.py +269 -0
  25. pymmcore_plus-0.13.1/src/pymmcore_plus/experimental/unicore/devices/_properties.py +400 -0
  26. pymmcore_plus-0.13.1/src/pymmcore_plus/experimental/unicore/devices/_stage.py +221 -0
  27. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/install.py +10 -7
  28. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/mda/_engine.py +152 -43
  29. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/mda/_runner.py +8 -1
  30. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/mda/events/__init__.py +2 -2
  31. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/mda/handlers/_ome_zarr_writer.py +2 -2
  32. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/mda/handlers/_tensorstore_handler.py +6 -2
  33. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/metadata/functions.py +18 -8
  34. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/metadata/schema.py +6 -5
  35. pymmcore_plus-0.13.1/src/pymmcore_plus/mocks.py +49 -0
  36. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/model/_config_file.py +1 -1
  37. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/model/_core_device.py +10 -1
  38. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/model/_device.py +17 -6
  39. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/model/_property.py +11 -2
  40. pymmcore_plus-0.13.1/src/pymmcore_plus/py.typed +0 -0
  41. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/seq_tester.py +1 -1
  42. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/tests/conftest.py +43 -9
  43. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/tests/io/test_zarr_writers.py +14 -2
  44. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/tests/test_adapter_class.py +1 -1
  45. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/tests/test_cli.py +36 -11
  46. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/tests/test_core.py +32 -78
  47. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/tests/test_events.py +26 -19
  48. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/tests/test_mda.py +75 -21
  49. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/tests/test_model.py +9 -1
  50. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/tests/test_pixel_config_class.py +1 -1
  51. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/tests/test_sequencing.py +15 -28
  52. pymmcore_plus-0.13.1/tests/test_slm_image.py +68 -0
  53. pymmcore_plus-0.13.1/tests/unicore/test_unicore.py +286 -0
  54. pymmcore_plus-0.13.1/tests/unicore/test_xy_stage.py +199 -0
  55. pymmcore_plus-0.12.0/src/pymmcore_plus/core/_sequencing.py +0 -299
  56. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/LICENSE +0 -0
  57. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/README.md +0 -0
  58. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/__init__.py +3 -3
  59. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/_build.py +0 -0
  60. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/core/_adapter.py +0 -0
  61. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/core/events/_device_signal_view.py +0 -0
  62. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/core/events/_norm_slot.py +0 -0
  63. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/core/events/_prop_event_mixin.py +0 -0
  64. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/core/events/_protocol.py +0 -0
  65. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/core/events/_psygnal.py +0 -0
  66. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/core/events/_qsignals.py +0 -0
  67. pymmcore_plus-0.12.0/src/pymmcore_plus/py.typed → pymmcore_plus-0.13.1/src/pymmcore_plus/experimental/__init__.py +0 -0
  68. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/mda/__init__.py +1 -1
  69. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/mda/_protocol.py +0 -0
  70. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/mda/_thread_relay.py +0 -0
  71. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/mda/events/_protocol.py +0 -0
  72. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/mda/events/_psygnal.py +0 -0
  73. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/mda/events/_qsignals.py +0 -0
  74. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/mda/handlers/_5d_writer_base.py +0 -0
  75. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/mda/handlers/__init__.py +1 -1
  76. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/mda/handlers/_img_sequence_writer.py +0 -0
  77. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/mda/handlers/_ome_tiff_writer.py +0 -0
  78. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/mda/handlers/_util.py +0 -0
  79. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/metadata/__init__.py +3 -3
  80. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/metadata/serialize.py +0 -0
  81. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/model/__init__.py +0 -0
  82. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/model/_config_group.py +0 -0
  83. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/model/_core_link.py +0 -0
  84. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/model/_microscope.py +0 -0
  85. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/src/pymmcore_plus/model/_pixel_size_config.py +0 -0
  86. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/tests/__init__.py +0 -0
  87. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/tests/io/test_image_sequence_writer.py +0 -0
  88. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/tests/io/test_ome_tiff.py +0 -0
  89. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/tests/local_config.cfg +0 -0
  90. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/tests/test_bench.py +0 -0
  91. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/tests/test_config_group_class.py +0 -0
  92. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/tests/test_device_class.py +0 -0
  93. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/tests/test_metadata.py +0 -0
  94. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/tests/test_misc.py +0 -0
  95. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/tests/test_property_class.py +0 -0
  96. {pymmcore_plus-0.12.0 → pymmcore_plus-0.13.1}/tests/test_thread_relay.py +0 -0
@@ -139,6 +139,7 @@ cython_debug/
139
139
 
140
140
  Micro-Manager-*
141
141
  .vscode
142
+ .idea
142
143
 
143
144
  docs/_includes/_cmmcore_members.md
144
145
  docs/_includes/_cmmcore_table.md
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: pymmcore-plus
3
- Version: 0.12.0
3
+ Version: 0.13.1
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
@@ -20,6 +20,7 @@ Classifier: Programming Language :: Python :: 3.9
20
20
  Classifier: Programming Language :: Python :: 3.10
21
21
  Classifier: Programming Language :: Python :: 3.11
22
22
  Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Programming Language :: Python :: 3.13
23
24
  Classifier: Topic :: System :: Hardware
24
25
  Classifier: Topic :: System :: Hardware :: Hardware Drivers
25
26
  Classifier: Topic :: Utilities
@@ -29,11 +30,10 @@ Requires-Dist: platformdirs>=3.0.0
29
30
  Requires-Dist: psygnal>=0.7
30
31
  Requires-Dist: pymmcore>=10.7.0.71.0
31
32
  Requires-Dist: rich>=10.2.0
32
- Requires-Dist: tensorstore
33
+ Requires-Dist: tensorstore; python_version < '3.13'
33
34
  Requires-Dist: typer>=0.4.2
34
35
  Requires-Dist: typing-extensions
35
- Requires-Dist: useq-schema>=0.5.0
36
- Requires-Dist: wrapt>=1.14
36
+ Requires-Dist: useq-schema>=0.6.2
37
37
  Provides-Extra: cli
38
38
  Requires-Dist: rich>=10.2.0; extra == 'cli'
39
39
  Requires-Dist: typer>=0.4.2; extra == 'cli'
@@ -53,9 +53,17 @@ Requires-Dist: mkdocstrings==0.22.0; extra == 'docs'
53
53
  Provides-Extra: io
54
54
  Requires-Dist: tifffile>=2021.6.14; extra == 'io'
55
55
  Requires-Dist: zarr<3,>=2.2; extra == 'io'
56
+ Provides-Extra: pyqt5
57
+ Requires-Dist: pyqt5>=5.15.4; extra == 'pyqt5'
58
+ Provides-Extra: pyqt6
59
+ Requires-Dist: pyqt6<6.8,>=6.4.2; extra == 'pyqt6'
60
+ Provides-Extra: pyside2
61
+ Requires-Dist: pyside2>=5.15; extra == 'pyside2'
62
+ Provides-Extra: pyside6
63
+ Requires-Dist: pyside6<6.8,>=6.4.0; extra == 'pyside6'
56
64
  Provides-Extra: test
57
65
  Requires-Dist: msgpack; extra == 'test'
58
- Requires-Dist: msgspec; extra == 'test'
66
+ Requires-Dist: msgspec; (python_version < '3.13') and extra == 'test'
59
67
  Requires-Dist: pytest-cov>=4; extra == 'test'
60
68
  Requires-Dist: pytest-qt>=4; extra == 'test'
61
69
  Requires-Dist: pytest>=7.3.2; extra == 'test'
@@ -28,6 +28,7 @@ classifiers = [
28
28
  "Programming Language :: Python :: 3.10",
29
29
  "Programming Language :: Python :: 3.11",
30
30
  "Programming Language :: Python :: 3.12",
31
+ "Programming Language :: Python :: 3.13",
31
32
  "Topic :: System :: Hardware",
32
33
  "Topic :: System :: Hardware :: Hardware Drivers",
33
34
  "Topic :: Utilities",
@@ -38,10 +39,9 @@ dependencies = [
38
39
  "numpy >=1.17.3",
39
40
  "psygnal >=0.7",
40
41
  "pymmcore >=10.7.0.71.0",
41
- "typing-extensions", # not actually required at runtime
42
- "useq-schema >=0.5.0",
43
- "wrapt >=1.14",
44
- "tensorstore",
42
+ "typing-extensions", # not actually required at runtime
43
+ "useq-schema >=0.6.2",
44
+ "tensorstore; python_version < '3.13'",
45
45
  # cli requirements included by default for now
46
46
  "typer >=0.4.2",
47
47
  "rich >=10.2.0",
@@ -52,8 +52,12 @@ dependencies = [
52
52
  [project.optional-dependencies]
53
53
  cli = ["typer >=0.4.2", "rich >=10.2.0"]
54
54
  io = ["tifffile >=2021.6.14", "zarr >=2.2,<3"]
55
+ PySide2 = ["PySide2 >=5.15"]
56
+ PySide6 = ["PySide6 >=6.4.0,<6.8"]
57
+ PyQt5 = ["PyQt5 >=5.15.4"]
58
+ PyQt6 = ["PyQt6 >=6.4.2,<6.8"]
55
59
  test = [
56
- "msgspec",
60
+ "msgspec; python_version < '3.13'",
57
61
  "msgpack",
58
62
  "pytest-cov >=4",
59
63
  "pytest-qt >=4",
@@ -98,7 +102,7 @@ include = ["/src", "/tests"]
98
102
  only-include = ["src"]
99
103
  sources = ["src"]
100
104
 
101
- # https://beta.ruff.rs/docs/rules/
105
+ # https://docs.astral.sh/ruff/rules/
102
106
  [tool.ruff]
103
107
  line-length = 88
104
108
  target-version = "py39"
@@ -117,7 +121,7 @@ select = [
117
121
  "A001", # flake8-builtins
118
122
  "RUF", # ruff-specific rules
119
123
  "TID", # tidy
120
- "TCH", # typecheck
124
+ "TC", # typecheck
121
125
  "SLF", # private-access
122
126
  ]
123
127
  ignore = [
@@ -141,6 +145,7 @@ docstring-code-format = true
141
145
  minversion = "6.0"
142
146
  testpaths = ["tests"]
143
147
  filterwarnings = ["error", "ignore:Failed to disconnect::pytestqt"]
148
+ markers = ["run_last: mark a test to run last"]
144
149
 
145
150
  # https://mypy.readthedocs.io/en/stable/config_file.html
146
151
  [tool.mypy]
@@ -151,6 +156,9 @@ disallow_subclassing_any = false
151
156
  show_error_codes = true
152
157
  pretty = true
153
158
  plugins = "pydantic.mypy"
159
+ # see https://github.com/python/mypy/issues/5374 and related discussions
160
+ # it causes more pain than it solves
161
+ disable_error_code = ["type-abstract"]
154
162
 
155
163
  [[tool.mypy.overrides]]
156
164
  module = ["tests.*"]
@@ -185,4 +193,4 @@ ignore = [
185
193
  ]
186
194
 
187
195
  [tool.typos.default]
188
- extend-ignore-identifiers-re = ["(?i)nd2?.*", "(?i)ome"]
196
+ extend-ignore-identifiers-re = ["(?i)nd2?.*", "(?i)ome", "anager"]
@@ -0,0 +1,203 @@
1
+ from __future__ import annotations
2
+
3
+ import timeit
4
+ import warnings
5
+ from typing import TYPE_CHECKING
6
+
7
+ from pymmcore_plus import CMMCorePlus, DeviceType
8
+
9
+ if TYPE_CHECKING:
10
+ from collections.abc import Iterable, Iterator, Sequence
11
+
12
+ from pymmcore_plus.core._device import Device
13
+
14
+
15
+ class Benchmark:
16
+ device_type = DeviceType.Camera
17
+
18
+ def __init__(self, core: CMMCorePlus, label: str = "") -> None:
19
+ self.core = core
20
+ self.label = label
21
+
22
+ def setup(self) -> None:
23
+ pass
24
+
25
+ def device(self) -> Device | None:
26
+ if self.label is not None:
27
+ return self.core.getDeviceObject(self.label)
28
+ return None
29
+
30
+ def run(self, number: int) -> Iterator[tuple[str, float | str]]:
31
+ # get methods in the order of definition, in reverse MRO order
32
+
33
+ try:
34
+ self.setup()
35
+ except Exception as e: # pragma: no cover
36
+ warnings.warn(
37
+ f"Setup failed on device {self.label!r}: {e}",
38
+ RuntimeWarning,
39
+ stacklevel=2,
40
+ )
41
+ return
42
+
43
+ methods: list[str] = []
44
+ for base in reversed(type(self).mro()):
45
+ methods.extend(m for m in base.__dict__ if m.startswith("bench_"))
46
+
47
+ for method_name in methods:
48
+ try:
49
+ t = timeit.timeit(getattr(self, method_name), number=number)
50
+ result: float | str = round(1000 * t / number, 3)
51
+ except Exception as e:
52
+ result = str(e)
53
+ yield method_name[6:], result
54
+
55
+
56
+ class CoreBenchmark(Benchmark):
57
+ device_type = DeviceType.Core
58
+
59
+ def bench_getDeviceAdapterNames(self) -> None:
60
+ self.core.getDeviceAdapterNames()
61
+
62
+ def bench_getLoadedDevices(self) -> None:
63
+ self.core.getLoadedDevices()
64
+
65
+ def bench_getSystemState(self) -> None:
66
+ self.core.getSystemState()
67
+
68
+
69
+ class CameraBenchmark(Benchmark):
70
+ device_type = DeviceType.Camera
71
+
72
+ def setup(self) -> None:
73
+ self.core.setCameraDevice(self.label)
74
+ self.core.setExposure(self.label, 1)
75
+
76
+ def bench_getMultiROI(self) -> None:
77
+ self.core.getMultiROI()
78
+
79
+ def bench_getExposure(self) -> None:
80
+ self.core.getExposure(self.label)
81
+
82
+ def bench_snapImage(self) -> None:
83
+ self.core.snapImage()
84
+
85
+ def bench_getImage(self) -> None:
86
+ self.core.getImage()
87
+
88
+ def bench_getImageWidth(self) -> None:
89
+ self.core.getImageWidth()
90
+
91
+ def bench_getImageHeight(self) -> None:
92
+ self.core.getImageHeight()
93
+
94
+ def bench_getImageBufferSize(self) -> None:
95
+ self.core.getImageBufferSize()
96
+
97
+ def bench_getImageBitDepth(self) -> None:
98
+ self.core.getImageBitDepth()
99
+
100
+ def bench_getNumberOfComponents(self) -> None:
101
+ self.core.getNumberOfComponents()
102
+
103
+ def bench_getNumberOfCameraChannels(self) -> None:
104
+ self.core.getNumberOfCameraChannels()
105
+
106
+
107
+ class XYStageBenchmark(Benchmark):
108
+ device_type = DeviceType.XYStage
109
+
110
+ def setup(self) -> None:
111
+ self.core.setXYStageDevice(self.label)
112
+ self.position = self.core.getXYPosition(self.label)
113
+
114
+ def bench_getXYPosition(self) -> None:
115
+ self.core.getXYPosition(self.label)
116
+
117
+ def bench_getXPosition(self) -> None:
118
+ self.core.getXPosition(self.label)
119
+
120
+ def bench_getYPosition(self) -> None:
121
+ self.core.getYPosition(self.label)
122
+
123
+ def bench_setXYPosition(self) -> None:
124
+ self.core.setXYPosition(self.label, *self.position)
125
+
126
+ def bench_setRelativeXYPosition(self) -> None:
127
+ self.core.setRelativeXYPosition(self.label, 0, 0)
128
+
129
+ def bench_isXYStageSequenceable(self) -> None:
130
+ self.core.isXYStageSequenceable(self.label)
131
+
132
+
133
+ class StageBenchmark(Benchmark):
134
+ device_type = DeviceType.Stage
135
+
136
+ def setup(self) -> None:
137
+ self.position = self.core.getPosition(self.label)
138
+
139
+ def bench_getPosition(self) -> None:
140
+ self.core.getPosition(self.label)
141
+
142
+ def bench_setPosition(self) -> None:
143
+ self.core.setPosition(self.label, self.position)
144
+
145
+ def bench_setRelativePosition(self) -> None:
146
+ self.core.setRelativePosition(self.label, 0)
147
+
148
+ def bench_isStageSequenceable(self) -> None:
149
+ self.core.isStageSequenceable(self.label)
150
+
151
+ def bench_isStageLinearSequenceable(self) -> None:
152
+ self.core.isStageLinearSequenceable(self.label)
153
+
154
+
155
+ class StateBenchmark(Benchmark):
156
+ device_type = DeviceType.State
157
+
158
+ def setup(self) -> None:
159
+ self.initial_state = self.core.getState(self.label)
160
+ try:
161
+ self.labels: Sequence[str] = self.core.getStateLabels(self.label)
162
+ except Exception:
163
+ self.labels = []
164
+
165
+ def bench_getState(self) -> None:
166
+ self.core.getState(self.label)
167
+
168
+ def bench_setState(self) -> None:
169
+ self.core.setState(self.label, self.initial_state)
170
+
171
+ def bench_getNumberOfStates(self) -> None:
172
+ self.core.getNumberOfStates(self.label)
173
+
174
+ def bench_getStateLabel(self) -> None:
175
+ self.core.getStateLabel(self.label)
176
+
177
+ def bench_getStateFromLabel(self) -> None:
178
+ for label in self.labels:
179
+ self.core.getStateFromLabel(self.label, label)
180
+
181
+
182
+ def benchmark_core_and_devices(
183
+ core: CMMCorePlus, number: int = 100
184
+ ) -> Iterable[Device | None | tuple[str, float | str]]:
185
+ """Take an initialized core with devices and benchmark various methods.
186
+
187
+ Yields
188
+ ------
189
+ Device | None | tuple[str, float | str]
190
+ If a `Device`, it is the device object being benchmarked.
191
+ If None, it is the core object being benchmarked.
192
+ If a tuple, it is the method name and the time taken to run it.
193
+ """
194
+ for cls in Benchmark.__subclasses__():
195
+ if cls.device_type == DeviceType.Core:
196
+ bench = cls(core, "Core")
197
+ yield bench.device()
198
+ yield from bench.run(number)
199
+ else:
200
+ for dev in core.getLoadedDevicesOfType(cls.device_type):
201
+ bench = cls(core, dev)
202
+ yield bench.device()
203
+ yield from bench.run(number)
@@ -8,6 +8,9 @@ from contextlib import suppress
8
8
  from pathlib import Path
9
9
  from typing import Optional, Union, cast
10
10
 
11
+ from pymmcore_plus.core._device import Device
12
+ from pymmcore_plus.core._mmcore_plus import CMMCorePlus
13
+
11
14
  try:
12
15
  import typer
13
16
  from rich import print
@@ -28,14 +31,31 @@ app = typer.Typer(name="mmcore", no_args_is_help=True)
28
31
 
29
32
  def _show_version_and_exit(value: bool) -> None:
30
33
  if value:
31
- import pymmcore
32
-
33
34
  typer.echo(f"pymmcore-plus v{pymmcore_plus.__version__}")
34
- typer.echo(f"pymmcore v{pymmcore.__version__}")
35
- typer.echo(f"MMCore v{pymmcore.CMMCore().getAPIVersionInfo()}")
35
+ try:
36
+ import pymmcore_nano as pymmcore
37
+
38
+ typer.echo(f"pymmcore-nano v{pymmcore.__version__}")
39
+ except ImportError:
40
+ import pymmcore
41
+
42
+ typer.echo(f"pymmcore v{pymmcore.__version__}")
43
+ typer.echo(f"MMCore v{pymmcore.CMMCore().getVersionInfo()}")
44
+ typer.echo(f"{pymmcore.CMMCore().getAPIVersionInfo()}")
36
45
  raise typer.Exit()
37
46
 
38
47
 
48
+ CONFIG_PARAM = typer.Option(
49
+ None,
50
+ "-c",
51
+ "--config",
52
+ dir_okay=False,
53
+ exists=True,
54
+ resolve_path=True,
55
+ help="Path to Micro-Manager system configuration file.",
56
+ )
57
+
58
+
39
59
  @app.callback()
40
60
  def _main(
41
61
  version: Optional[bool] = typer.Option(
@@ -168,15 +188,7 @@ def run(
168
188
  resolve_path=True,
169
189
  help="Path to useq-schema file.",
170
190
  ),
171
- config: Optional[Path] = typer.Option(
172
- None,
173
- "-c",
174
- "--config",
175
- dir_okay=False,
176
- exists=True,
177
- resolve_path=True,
178
- help="Path to Micro-Manager system configuration file.",
179
- ),
191
+ config: Optional[Path] = CONFIG_PARAM,
180
192
  z_go_up: Optional[bool] = typer.Option(None, help="Acquire from bottom to top."),
181
193
  z_top: Optional[float] = typer.Option(None, help="Top of z-stack."),
182
194
  z_bottom: Optional[float] = typer.Option(None, help="Bottom of z-stack."),
@@ -423,5 +435,58 @@ def _tail_file(file_path: Union[str, Path], interval: float = 0.1) -> None:
423
435
  time.sleep(1)
424
436
 
425
437
 
438
+ @app.command()
439
+ def bench(
440
+ config: Optional[Path] = CONFIG_PARAM,
441
+ number: int = typer.Option(
442
+ 10, "-n", "--number", help="Number of iterations for each test."
443
+ ),
444
+ ) -> None:
445
+ """Run a benchmark of Core and Devices loaded with `config` (or Demo)."""
446
+ from rich.console import Console
447
+ from rich.live import Live
448
+ from rich.table import Table
449
+
450
+ from pymmcore_plus._benchmark import benchmark_core_and_devices
451
+
452
+ console = Console()
453
+
454
+ core = CMMCorePlus()
455
+ if config is not None:
456
+ console.log(
457
+ f"Loading config {config} ...",
458
+ style="bright_blue",
459
+ end="",
460
+ )
461
+ core.loadSystemConfiguration(str(config))
462
+ else:
463
+ console.log("Loading DEMO configuration ...", style="bright_blue", end="")
464
+ core.loadSystemConfiguration()
465
+ console.log("Loaded.", style="bright_blue")
466
+
467
+ table = Table()
468
+ table.add_column("Method")
469
+ table.add_column("Time (ms)")
470
+
471
+ with Live(table, console=console, refresh_per_second=4):
472
+ for item in benchmark_core_and_devices(core, number):
473
+ if item is None:
474
+ table.add_row("Device: Core", "------", style="yellow")
475
+ elif isinstance(item, Device):
476
+ console.print(
477
+ f"Measuring ({item.type()}) Device: "
478
+ f"{item.label!r} <{item.library()}::{item.name()}>"
479
+ f": {item.description()}",
480
+ style="#333333",
481
+ )
482
+ table.add_row(f"Device: {item.label}", "------", style="yellow")
483
+ else:
484
+ method, time = item
485
+ if isinstance(time, float):
486
+ table.add_row(method, f"{time:.4f}")
487
+ else:
488
+ table.add_row(method, str(time), style="red")
489
+
490
+
426
491
  def main() -> None: # pragma: no cover
427
492
  app()
@@ -118,9 +118,17 @@ def configure_logging(
118
118
 
119
119
  # automatically log to stderr
120
120
  if log_to_stderr and sys.stderr:
121
- stderr_handler = logging.StreamHandler(sys.stderr)
121
+ # try to use rich for stderr logging
122
+ # fallback to plain text if rich is not installed
123
+ try:
124
+ from rich.logging import RichHandler
125
+
126
+ stderr_handler: logging.Handler = RichHandler()
127
+ except ImportError:
128
+ stderr_handler = logging.StreamHandler(sys.stderr)
129
+ stderr_handler.setFormatter(CustomFormatter())
130
+
122
131
  stderr_handler.setLevel(stderr_level)
123
- stderr_handler.setFormatter(CustomFormatter())
124
132
  logger.addHandler(stderr_handler)
125
133
 
126
134
  # automatically log to file
@@ -0,0 +1,12 @@
1
+ """Internal module to choose between pymmcore and pymmcore-nano."""
2
+
3
+ try:
4
+ from pymmcore_nano import * # noqa F403
5
+ from pymmcore_nano import __version__
6
+
7
+ BACKEND = "pymmcore-nano"
8
+ except ImportError:
9
+ from pymmcore import * # noqa F403
10
+ from pymmcore import __version__ # noqa F401
11
+
12
+ BACKEND = "pymmcore"
@@ -39,7 +39,7 @@ except ImportError:
39
39
  from contextlib import nullcontext as no_stdout
40
40
 
41
41
 
42
- __all__ = ["find_micromanager", "retry", "no_stdout", "signals_backend"]
42
+ __all__ = ["find_micromanager", "no_stdout", "retry", "signals_backend"]
43
43
 
44
44
  APP_NAME = "pymmcore-plus"
45
45
  USER_DATA_DIR = Path(user_data_dir(appname=APP_NAME))
@@ -418,13 +418,10 @@ def _sorted_rows(data: dict, sort: str | None) -> list[tuple]:
418
418
  """Return a list of rows, sorted by the given column name."""
419
419
  rows = list(zip(*data.values()))
420
420
  if sort is not None:
421
- try:
421
+ with suppress(ValueError):
422
+ # silently ignore if the sort column is not found
422
423
  sort_idx = [x.lower() for x in data].index(sort.lower())
423
- except ValueError: # pragma: no cover
424
- raise ValueError(
425
- f"invalid sort column: {sort!r}. Must be one of {list(data)}"
426
- ) from None
427
- rows.sort(key=lambda x: x[sort_idx])
424
+ rows.sort(key=lambda x: x[sort_idx])
428
425
  return rows
429
426
 
430
427
 
@@ -550,16 +547,25 @@ def system_info() -> dict[str, str]:
550
547
 
551
548
  This backs the `mmcore info` command in the CLI.
552
549
  """
553
- import pymmcore
554
-
555
550
  import pymmcore_plus
556
551
 
557
552
  info = {
558
553
  "python": sys.version,
559
554
  "platform": platform.platform(),
560
555
  "pymmcore-plus": getattr(pymmcore_plus, "__version__", "err"),
561
- "pymmcore": getattr(pymmcore, "__version__", "err"),
562
556
  }
557
+ try:
558
+ import pymmcore
559
+
560
+ info["pymmcore"] = getattr(pymmcore, "__version__", "err")
561
+ except ImportError:
562
+ info["pymmcore"] = ""
563
+ try:
564
+ import pymmcore_nano
565
+
566
+ info["pymmcore-nano"] = getattr(pymmcore_nano, "__version__", "err")
567
+ except ImportError:
568
+ info["pymmcore-nano"] = ""
563
569
 
564
570
  with suppress(Exception):
565
571
  core = pymmcore_plus.CMMCorePlus.instance()
@@ -18,6 +18,8 @@ __all__ = [
18
18
  "PixelFormat",
19
19
  "PortType",
20
20
  "PropertyType",
21
+ "SequencedEvent",
22
+ "iter_sequenced_events",
21
23
  ]
22
24
 
23
25
  from ._adapter import DeviceAdapter
@@ -41,3 +43,4 @@ from ._device import Device
41
43
  from ._metadata import Metadata
42
44
  from ._mmcore_plus import CMMCorePlus
43
45
  from ._property import DeviceProperty
46
+ from ._sequencing import SequencedEvent, iter_sequenced_events
@@ -5,7 +5,7 @@ from __future__ import annotations
5
5
  from collections import defaultdict
6
6
  from typing import TYPE_CHECKING, Any, overload
7
7
 
8
- import pymmcore
8
+ import pymmcore_plus._pymmcore as pymmcore
9
9
 
10
10
  if TYPE_CHECKING:
11
11
  from collections.abc import Iterable, Iterator
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  from collections.abc import Iterator, MutableMapping
4
4
  from typing import TYPE_CHECKING, Any, Literal, overload
5
5
 
6
- import pymmcore
6
+ import pymmcore_plus._pymmcore as pymmcore
7
7
 
8
8
  from ._config import Configuration
9
9
  from ._property import DeviceProperty
@@ -73,7 +73,7 @@ class ConfigGroup(MutableMapping[str, Configuration]):
73
73
  def __getitem__(self, configName: str) -> Configuration:
74
74
  try:
75
75
  return self._mmc.getConfigData(self._name, configName)
76
- except ValueError as e:
76
+ except (ValueError, RuntimeError) as e:
77
77
  if configName not in self:
78
78
  raise KeyError(
79
79
  f"Group {self._name!r} does not have a config {configName!r}"
@@ -1,9 +1,9 @@
1
1
  from __future__ import annotations
2
2
 
3
- from enum import Enum, IntEnum
4
- from typing import Literal
3
+ from enum import Enum, IntEnum, auto
4
+ from typing import Any, Literal
5
5
 
6
- import pymmcore
6
+ import pymmcore_plus._pymmcore as pymmcore
7
7
 
8
8
  # NOTE: by using pymmcore.attributes, we guarantee that the values are the same
9
9
  # however, we also risk AttributeErrors in the future.
@@ -157,6 +157,7 @@ class PropertyType(IntEnum):
157
157
  String = pymmcore.String
158
158
  Float = pymmcore.Float
159
159
  Integer = pymmcore.Integer
160
+ Boolean = auto() # not supported in pymmcore
160
161
 
161
162
  def to_python(self) -> type | None:
162
163
  return {0: None, 1: str, 2: float, 3: int}[self]
@@ -167,6 +168,29 @@ class PropertyType(IntEnum):
167
168
  def __repr__(self) -> Literal["undefined", "float", "int", "str"]:
168
169
  return getattr(self.to_python(), "__name__", "undefined")
169
170
 
171
+ @classmethod
172
+ def create(cls, value: Any) -> PropertyType:
173
+ if isinstance(value, PropertyType):
174
+ return value
175
+ if value is None:
176
+ return PropertyType.Undef
177
+ if isinstance(value, str):
178
+ return PropertyType[value.lower().capitalize()]
179
+ if isinstance(value, type):
180
+ if value is float:
181
+ return PropertyType.Float
182
+ elif value is int:
183
+ return PropertyType.Integer
184
+ elif value is str:
185
+ return PropertyType.String
186
+ elif value is bool:
187
+ return PropertyType.Boolean
188
+
189
+ raise TypeError(
190
+ f"Property type must be a PropertyType enum member, "
191
+ f"a string, or a type. Got: {type(value)}"
192
+ )
193
+
170
194
 
171
195
  class ActionType(IntEnum):
172
196
  NoAction = pymmcore.NoAction
@@ -43,12 +43,12 @@ class Device:
43
43
  >>> device.schema() # JSON schema of device properties
44
44
  """
45
45
 
46
- UNASIGNED = "__UNASIGNED__"
46
+ UNASSIGNED = "__UNASSIGNED__"
47
47
  propertyChanged: PSignalInstance
48
48
 
49
49
  def __init__(
50
50
  self,
51
- device_label: str = UNASIGNED,
51
+ device_label: str = UNASSIGNED,
52
52
  mmcore: CMMCorePlus | None = None,
53
53
  adapter_name: str = "",
54
54
  device_name: str = "",
@@ -162,7 +162,7 @@ class Device:
162
162
  raise TypeError("Must specify device_name")
163
163
  if device_label:
164
164
  self.label = device_label
165
- elif self.label == self.UNASIGNED:
165
+ elif self.label == self.UNASSIGNED:
166
166
  self.label = f"{adapter_name}-{device_name}"
167
167
 
168
168
  self._mmc.loadDevice(self.label, adapter_name, device_name)
@@ -208,7 +208,7 @@ class Device:
208
208
  def __repr__(self) -> str:
209
209
  if self.isLoaded():
210
210
  n = len(self.propertyNames())
211
- props = f'{n} {"properties" if n>1 else "property"}'
211
+ props = f"{n} {'properties' if n > 1 else 'property'}"
212
212
  lib = f"({self.library()}::{self.name()}) "
213
213
  else:
214
214
  props = "NOT LOADED"
@@ -4,7 +4,7 @@ from collections.abc import ItemsView, Iterator, KeysView, Mapping, ValuesView
4
4
  from types import new_class
5
5
  from typing import Any, cast
6
6
 
7
- import pymmcore
7
+ import pymmcore_plus._pymmcore as pymmcore
8
8
 
9
9
  _NULL = object()
10
10