pymmcore-plus 0.13.7__tar.gz → 0.14.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 (99) hide show
  1. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/PKG-INFO +14 -37
  2. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/pyproject.toml +57 -35
  3. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/__init__.py +2 -0
  4. pymmcore_plus-0.14.0/src/pymmcore_plus/_accumulator.py +258 -0
  5. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/core/__init__.py +34 -1
  6. pymmcore_plus-0.14.0/src/pymmcore_plus/core/_device.py +937 -0
  7. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/core/_mmcore_plus.py +165 -8
  8. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/mda/handlers/_tensorstore_handler.py +3 -1
  9. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/tests/io/test_zarr_writers.py +6 -0
  10. pymmcore_plus-0.14.0/tests/test_accumulators.py +107 -0
  11. pymmcore_plus-0.14.0/tests/test_device_class.py +234 -0
  12. pymmcore_plus-0.13.7/src/pymmcore_plus/core/_device.py +0 -217
  13. pymmcore_plus-0.13.7/tests/test_device_class.py +0 -73
  14. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/.gitignore +0 -0
  15. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/LICENSE +0 -0
  16. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/README.md +0 -0
  17. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/_benchmark.py +0 -0
  18. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/_build.py +0 -0
  19. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/_cli.py +0 -0
  20. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/_logger.py +0 -0
  21. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/_pymmcore.py +0 -0
  22. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/_util.py +0 -0
  23. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/core/_adapter.py +0 -0
  24. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/core/_config.py +0 -0
  25. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/core/_config_group.py +0 -0
  26. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/core/_constants.py +0 -0
  27. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/core/_metadata.py +0 -0
  28. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/core/_property.py +0 -0
  29. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/core/_sequencing.py +0 -0
  30. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/core/events/__init__.py +0 -0
  31. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/core/events/_device_signal_view.py +0 -0
  32. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/core/events/_norm_slot.py +0 -0
  33. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/core/events/_prop_event_mixin.py +0 -0
  34. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/core/events/_protocol.py +0 -0
  35. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/core/events/_psygnal.py +0 -0
  36. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/core/events/_qsignals.py +0 -0
  37. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/experimental/__init__.py +0 -0
  38. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/experimental/unicore/__init__.py +0 -0
  39. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/experimental/unicore/_device_manager.py +0 -0
  40. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/experimental/unicore/_proxy.py +0 -0
  41. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/experimental/unicore/_unicore.py +0 -0
  42. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/experimental/unicore/devices/__init__.py +0 -0
  43. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/experimental/unicore/devices/_device.py +0 -0
  44. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/experimental/unicore/devices/_properties.py +0 -0
  45. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/experimental/unicore/devices/_stage.py +0 -0
  46. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/install.py +0 -0
  47. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/mda/__init__.py +0 -0
  48. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/mda/_engine.py +0 -0
  49. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/mda/_protocol.py +0 -0
  50. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/mda/_runner.py +0 -0
  51. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/mda/_thread_relay.py +0 -0
  52. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/mda/events/__init__.py +0 -0
  53. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/mda/events/_protocol.py +0 -0
  54. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/mda/events/_psygnal.py +0 -0
  55. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/mda/events/_qsignals.py +0 -0
  56. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/mda/handlers/_5d_writer_base.py +0 -0
  57. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/mda/handlers/__init__.py +0 -0
  58. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/mda/handlers/_img_sequence_writer.py +0 -0
  59. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/mda/handlers/_ome_tiff_writer.py +0 -0
  60. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/mda/handlers/_ome_zarr_writer.py +0 -0
  61. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/mda/handlers/_util.py +0 -0
  62. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/metadata/__init__.py +0 -0
  63. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/metadata/functions.py +0 -0
  64. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/metadata/schema.py +0 -0
  65. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/metadata/serialize.py +0 -0
  66. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/mocks.py +0 -0
  67. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/model/__init__.py +0 -0
  68. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/model/_config_file.py +0 -0
  69. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/model/_config_group.py +0 -0
  70. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/model/_core_device.py +0 -0
  71. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/model/_core_link.py +0 -0
  72. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/model/_device.py +0 -0
  73. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/model/_microscope.py +0 -0
  74. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/model/_pixel_size_config.py +0 -0
  75. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/model/_property.py +0 -0
  76. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/py.typed +0 -0
  77. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/src/pymmcore_plus/seq_tester.py +0 -0
  78. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/tests/__init__.py +0 -0
  79. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/tests/conftest.py +0 -0
  80. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/tests/io/test_image_sequence_writer.py +0 -0
  81. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/tests/io/test_ome_tiff.py +0 -0
  82. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/tests/local_config.cfg +0 -0
  83. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/tests/test_adapter_class.py +0 -0
  84. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/tests/test_bench.py +0 -0
  85. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/tests/test_cli.py +0 -0
  86. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/tests/test_config_group_class.py +0 -0
  87. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/tests/test_core.py +0 -0
  88. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/tests/test_events.py +0 -0
  89. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/tests/test_mda.py +0 -0
  90. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/tests/test_metadata.py +0 -0
  91. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/tests/test_misc.py +0 -0
  92. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/tests/test_model.py +0 -0
  93. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/tests/test_pixel_config_class.py +0 -0
  94. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/tests/test_property_class.py +0 -0
  95. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/tests/test_sequencing.py +0 -0
  96. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/tests/test_slm_image.py +0 -0
  97. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/tests/test_thread_relay.py +0 -0
  98. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.0}/tests/unicore/test_unicore.py +0 -0
  99. {pymmcore_plus-0.13.7 → pymmcore_plus-0.14.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.14.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,54 @@ 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 = ["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 = "test" },
90
+ "PyQt6 >=6.4.2",
91
+ "ipython>=8.18.1",
92
+ "pdbpp>=0.11.6 ; sys_platform != 'win32'",
93
+ "mypy>=1.14.1",
94
+ "pre-commit>=4.1.0",
95
+ "ruff>=0.9.4",
96
+ "pydantic >2.7.4; python_version >= '3.13'",
97
+ ]
98
+
99
+ [tool.uv.sources]
100
+ pymmcore-plus = { workspace = true }
101
+
102
+ [tool.uv]
103
+ override-dependencies = [
104
+ "griffe @ git+https://github.com/tlambert03/griffe@recursion#egg=griffe",
89
105
  ]
90
106
 
91
107
  [project.urls]
@@ -115,6 +131,8 @@ sources = ["src"]
115
131
  [tool.ruff]
116
132
  line-length = 88
117
133
  target-version = "py39"
134
+ fix = true
135
+ unsafe-fixes = true
118
136
 
119
137
  [tool.ruff.lint]
120
138
  pydocstyle = { convention = "numpy" }
@@ -153,7 +171,11 @@ docstring-code-format = true
153
171
  [tool.pytest.ini_options]
154
172
  minversion = "6.0"
155
173
  testpaths = ["tests"]
156
- filterwarnings = ["error", "ignore:Failed to disconnect::pytestqt"]
174
+ filterwarnings = [
175
+ "error",
176
+ "ignore:Failed to disconnect::pytestqt",
177
+ "ignore:numpy.core.multiarray is deprecated",
178
+ ]
157
179
  markers = ["run_last: mark a test to run last"]
158
180
 
159
181
  # 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)
@@ -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