pymmcore-plus 0.13.7__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 (111) hide show
  1. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/PKG-INFO +14 -37
  2. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/pyproject.toml +57 -36
  3. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/__init__.py +2 -0
  4. pymmcore_plus-0.15.0/src/pymmcore_plus/_accumulator.py +258 -0
  5. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/_pymmcore.py +4 -2
  6. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/core/__init__.py +34 -1
  7. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/core/_constants.py +21 -3
  8. pymmcore_plus-0.15.0/src/pymmcore_plus/core/_device.py +937 -0
  9. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/core/_mmcore_plus.py +260 -47
  10. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/core/events/_protocol.py +49 -34
  11. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/core/events/_psygnal.py +2 -2
  12. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/experimental/unicore/__init__.py +7 -1
  13. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/experimental/unicore/_proxy.py +20 -3
  14. pymmcore_plus-0.15.0/src/pymmcore_plus/experimental/unicore/core/_sequence_buffer.py +318 -0
  15. pymmcore_plus-0.15.0/src/pymmcore_plus/experimental/unicore/core/_unicore.py +1702 -0
  16. pymmcore_plus-0.15.0/src/pymmcore_plus/experimental/unicore/devices/_camera.py +196 -0
  17. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/experimental/unicore/devices/_device.py +54 -28
  18. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/experimental/unicore/devices/_properties.py +8 -1
  19. pymmcore_plus-0.15.0/src/pymmcore_plus/experimental/unicore/devices/_slm.py +82 -0
  20. pymmcore_plus-0.15.0/src/pymmcore_plus/experimental/unicore/devices/_state.py +152 -0
  21. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/mda/events/_protocol.py +8 -8
  22. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/mda/handlers/_tensorstore_handler.py +3 -1
  23. pymmcore_plus-0.15.0/src/pymmcore_plus/py.typed +0 -0
  24. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/tests/conftest.py +11 -8
  25. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/tests/io/test_zarr_writers.py +6 -0
  26. pymmcore_plus-0.15.0/tests/test_accumulators.py +107 -0
  27. pymmcore_plus-0.15.0/tests/test_bench.py +191 -0
  28. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/tests/test_cli.py +4 -2
  29. pymmcore_plus-0.15.0/tests/test_device_class.py +234 -0
  30. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/tests/test_events.py +5 -4
  31. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/tests/test_sequencing.py +8 -1
  32. pymmcore_plus-0.15.0/tests/unicore/conftest.py +13 -0
  33. pymmcore_plus-0.15.0/tests/unicore/test_camera.py +266 -0
  34. pymmcore_plus-0.15.0/tests/unicore/test_sequence_buffer.py +577 -0
  35. pymmcore_plus-0.15.0/tests/unicore/test_slm.py +337 -0
  36. pymmcore_plus-0.15.0/tests/unicore/test_state.py +362 -0
  37. pymmcore_plus-0.13.7/src/pymmcore_plus/core/_device.py +0 -217
  38. pymmcore_plus-0.13.7/src/pymmcore_plus/experimental/unicore/_unicore.py +0 -703
  39. pymmcore_plus-0.13.7/tests/test_bench.py +0 -89
  40. pymmcore_plus-0.13.7/tests/test_device_class.py +0 -73
  41. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/.gitignore +0 -0
  42. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/LICENSE +0 -0
  43. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/README.md +0 -0
  44. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/_benchmark.py +0 -0
  45. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/_build.py +0 -0
  46. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/_cli.py +0 -0
  47. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/_logger.py +0 -0
  48. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/_util.py +0 -0
  49. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/core/_adapter.py +0 -0
  50. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/core/_config.py +0 -0
  51. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/core/_config_group.py +0 -0
  52. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/core/_metadata.py +0 -0
  53. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/core/_property.py +0 -0
  54. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/core/_sequencing.py +0 -0
  55. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/core/events/__init__.py +0 -0
  56. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/core/events/_device_signal_view.py +0 -0
  57. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/core/events/_norm_slot.py +0 -0
  58. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/core/events/_prop_event_mixin.py +0 -0
  59. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/core/events/_qsignals.py +0 -0
  60. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/experimental/__init__.py +0 -0
  61. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/experimental/unicore/_device_manager.py +0 -0
  62. {pymmcore_plus-0.13.7/src/pymmcore_plus/experimental/unicore/devices → pymmcore_plus-0.15.0/src/pymmcore_plus/experimental/unicore/core}/__init__.py +0 -0
  63. /pymmcore_plus-0.13.7/src/pymmcore_plus/py.typed → /pymmcore_plus-0.15.0/src/pymmcore_plus/experimental/unicore/devices/__init__.py +0 -0
  64. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/experimental/unicore/devices/_stage.py +0 -0
  65. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/install.py +0 -0
  66. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/mda/__init__.py +0 -0
  67. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/mda/_engine.py +0 -0
  68. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/mda/_protocol.py +0 -0
  69. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/mda/_runner.py +0 -0
  70. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/mda/_thread_relay.py +0 -0
  71. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/mda/events/__init__.py +0 -0
  72. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/mda/events/_psygnal.py +0 -0
  73. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/mda/events/_qsignals.py +0 -0
  74. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/mda/handlers/_5d_writer_base.py +0 -0
  75. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/mda/handlers/__init__.py +0 -0
  76. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/mda/handlers/_img_sequence_writer.py +0 -0
  77. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/mda/handlers/_ome_tiff_writer.py +0 -0
  78. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/mda/handlers/_ome_zarr_writer.py +0 -0
  79. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/mda/handlers/_util.py +0 -0
  80. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/metadata/__init__.py +0 -0
  81. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/metadata/functions.py +0 -0
  82. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/metadata/schema.py +0 -0
  83. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/metadata/serialize.py +0 -0
  84. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/mocks.py +0 -0
  85. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/model/__init__.py +0 -0
  86. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/model/_config_file.py +0 -0
  87. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/model/_config_group.py +0 -0
  88. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/model/_core_device.py +0 -0
  89. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/model/_core_link.py +0 -0
  90. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/model/_device.py +0 -0
  91. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/model/_microscope.py +0 -0
  92. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/model/_pixel_size_config.py +0 -0
  93. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/model/_property.py +0 -0
  94. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/src/pymmcore_plus/seq_tester.py +0 -0
  95. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/tests/__init__.py +0 -0
  96. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/tests/io/test_image_sequence_writer.py +0 -0
  97. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/tests/io/test_ome_tiff.py +0 -0
  98. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/tests/local_config.cfg +0 -0
  99. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/tests/test_adapter_class.py +0 -0
  100. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/tests/test_config_group_class.py +0 -0
  101. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/tests/test_core.py +0 -0
  102. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/tests/test_mda.py +0 -0
  103. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/tests/test_metadata.py +0 -0
  104. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/tests/test_misc.py +0 -0
  105. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/tests/test_model.py +0 -0
  106. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/tests/test_pixel_config_class.py +0 -0
  107. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/tests/test_property_class.py +0 -0
  108. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/tests/test_slm_image.py +0 -0
  109. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/tests/test_thread_relay.py +0 -0
  110. {pymmcore_plus-0.13.7 → pymmcore_plus-0.15.0}/tests/unicore/test_unicore.py +0 -0
  111. {pymmcore_plus-0.13.7 → 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.13.7
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
@@ -25,55 +25,32 @@ Classifier: Topic :: System :: Hardware
25
25
  Classifier: Topic :: System :: Hardware :: Hardware Drivers
26
26
  Classifier: Topic :: Utilities
27
27
  Requires-Python: >=3.9
28
- Requires-Dist: numpy>=1.17.3
28
+ Requires-Dist: numpy>=1.25.2
29
+ Requires-Dist: numpy>=1.26.0; python_version >= '3.12'
30
+ Requires-Dist: numpy>=2.1.0; python_version >= '3.13'
29
31
  Requires-Dist: platformdirs>=3.0.0
30
- Requires-Dist: psygnal>=0.7
31
- Requires-Dist: pymmcore>=10.7.0.71.0
32
+ Requires-Dist: psygnal>=0.10
33
+ Requires-Dist: pymmcore>=11.2.1.71.0
32
34
  Requires-Dist: rich>=10.2.0
33
- Requires-Dist: tensorstore<=0.1.71
35
+ Requires-Dist: tensorstore!=0.1.72,>=0.1.67
36
+ Requires-Dist: tensorstore!=0.1.72,>=0.1.71; python_version >= '3.13'
34
37
  Requires-Dist: typer>=0.4.2
35
- Requires-Dist: typing-extensions
36
- Requires-Dist: useq-schema>=0.7.0
38
+ Requires-Dist: typing-extensions>=4
39
+ Requires-Dist: useq-schema>=0.7.2
37
40
  Provides-Extra: cli
38
41
  Requires-Dist: rich>=10.2.0; extra == 'cli'
39
42
  Requires-Dist: typer>=0.4.2; extra == 'cli'
40
- Provides-Extra: dev
41
- Requires-Dist: ipython; extra == 'dev'
42
- Requires-Dist: mypy; extra == 'dev'
43
- Requires-Dist: pdbpp; (sys_platform != 'win32') and extra == 'dev'
44
- Requires-Dist: pre-commit; extra == 'dev'
45
- Requires-Dist: ruff; extra == 'dev'
46
- Requires-Dist: tensorstore-stubs; extra == 'dev'
47
- Provides-Extra: docs
48
- Requires-Dist: mkdocs-autorefs==1.3.1; extra == 'docs'
49
- Requires-Dist: mkdocs-material; extra == 'docs'
50
- Requires-Dist: mkdocs-typer==0.0.3; extra == 'docs'
51
- Requires-Dist: mkdocs>=1.4; extra == 'docs'
52
- Requires-Dist: mkdocstrings-python==1.1.2; extra == 'docs'
53
- Requires-Dist: mkdocstrings==0.22.0; extra == 'docs'
54
43
  Provides-Extra: io
55
44
  Requires-Dist: tifffile>=2021.6.14; extra == 'io'
56
- Requires-Dist: zarr<3,>=2.2; extra == 'io'
45
+ Requires-Dist: zarr<3,>=2.15; extra == 'io'
57
46
  Provides-Extra: pyqt5
58
47
  Requires-Dist: pyqt5>=5.15.4; extra == 'pyqt5'
59
48
  Provides-Extra: pyqt6
60
- Requires-Dist: pyqt6<6.8,>=6.4.2; extra == 'pyqt6'
49
+ Requires-Dist: pyqt6>=6.4.2; extra == 'pyqt6'
61
50
  Provides-Extra: pyside2
62
- Requires-Dist: pyside2>=5.15; extra == 'pyside2'
51
+ Requires-Dist: pyside2>=5.15.2.1; extra == 'pyside2'
63
52
  Provides-Extra: pyside6
64
- Requires-Dist: pyside6<6.8,>=6.4.0; extra == 'pyside6'
65
- Provides-Extra: test
66
- Requires-Dist: msgpack; extra == 'test'
67
- Requires-Dist: msgspec; extra == 'test'
68
- Requires-Dist: pytest-cov>=4; extra == 'test'
69
- Requires-Dist: pytest-qt>=4; extra == 'test'
70
- Requires-Dist: pytest>=7.3.2; extra == 'test'
71
- Requires-Dist: qtpy>=2; extra == 'test'
72
- Requires-Dist: rich; extra == 'test'
73
- Requires-Dist: tifffile>=2021.6.14; extra == 'test'
74
- Requires-Dist: typer>=0.4.2; extra == 'test'
75
- Requires-Dist: xarray; extra == 'test'
76
- Requires-Dist: zarr<3,>=2.2; extra == 'test'
53
+ Requires-Dist: pyside6==6.7.3; extra == 'pyside6'
77
54
  Description-Content-Type: text/markdown
78
55
 
79
56
  # pymmcore-plus
@@ -36,13 +36,15 @@ classifiers = [
36
36
  dynamic = ["version"]
37
37
  dependencies = [
38
38
  "platformdirs >=3.0.0",
39
- "numpy >=1.17.3",
40
- "psygnal >=0.7",
41
- "pymmcore >=10.7.0.71.0",
42
- "typing-extensions", # not actually required at runtime
43
- "useq-schema >=0.7.0",
44
- # until https://github.com/google/tensorstore/issues/217 is resolved
45
- "tensorstore <= 0.1.71",
39
+ "numpy >=2.1.0; python_version >= '3.13'",
40
+ "numpy >=1.26.0; python_version >= '3.12'",
41
+ "numpy >=1.25.2",
42
+ "psygnal >=0.10",
43
+ "pymmcore >=11.2.1.71.0",
44
+ "typing-extensions >=4", # not actually required at runtime
45
+ "useq-schema >=0.7.2",
46
+ "tensorstore >=0.1.71,!=0.1.72; python_version >= '3.13'",
47
+ "tensorstore >=0.1.67,!=0.1.72",
46
48
  # cli requirements included by default for now
47
49
  "typer >=0.4.2",
48
50
  "rich >=10.2.0",
@@ -52,40 +54,53 @@ dependencies = [
52
54
  # https://peps.python.org/pep-0621/#dependencies-optional-dependencies
53
55
  [project.optional-dependencies]
54
56
  cli = ["typer >=0.4.2", "rich >=10.2.0"]
55
- io = ["tifffile >=2021.6.14", "zarr >=2.2,<3"]
56
- PySide2 = ["PySide2 >=5.15"]
57
- PySide6 = ["PySide6 >=6.4.0,<6.8"]
57
+ io = ["tifffile >=2021.6.14", "zarr >=2.15,<3"]
58
+ PySide2 = ["PySide2 >=5.15.2.1"]
59
+ PySide6 = ["PySide6 ==6.7.3"]
58
60
  PyQt5 = ["PyQt5 >=5.15.4"]
59
- PyQt6 = ["PyQt6 >=6.4.2,<6.8"]
60
- test = [
61
- "msgspec",
62
- "msgpack",
63
- "pytest-cov >=4",
64
- "pytest-qt >=4",
65
- "pytest >=7.3.2",
66
- "qtpy >=2",
67
- "rich",
68
- "typer >=0.4.2",
69
- "tifffile >=2021.6.14",
70
- "zarr >=2.2,<3",
71
- "xarray",
72
- ]
73
- dev = [
74
- "ipython",
75
- "mypy",
76
- "pdbpp; sys_platform != 'win32'",
77
- "pre-commit",
78
- "ruff",
79
- "tensorstore-stubs",
80
- ]
61
+ PyQt6 = ["PyQt6 >=6.4.2"]
62
+
63
+ [dependency-groups]
81
64
  docs = [
82
65
  "mkdocs >=1.4",
83
- "mkdocs-material",
66
+ "mkdocs-material>=9.5",
84
67
  "mkdocstrings ==0.22.0",
85
68
  "mkdocs-autorefs ==1.3.1",
86
69
  "mkdocstrings-python ==1.1.2",
87
70
  "mkdocs-typer ==0.0.3",
88
- # "griffe @ git+https://github.com/tlambert03/griffe@recursion"
71
+ ]
72
+ test = [
73
+ "pymmcore-plus[io]",
74
+ "msgspec >= 0.19",
75
+ "msgpack >=1",
76
+ "pytest-cov >=5",
77
+ "pytest >=8",
78
+ "xarray >=2024.1",
79
+ "pymmcore >=11.5.0.73",
80
+ ]
81
+ test-codspeed = [{ include-group = "test" }, "pytest-codspeed >=3.2.0"]
82
+ test-qt = [{ include-group = 'test' }, "pytest-qt >=4.4", "qtpy >=2"]
83
+ PyQt6 = [{ include-group = 'test-qt' }, "pymmcore-plus[PyQt6]"]
84
+ PySide6 = [{ include-group = 'test-qt' }, "pymmcore-plus[PySide6]"]
85
+ PyQt5 = [{ include-group = 'test-qt' }, "pymmcore-plus[PyQt5]"]
86
+ PySide2 = [{ include-group = 'test-qt' }, "pymmcore-plus[PySide2]"]
87
+ dev = [
88
+ # { include-group = "docs" },
89
+ { include-group = "PyQt6" },
90
+ "ipython>=8.18.1",
91
+ "pdbpp>=0.11.6 ; sys_platform != 'win32'",
92
+ "mypy>=1.14.1",
93
+ "pre-commit>=4.1.0",
94
+ "ruff>=0.9.4",
95
+ "pydantic >2.7.4; python_version >= '3.13'",
96
+ ]
97
+
98
+ [tool.uv.sources]
99
+ pymmcore-plus = { workspace = true }
100
+
101
+ [tool.uv]
102
+ override-dependencies = [
103
+ "griffe @ git+https://github.com/tlambert03/griffe@recursion#egg=griffe",
89
104
  ]
90
105
 
91
106
  [project.urls]
@@ -115,6 +130,8 @@ sources = ["src"]
115
130
  [tool.ruff]
116
131
  line-length = 88
117
132
  target-version = "py39"
133
+ fix = true
134
+ unsafe-fixes = true
118
135
 
119
136
  [tool.ruff.lint]
120
137
  pydocstyle = { convention = "numpy" }
@@ -140,7 +157,7 @@ ignore = [
140
157
  ]
141
158
 
142
159
  [tool.ruff.lint.per-file-ignores]
143
- "tests/*.py" = ["D", "SLF"]
160
+ "tests/**/*.py" = ["D", "SLF"]
144
161
  "examples/*.py" = ["D"]
145
162
  "_cli.py" = ["B008"]
146
163
  "docs/*.py" = ["A", "D"]
@@ -153,7 +170,11 @@ docstring-code-format = true
153
170
  [tool.pytest.ini_options]
154
171
  minversion = "6.0"
155
172
  testpaths = ["tests"]
156
- filterwarnings = ["error", "ignore:Failed to disconnect::pytestqt"]
173
+ filterwarnings = [
174
+ "error",
175
+ "ignore:Failed to disconnect::pytestqt",
176
+ "ignore:numpy.core.multiarray is deprecated",
177
+ ]
157
178
  markers = ["run_last: mark a test to run last"]
158
179
 
159
180
  # https://mypy.readthedocs.io/en/stable/config_file.html
@@ -8,6 +8,7 @@ except PackageNotFoundError: # pragma: no cover
8
8
  __version__ = "unknown"
9
9
 
10
10
 
11
+ from ._accumulator import AbstractChangeAccumulator
11
12
  from ._logger import configure_logging
12
13
  from ._util import find_micromanager, use_micromanager
13
14
  from .core import (
@@ -34,6 +35,7 @@ from .core.events import CMMCoreSignaler, PCoreSignaler
34
35
  from .mda._runner import GeneratorMDASequence
35
36
 
36
37
  __all__ = [
38
+ "AbstractChangeAccumulator",
37
39
  "ActionType",
38
40
  "CFGCommand",
39
41
  "CFGGroup",
@@ -0,0 +1,258 @@
1
+ """Accumulate `setX` calls to a device value or property."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import abc
6
+ import sys
7
+ from abc import ABC, abstractmethod
8
+ from collections.abc import Sequence
9
+ from typing import TYPE_CHECKING, Any, ClassVar, Generic, Literal, TypeVar
10
+
11
+ import psygnal
12
+
13
+ from pymmcore_plus.core._constants import DeviceType
14
+ from pymmcore_plus.core._mmcore_plus import CMMCorePlus
15
+
16
+ if TYPE_CHECKING:
17
+ from typing_extensions import Self
18
+ T = TypeVar("T")
19
+ DT = TypeVar("DT", bound=DeviceType)
20
+
21
+
22
+ class AbstractChangeAccumulator(ABC, Generic[T]):
23
+ """Abstract base class for accumulating a series of `setX` calls to a device.
24
+
25
+ A `ChangeAccumulator`` is a class that accumulates a series of `setX` calls to a
26
+ device, retaining an internal target value, and emitting a signal when the device
27
+ has reached its target and is idle. It can be shared by multiple players (e.g.
28
+ widgets, or other classes) that want to control the same device, and allows them all
29
+ to issue relative/absolute moves, and be notified when the device is idle.
30
+
31
+ A common use case is to accumulate setPosition calls made to a stage device, where
32
+ you might want to accumulate a series of relative moves, and snap an image only when
33
+ the stage is idle after reaching its target position.
34
+ """
35
+
36
+ finished = psygnal.Signal()
37
+ """Signal emitted when the device has reached its target and is idle."""
38
+
39
+ def __init__(self, zero: T) -> None:
40
+ self._zero = zero
41
+ self._reset()
42
+
43
+ def _reset(self) -> None:
44
+ self._base: T | None = None
45
+ self._delta: T | None = None
46
+
47
+ # ------------------------ Public API ------------------------
48
+
49
+ def add_relative(self, delta: T) -> None:
50
+ """Add a relative value to the target."""
51
+ if self._delta is None:
52
+ self._base = self._get_value()
53
+ self._delta = delta
54
+ else:
55
+ self._delta = self._add(self._delta, delta)
56
+ self._issue_move()
57
+
58
+ def set_absolute(self, target: T) -> None:
59
+ """Assign an absolute value to the target.
60
+
61
+ This will reset the accumulated state and issue a move to the target position.
62
+ After the move finishes, new `move_relative()` calls are interpreted
63
+ relative to *target*.
64
+ """
65
+ self._base = target # anchor for later relatives
66
+ self._delta = self._zero # target == base + delta
67
+ self._issue_move()
68
+
69
+ def poll_done(self) -> bool:
70
+ """Check if the device is done moving.
71
+
72
+ This should be called repeatedly by some event loop driver.
73
+
74
+ Returns True exactly once when:
75
+ 1. The device is idle (not busy) AND
76
+ 2. The last issued move command has been completed
77
+
78
+ After returning True it resets its state and will return False until the next
79
+ move_relative() call.
80
+ """
81
+ # if we have no base or delta, we're not moving
82
+ if self._delta is None:
83
+ return False
84
+
85
+ # if the device is busy, we're not done
86
+ if self._is_busy():
87
+ return False
88
+
89
+ # no new work, we're done
90
+ self._reset()
91
+ self.finished.emit()
92
+ return True
93
+
94
+ @property
95
+ def is_moving(self) -> bool:
96
+ """Returns True if the device is moving."""
97
+ return self._delta is not None
98
+
99
+ @property
100
+ def target(self) -> T | None:
101
+ """The target position of the stage. Or None if not moving."""
102
+ if self._base is None or self._delta is None:
103
+ return None
104
+ return self._add(self._base, self._delta)
105
+
106
+ # ------------------------ Public API ------------------------
107
+
108
+ def _issue_move(self) -> None:
109
+ # self._base and self._delta are guaranteed to be not None here
110
+ target = self._add(self._base, self._delta) # type: ignore[arg-type]
111
+ # issue the move command
112
+ try:
113
+ self._set_value(target)
114
+ except Exception: # pragma: no cover
115
+ from pymmcore_plus._logger import logger
116
+
117
+ logger.exception(f"Error setting {type(self)} to {target}")
118
+
119
+ # ------------------------ Abstract methods ------------------------
120
+
121
+ @abstractmethod
122
+ def _get_value(self) -> T:
123
+ """Get the current position of the device."""
124
+
125
+ @abstractmethod
126
+ def _set_value(self, value: T) -> None:
127
+ """Set the position of the device."""
128
+
129
+ @abstractmethod
130
+ def _add(self, a: T, b: T) -> T:
131
+ """Add two values together.
132
+
133
+ Provided for more complex types like sequences.
134
+ """
135
+
136
+ @abstractmethod
137
+ def _is_busy(self) -> bool:
138
+ """Return True if the device is busy."""
139
+
140
+
141
+ class FloatChangeAccumulator(AbstractChangeAccumulator[float]):
142
+ def __init__(self) -> None:
143
+ super().__init__(zero=0.0)
144
+
145
+ def _add(self, a: float, b: float) -> float:
146
+ return a + b
147
+
148
+
149
+ ZIP_STRICT = {"strict": True} if sys.version_info >= (3, 10) else {}
150
+
151
+
152
+ class SequenceChangeAccumulator(AbstractChangeAccumulator[Sequence[float]]):
153
+ def __init__(self, sequence_length: int) -> None:
154
+ self.sequence_length = sequence_length
155
+ super().__init__(zero=[0.0] * sequence_length)
156
+
157
+ def _add(self, a: Sequence[float], b: Sequence[float]) -> Sequence[float]:
158
+ return [x + y for x, y in zip(a, b, **ZIP_STRICT)]
159
+
160
+
161
+ class DeviceAccumulator(abc.ABC, Generic[DT]):
162
+ def __init__(
163
+ self,
164
+ *,
165
+ device_label: str,
166
+ mmcore: CMMCorePlus | None = None,
167
+ **kwargs: Any,
168
+ ) -> None:
169
+ self._mmcore = mmcore or CMMCorePlus.instance()
170
+ dev_type = self._device_type()
171
+ if not self._mmcore.getDeviceType(device_label) == dev_type: # pragma: no cover
172
+ raise ValueError(
173
+ f"Cannot create {self.__class__.__name__}. "
174
+ f"Device {device_label!r} is not a {dev_type.name}. "
175
+ )
176
+
177
+ self._device_label = device_label
178
+ super().__init__(**kwargs)
179
+
180
+ def _is_busy(self) -> bool:
181
+ return self._mmcore.deviceBusy(self._device_label)
182
+
183
+ @classmethod
184
+ @abstractmethod
185
+ def _device_type(cls) -> DT:
186
+ """Return the device type for this class."""
187
+
188
+ _CACHE: ClassVar[dict[tuple[int, str], DeviceAccumulator]] = {}
189
+
190
+ @classmethod
191
+ def get_cached(cls, device: str, mmcore: CMMCorePlus | None = None) -> Self:
192
+ """Get a cached instance of the class for the given (device, core) pair.
193
+
194
+ This is intended to be called on the subclass for the device type you want to
195
+ create. For example, if you want to create a `PositionChangeAccumulator` for a
196
+ `StageDevice`, you would call: `PositionChangeAccumulator.get_cached(device)`.
197
+
198
+ But it may also be called on the base class, in which case it will still return
199
+ the correct subclass instance, but you will not have type safety on the return
200
+ type.
201
+ """
202
+ mmcore = mmcore or CMMCorePlus.instance()
203
+ cache_key = (id(mmcore), device)
204
+ device_type = mmcore.getDeviceType(device)
205
+ if cache_key not in DeviceAccumulator._CACHE:
206
+ if device_type == cls._device_type():
207
+ cls._CACHE[cache_key] = cls(device_label=device, mmcore=mmcore)
208
+ else:
209
+ for sub in cls.__subclasses__():
210
+ if sub._device_type() == device_type: # noqa: SLF001
211
+ cls._CACHE[cache_key] = sub(device_label=device, mmcore=mmcore)
212
+ break
213
+ else:
214
+ raise ValueError(
215
+ "No matching DeviceTypeMixin subclass found for device type "
216
+ f"{device_type.name} (for device {device!r})."
217
+ )
218
+ obj = cls._CACHE[cache_key]
219
+ if not isinstance(obj, cls):
220
+ raise TypeError(
221
+ f"Cannot create {cls.__name__} for {device!r}. "
222
+ f"Device is a {device_type!r}, not a {cls._device_type().name}. "
223
+ )
224
+ return obj
225
+
226
+
227
+ class PositionChangeAccumulator(DeviceAccumulator, FloatChangeAccumulator):
228
+ """Accumulator for single axis stage devices."""
229
+
230
+ def __init__(self, device_label: str, mmcore: CMMCorePlus | None = None) -> None:
231
+ super().__init__(device_label=device_label, mmcore=mmcore)
232
+
233
+ @classmethod
234
+ def _device_type(cls) -> Literal[DeviceType.StageDevice]:
235
+ return DeviceType.StageDevice
236
+
237
+ def _get_value(self) -> float:
238
+ return self._mmcore.getPosition(self._device_label)
239
+
240
+ def _set_value(self, value: float) -> None:
241
+ self._mmcore.setPosition(self._device_label, value)
242
+
243
+
244
+ class XYPositionChangeAccumulator(DeviceAccumulator, SequenceChangeAccumulator):
245
+ """Accumulator for XY stage devices."""
246
+
247
+ def __init__(self, device_label: str, mmcore: CMMCorePlus | None = None) -> None:
248
+ super().__init__(device_label=device_label, mmcore=mmcore, sequence_length=2)
249
+
250
+ @classmethod
251
+ def _device_type(cls) -> Literal[DeviceType.XYStageDevice]:
252
+ return DeviceType.XYStageDevice
253
+
254
+ def _get_value(self) -> Sequence[float]:
255
+ return self._mmcore.getXYPosition(self._device_label)
256
+
257
+ def _set_value(self, value: Sequence[float]) -> None:
258
+ self._mmcore.setXYPosition(self._device_label, *value)
@@ -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))
@@ -1,10 +1,14 @@
1
1
  __all__ = [
2
2
  "ActionType",
3
+ "AutoFocusDevice",
3
4
  "CFGCommand",
4
5
  "CFGGroup",
5
6
  "CMMCorePlus",
7
+ "CameraDevice",
6
8
  "ConfigGroup",
7
9
  "Configuration",
10
+ "CoreDevice",
11
+ "Device",
8
12
  "Device",
9
13
  "DeviceAdapter",
10
14
  "DeviceDetectionStatus",
@@ -13,12 +17,24 @@ __all__ = [
13
17
  "DeviceProperty",
14
18
  "DeviceType",
15
19
  "FocusDirection",
20
+ "GalvoDevice",
21
+ "GenericDevice",
22
+ "HubDevice",
23
+ "ImageProcessorDevice",
16
24
  "Keyword",
25
+ "MagnifierDevice",
17
26
  "Metadata",
18
27
  "PixelFormat",
19
28
  "PortType",
20
29
  "PropertyType",
30
+ "SLMDevice",
21
31
  "SequencedEvent",
32
+ "SerialDevice",
33
+ "ShutterDevice",
34
+ "SignalIODevice",
35
+ "StageDevice",
36
+ "StateDevice",
37
+ "XYStageDevice",
22
38
  "iter_sequenced_events",
23
39
  ]
24
40
 
@@ -39,7 +55,24 @@ from ._constants import (
39
55
  PortType,
40
56
  PropertyType,
41
57
  )
42
- from ._device import Device
58
+ from ._device import (
59
+ AutoFocusDevice,
60
+ CameraDevice,
61
+ CoreDevice,
62
+ Device,
63
+ GalvoDevice,
64
+ GenericDevice,
65
+ HubDevice,
66
+ ImageProcessorDevice,
67
+ MagnifierDevice,
68
+ SerialDevice,
69
+ ShutterDevice,
70
+ SignalIODevice,
71
+ SLMDevice,
72
+ StageDevice,
73
+ StateDevice,
74
+ XYStageDevice,
75
+ )
43
76
  from ._metadata import Metadata
44
77
  from ._mmcore_plus import CMMCorePlus
45
78
  from ._property import DeviceProperty
@@ -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, "