pymmcore-plus 0.14.0__tar.gz → 0.15.2__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 (116) hide show
  1. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/.gitignore +1 -0
  2. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/PKG-INFO +2 -2
  3. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/pyproject.toml +5 -7
  4. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/__init__.py +25 -0
  5. pymmcore_plus-0.15.2/src/pymmcore_plus/_ipy_completion.py +363 -0
  6. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/_pymmcore.py +4 -2
  7. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/core/_constants.py +25 -3
  8. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/core/_mmcore_plus.py +110 -55
  9. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/core/_sequencing.py +1 -1
  10. pymmcore_plus-0.15.2/src/pymmcore_plus/core/events/_deprecated.py +67 -0
  11. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/core/events/_protocol.py +64 -39
  12. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/core/events/_psygnal.py +35 -6
  13. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/core/events/_qsignals.py +34 -6
  14. pymmcore_plus-0.15.2/src/pymmcore_plus/experimental/unicore/__init__.py +24 -0
  15. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/experimental/unicore/_device_manager.py +1 -1
  16. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/experimental/unicore/_proxy.py +20 -3
  17. pymmcore_plus-0.15.2/src/pymmcore_plus/experimental/unicore/core/_sequence_buffer.py +314 -0
  18. pymmcore_plus-0.15.2/src/pymmcore_plus/experimental/unicore/core/_unicore.py +1769 -0
  19. pymmcore_plus-0.15.2/src/pymmcore_plus/experimental/unicore/devices/_camera.py +201 -0
  20. pymmcore_plus-0.14.0/src/pymmcore_plus/experimental/unicore/devices/_device.py → pymmcore_plus-0.15.2/src/pymmcore_plus/experimental/unicore/devices/_device_base.py +54 -28
  21. pymmcore_plus-0.15.2/src/pymmcore_plus/experimental/unicore/devices/_generic_device.py +12 -0
  22. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/experimental/unicore/devices/_properties.py +9 -2
  23. pymmcore_plus-0.15.2/src/pymmcore_plus/experimental/unicore/devices/_shutter.py +30 -0
  24. pymmcore_plus-0.15.2/src/pymmcore_plus/experimental/unicore/devices/_slm.py +82 -0
  25. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/experimental/unicore/devices/_stage.py +1 -1
  26. pymmcore_plus-0.15.2/src/pymmcore_plus/experimental/unicore/devices/_state.py +152 -0
  27. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/mda/events/_protocol.py +8 -8
  28. pymmcore_plus-0.15.2/src/pymmcore_plus/py.typed +0 -0
  29. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/tests/conftest.py +11 -8
  30. pymmcore_plus-0.15.2/tests/test_bench.py +193 -0
  31. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/tests/test_cli.py +4 -2
  32. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/tests/test_events.py +90 -8
  33. pymmcore_plus-0.15.2/tests/test_ipy_completions.py +164 -0
  34. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/tests/test_sequencing.py +8 -1
  35. pymmcore_plus-0.15.2/tests/unicore/conftest.py +13 -0
  36. pymmcore_plus-0.15.2/tests/unicore/test_camera.py +275 -0
  37. pymmcore_plus-0.15.2/tests/unicore/test_sequence_buffer.py +577 -0
  38. pymmcore_plus-0.15.2/tests/unicore/test_shutter.py +56 -0
  39. pymmcore_plus-0.15.2/tests/unicore/test_slm.py +337 -0
  40. pymmcore_plus-0.15.2/tests/unicore/test_state.py +362 -0
  41. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/tests/unicore/test_unicore.py +4 -4
  42. pymmcore_plus-0.14.0/src/pymmcore_plus/experimental/unicore/__init__.py +0 -14
  43. pymmcore_plus-0.14.0/src/pymmcore_plus/experimental/unicore/_unicore.py +0 -703
  44. pymmcore_plus-0.14.0/tests/test_bench.py +0 -89
  45. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/LICENSE +0 -0
  46. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/README.md +0 -0
  47. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/_accumulator.py +0 -0
  48. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/_benchmark.py +0 -0
  49. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/_build.py +0 -0
  50. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/_cli.py +0 -0
  51. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/_logger.py +0 -0
  52. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/_util.py +0 -0
  53. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/core/__init__.py +0 -0
  54. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/core/_adapter.py +0 -0
  55. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/core/_config.py +0 -0
  56. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/core/_config_group.py +0 -0
  57. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/core/_device.py +0 -0
  58. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/core/_metadata.py +0 -0
  59. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/core/_property.py +0 -0
  60. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/core/events/__init__.py +0 -0
  61. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/core/events/_device_signal_view.py +0 -0
  62. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/core/events/_norm_slot.py +0 -0
  63. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/core/events/_prop_event_mixin.py +0 -0
  64. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/experimental/__init__.py +0 -0
  65. {pymmcore_plus-0.14.0/src/pymmcore_plus/experimental/unicore/devices → pymmcore_plus-0.15.2/src/pymmcore_plus/experimental/unicore/core}/__init__.py +0 -0
  66. /pymmcore_plus-0.14.0/src/pymmcore_plus/py.typed → /pymmcore_plus-0.15.2/src/pymmcore_plus/experimental/unicore/devices/__init__.py +0 -0
  67. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/install.py +0 -0
  68. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/mda/__init__.py +0 -0
  69. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/mda/_engine.py +0 -0
  70. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/mda/_protocol.py +0 -0
  71. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/mda/_runner.py +0 -0
  72. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/mda/_thread_relay.py +0 -0
  73. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/mda/events/__init__.py +0 -0
  74. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/mda/events/_psygnal.py +0 -0
  75. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/mda/events/_qsignals.py +0 -0
  76. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/mda/handlers/_5d_writer_base.py +0 -0
  77. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/mda/handlers/__init__.py +0 -0
  78. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/mda/handlers/_img_sequence_writer.py +0 -0
  79. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/mda/handlers/_ome_tiff_writer.py +0 -0
  80. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/mda/handlers/_ome_zarr_writer.py +0 -0
  81. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/mda/handlers/_tensorstore_handler.py +0 -0
  82. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/mda/handlers/_util.py +0 -0
  83. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/metadata/__init__.py +0 -0
  84. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/metadata/functions.py +0 -0
  85. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/metadata/schema.py +0 -0
  86. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/metadata/serialize.py +0 -0
  87. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/mocks.py +0 -0
  88. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/model/__init__.py +0 -0
  89. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/model/_config_file.py +0 -0
  90. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/model/_config_group.py +0 -0
  91. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/model/_core_device.py +0 -0
  92. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/model/_core_link.py +0 -0
  93. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/model/_device.py +0 -0
  94. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/model/_microscope.py +0 -0
  95. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/model/_pixel_size_config.py +0 -0
  96. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/model/_property.py +0 -0
  97. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/seq_tester.py +0 -0
  98. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/tests/__init__.py +0 -0
  99. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/tests/io/test_image_sequence_writer.py +0 -0
  100. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/tests/io/test_ome_tiff.py +0 -0
  101. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/tests/io/test_zarr_writers.py +0 -0
  102. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/tests/local_config.cfg +0 -0
  103. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/tests/test_accumulators.py +0 -0
  104. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/tests/test_adapter_class.py +0 -0
  105. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/tests/test_config_group_class.py +0 -0
  106. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/tests/test_core.py +0 -0
  107. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/tests/test_device_class.py +0 -0
  108. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/tests/test_mda.py +0 -0
  109. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/tests/test_metadata.py +0 -0
  110. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/tests/test_misc.py +0 -0
  111. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/tests/test_model.py +0 -0
  112. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/tests/test_pixel_config_class.py +0 -0
  113. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/tests/test_property_class.py +0 -0
  114. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/tests/test_slm_image.py +0 -0
  115. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/tests/test_thread_relay.py +0 -0
  116. {pymmcore_plus-0.14.0 → pymmcore_plus-0.15.2}/tests/unicore/test_xy_stage.py +0 -0
@@ -147,3 +147,4 @@ docs/_includes/_cmmcoreplus_members.md
147
147
 
148
148
  example_*.some.tiff
149
149
  example.zarr
150
+ uv.lock
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pymmcore-plus
3
- Version: 0.14.0
3
+ Version: 0.15.2
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
@@ -30,7 +30,7 @@ Requires-Dist: numpy>=1.26.0; python_version >= '3.12'
30
30
  Requires-Dist: numpy>=2.1.0; python_version >= '3.13'
31
31
  Requires-Dist: platformdirs>=3.0.0
32
32
  Requires-Dist: psygnal>=0.10
33
- Requires-Dist: pymmcore>=11.2.1.71.0
33
+ Requires-Dist: pymmcore>=11.9.0.73.0
34
34
  Requires-Dist: rich>=10.2.0
35
35
  Requires-Dist: tensorstore!=0.1.72,>=0.1.67
36
36
  Requires-Dist: tensorstore!=0.1.72,>=0.1.71; python_version >= '3.13'
@@ -40,7 +40,7 @@ dependencies = [
40
40
  "numpy >=1.26.0; python_version >= '3.12'",
41
41
  "numpy >=1.25.2",
42
42
  "psygnal >=0.10",
43
- "pymmcore >=11.2.1.71.0",
43
+ "pymmcore >=11.9.0.73.0",
44
44
  "typing-extensions >=4", # not actually required at runtime
45
45
  "useq-schema >=0.7.2",
46
46
  "tensorstore >=0.1.71,!=0.1.72; python_version >= '3.13'",
@@ -74,21 +74,19 @@ test = [
74
74
  "msgspec >= 0.19",
75
75
  "msgpack >=1",
76
76
  "pytest-cov >=5",
77
+ "ipython>=8.18.0",
77
78
  "pytest >=8",
78
79
  "xarray >=2024.1",
79
- "pymmcore >=11.5.0.73",
80
80
  ]
81
81
  test-codspeed = [{ include-group = "test" }, "pytest-codspeed >=3.2.0"]
82
- test-qt = ["pytest-qt >=4.4", "qtpy >=2"]
82
+ test-qt = [{ include-group = 'test' }, "pytest-qt ==4.4", "qtpy >=2"]
83
83
  PyQt6 = [{ include-group = 'test-qt' }, "pymmcore-plus[PyQt6]"]
84
84
  PySide6 = [{ include-group = 'test-qt' }, "pymmcore-plus[PySide6]"]
85
85
  PyQt5 = [{ include-group = 'test-qt' }, "pymmcore-plus[PyQt5]"]
86
86
  PySide2 = [{ include-group = 'test-qt' }, "pymmcore-plus[PySide2]"]
87
87
  dev = [
88
88
  # { include-group = "docs" },
89
- { include-group = "test" },
90
- "PyQt6 >=6.4.2",
91
- "ipython>=8.18.1",
89
+ { include-group = "PyQt6" },
92
90
  "pdbpp>=0.11.6 ; sys_platform != 'win32'",
93
91
  "mypy>=1.14.1",
94
92
  "pre-commit>=4.1.0",
@@ -158,7 +156,7 @@ ignore = [
158
156
  ]
159
157
 
160
158
  [tool.ruff.lint.per-file-ignores]
161
- "tests/*.py" = ["D", "SLF"]
159
+ "tests/**/*.py" = ["D", "SLF"]
162
160
  "examples/*.py" = ["D"]
163
161
  "_cli.py" = ["B008"]
164
162
  "docs/*.py" = ["A", "D"]
@@ -1,5 +1,6 @@
1
1
  """pymmcore superset providing improved APIs, event handling, and a pure python acquisition engine.""" # noqa: E501
2
2
 
3
+ import logging
3
4
  from importlib.metadata import PackageNotFoundError, version
4
5
 
5
6
  try:
@@ -12,6 +13,7 @@ from ._accumulator import AbstractChangeAccumulator
12
13
  from ._logger import configure_logging
13
14
  from ._util import find_micromanager, use_micromanager
14
15
  from .core import (
16
+ ActionType,
15
17
  CFGCommand,
16
18
  CFGGroup,
17
19
  CMMCorePlus,
@@ -63,3 +65,26 @@ __all__ = [
63
65
  "find_micromanager",
64
66
  "use_micromanager",
65
67
  ]
68
+
69
+
70
+ def _install_ipy_completer() -> None: # pragma: no cover
71
+ import os
72
+ import sys
73
+
74
+ if os.getenv("PYMM_DISABLE_IPYTHON_COMPLETIONS", "0") == "1":
75
+ return
76
+ try:
77
+ if (IPython := sys.modules.get("IPython")) and (shell := IPython.get_ipython()):
78
+ from ._ipy_completion import install_pymmcore_ipy_completion
79
+
80
+ install_pymmcore_ipy_completion(shell)
81
+ except Exception as e:
82
+ # If we fail to install the completer, we don't want to crash the import.
83
+ # This is a best-effort installation.
84
+ logging.warning(
85
+ f"Failed to install pymmcore-plus IPython completer:\n {e}",
86
+ )
87
+
88
+
89
+ _install_ipy_completer()
90
+ del _install_ipy_completer
@@ -0,0 +1,363 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from typing import TYPE_CHECKING, Callable
5
+
6
+ from IPython import get_ipython # pyright: ignore
7
+ from IPython.core.completer import SimpleCompletion, context_matcher
8
+
9
+ from pymmcore_plus import CMMCorePlus, DeviceType
10
+
11
+ if TYPE_CHECKING:
12
+ from collections.abc import Sequence
13
+
14
+ from IPython.core.completer import (
15
+ CompletionContext,
16
+ SimpleMatcherResult,
17
+ )
18
+ from IPython.core.interactiveshell import InteractiveShell
19
+
20
+ # functions that only require the CMMCorePlus instance to return completions
21
+ CoreCompleter = Callable[[CMMCorePlus], Sequence[SimpleCompletion]]
22
+ # functions that require the CMMCorePlus instance and a label to return completions
23
+ CoreLabelCompleter = Callable[[CMMCorePlus, str], Sequence[SimpleCompletion]]
24
+
25
+
26
+ def _get_argument_index(src: str, ns: dict[str, object]) -> int:
27
+ """Parse argument index from method call string.
28
+
29
+ Uses a simple comma-counting approach that works reliably across
30
+ different backends (pymmcore, pymmcore-nano, etc.) without relying
31
+ on jedi's ability to understand method signatures.
32
+ """
33
+ p0 = src.rfind("(") + 1
34
+ p1 = src.rfind(")") if ")" in src else None
35
+ if inner := src[slice(p0, p1)].strip():
36
+ # split on commas that are not inside quotes
37
+ return len(inner.split(",")) - 1 # 0-based
38
+ return 0
39
+
40
+
41
+ # matches "obj.attr(" ... note trailing paren
42
+ OBJ_METHOD_RE = re.compile(r"(?P<obj>[A-Za-z_][\w\.]*)\s*\.\s*(?P<attr>\w+)\s*\(")
43
+
44
+
45
+ def _null() -> SimpleMatcherResult:
46
+ return {"completions": [], "matched_fragment": "", "suppress": False}
47
+
48
+
49
+ def _dev_labels(
50
+ core: CMMCorePlus, *dev_types: DeviceType, with_null: bool = False
51
+ ) -> Sequence[SimpleCompletion]:
52
+ try:
53
+ if not dev_types:
54
+ labels = list(core.getLoadedDevices())
55
+ else:
56
+ labels = [
57
+ d
58
+ for dev_type in dev_types
59
+ for d in core.getLoadedDevicesOfType(dev_type)
60
+ ]
61
+ completions = [SimpleCompletion(f'"{lbl}"') for lbl in labels]
62
+ if with_null:
63
+ completions.append(SimpleCompletion("''"))
64
+ return completions
65
+ except Exception: # pragma: no cover
66
+ return []
67
+
68
+
69
+ def _config_group_names(core: CMMCorePlus) -> Sequence[SimpleCompletion]:
70
+ """Get the names of all configuration groups."""
71
+ try:
72
+ return [
73
+ SimpleCompletion(f'"{name}"') for name in core.getAvailableConfigGroups()
74
+ ]
75
+ except Exception: # pragma: no cover
76
+ return []
77
+
78
+
79
+ def _config_preset_names(core: CMMCorePlus, group: str) -> Sequence[SimpleCompletion]:
80
+ """Get the names of all configuration presets for a given group."""
81
+ try:
82
+ return [
83
+ SimpleCompletion(f'"{name}"') for name in core.getAvailableConfigs(group)
84
+ ]
85
+ except Exception: # pragma: no cover
86
+ return []
87
+
88
+
89
+ # fmt: off
90
+ # map of (method_name, arg_index) -> function that returns possible completions
91
+ CORE_COMPLETERS: dict[tuple[str, int], CoreCompleter] = {
92
+ # ROLES -----------------------------------------------------------------------
93
+ ("setAutoFocusDevice", 0): lambda core: _dev_labels(core, DeviceType.AutoFocus, with_null=True), # noqa
94
+ ("setCameraDevice", 0): lambda core: _dev_labels(core, DeviceType.Camera, with_null=True), # noqa
95
+ ("setFocusDevice", 0): lambda core: _dev_labels(core, DeviceType.Stage, with_null=True), # noqa
96
+ ("setGalvoDevice", 0): lambda core: _dev_labels(core, DeviceType.Galvo, with_null=True), # noqa
97
+ ("setImageProcessorDevice", 0): lambda core: _dev_labels(core, DeviceType.ImageProcessor, with_null=True), # noqa
98
+ ("setShutterDevice", 0): lambda core: _dev_labels(core, DeviceType.Shutter, with_null=True), # noqa
99
+ ("setSLMDevice", 0): lambda core: _dev_labels(core, DeviceType.SLM, with_null=True),
100
+ ("setXYStageDevice", 0): lambda core: _dev_labels(core, DeviceType.XYStage, with_null=True), # noqa
101
+ # -----------------------
102
+ ("getFocusDirection", 0): lambda core: _dev_labels(core, DeviceType.Stage),
103
+ ("getPosition", 0): lambda core: _dev_labels(core, DeviceType.Stage),
104
+ ("getStageSequenceMaxLength", 0): lambda core: _dev_labels(core, DeviceType.Stage),
105
+ ("home", 0): lambda core: _dev_labels(core, DeviceType.Stage, DeviceType.XYStage),
106
+ ("isContinuousFocusDrive", 0): lambda core: _dev_labels(core, DeviceType.Stage),
107
+ ("isStageLinearSequenceable", 0): lambda core: _dev_labels(core, DeviceType.Stage),
108
+ ("isStageSequenceable", 0): lambda core: _dev_labels(core, DeviceType.Stage),
109
+ ("loadStageSequence", 0): lambda core: _dev_labels(core, DeviceType.Stage),
110
+ ("setAdapterOrigin", 0): lambda core: _dev_labels(core, DeviceType.Stage),
111
+ ("setFocusDirection", 0): lambda core: _dev_labels(core, DeviceType.Stage),
112
+ ("setOrigin", 0): lambda core: _dev_labels(core, DeviceType.Stage),
113
+ ("setPosition", 0): lambda core: _dev_labels(core, DeviceType.Stage),
114
+ ("setRelativePosition", 0): lambda core: _dev_labels(core, DeviceType.Stage),
115
+ ("setStageLinearSequence", 0): lambda core: _dev_labels(core, DeviceType.Stage),
116
+ ("startStageSequence", 0): lambda core: _dev_labels(core, DeviceType.Stage),
117
+ ("stop", 0): lambda core: _dev_labels(core, DeviceType.Stage, DeviceType.XYStage),
118
+ ("stopStageSequence", 0): lambda core: _dev_labels(core, DeviceType.Stage),
119
+ # --------------------
120
+ ("getXPosition", 0): lambda core: _dev_labels(core, DeviceType.XYStage),
121
+ ("getXYPosition", 0): lambda core: _dev_labels(core, DeviceType.XYStage),
122
+ ("getXYStageSequenceMaxLength", 0): lambda core: _dev_labels(core, DeviceType.XYStage), # noqa
123
+ ("getYPosition", 0): lambda core: _dev_labels(core, DeviceType.XYStage),
124
+ ("isXYStageSequenceable", 0): lambda core: _dev_labels(core, DeviceType.XYStage),
125
+ ("loadXYStageSequence", 0): lambda core: _dev_labels(core, DeviceType.XYStage),
126
+ ("setAdapterOriginXY", 0): lambda core: _dev_labels(core, DeviceType.XYStage),
127
+ ("setOriginX", 0): lambda core: _dev_labels(core, DeviceType.XYStage),
128
+ ("setOriginXY", 0): lambda core: _dev_labels(core, DeviceType.XYStage),
129
+ ("setOriginY", 0): lambda core: _dev_labels(core, DeviceType.XYStage),
130
+ ("setRelativeXYPosition", 0): lambda core: _dev_labels(core, DeviceType.XYStage),
131
+ ("setXYPosition", 0): lambda core: _dev_labels(core, DeviceType.XYStage),
132
+ ("startXYStageSequence", 0): lambda core: _dev_labels(core, DeviceType.XYStage),
133
+ ("stopXYStageSequence", 0): lambda core: _dev_labels(core, DeviceType.XYStage),
134
+ # --------------------
135
+ ("deleteConfig", 2): _dev_labels,
136
+ ("detectDevice", 0): _dev_labels,
137
+ ("deviceBusy", 0): _dev_labels,
138
+ ("getAllowedPropertyValues", 0): _dev_labels,
139
+ ("getDeviceDelayMs", 0): _dev_labels,
140
+ ("getDeviceDescription", 0): _dev_labels,
141
+ ("getDeviceInitializationState", 0): _dev_labels,
142
+ ("getDeviceLibrary", 0): _dev_labels,
143
+ ("getDeviceName", 0): _dev_labels,
144
+ ("getDevicePropertyNames", 0): _dev_labels,
145
+ ("getDeviceType", 0): _dev_labels,
146
+ ("getParentLabel", 0): _dev_labels,
147
+ ("getProperty", 0): _dev_labels,
148
+ ("getPropertyFromCache", 0): _dev_labels,
149
+ ("getPropertyLowerLimit", 0): _dev_labels, # ?
150
+ ("getPropertySequenceMaxLength", 0): _dev_labels, # ?
151
+ ("getPropertyType", 0): _dev_labels,
152
+ ("getPropertyUpperLimit", 0): _dev_labels, # ?
153
+ ("hasProperty", 0): _dev_labels,
154
+ ("hasPropertyLimits", 0): _dev_labels,
155
+ ("initializeDevice", 0): _dev_labels,
156
+ ("isPropertyPreInit", 0): _dev_labels,
157
+ ("isPropertyReadOnly", 0): _dev_labels,
158
+ ("isPropertySequenceable", 0): _dev_labels,
159
+ ("loadPropertySequence", 0): _dev_labels, # ?
160
+ ("setDeviceDelayMs", 0): _dev_labels,
161
+ ("setParentLabel", 0): _dev_labels,
162
+ ("setProperty", 0): _dev_labels,
163
+ ("startPropertySequence", 0): _dev_labels, # ?
164
+ ("unloadDevice", 0): _dev_labels,
165
+ ("usesDeviceDelay", 0): _dev_labels,
166
+ ("waitForDevice", 0): _dev_labels,
167
+ # --------------------
168
+ ("displaySLMImage", 0): lambda core: _dev_labels(core, DeviceType.SLM),
169
+ ("getSLMBytesPerPixel", 0): lambda core: _dev_labels(core, DeviceType.SLM),
170
+ ("getSLMExposure", 0): lambda core: _dev_labels(core, DeviceType.SLM),
171
+ ("getSLMHeight", 0): lambda core: _dev_labels(core, DeviceType.SLM),
172
+ ("getSLMNumberOfComponents", 0): lambda core: _dev_labels(core, DeviceType.SLM),
173
+ ("getSLMSequenceMaxLength", 0): lambda core: _dev_labels(core, DeviceType.SLM),
174
+ ("getSLMWidth", 0): lambda core: _dev_labels(core, DeviceType.SLM),
175
+ ("loadSLMSequence", 0): lambda core: _dev_labels(core, DeviceType.SLM),
176
+ ("setSLMExposure", 0): lambda core: _dev_labels(core, DeviceType.SLM),
177
+ ("setSLMImage", 0): lambda core: _dev_labels(core, DeviceType.SLM),
178
+ ("setSLMPixelsTo", 0): lambda core: _dev_labels(core, DeviceType.SLM),
179
+ ("startSLMSequence", 0): lambda core: _dev_labels(core, DeviceType.SLM),
180
+ ("stopSLMSequence", 0): lambda core: _dev_labels(core, DeviceType.SLM),
181
+ # --------------------
182
+ ("deleteGalvoPolygons", 0): lambda core: _dev_labels(core, DeviceType.Galvo),
183
+ ("getGalvoChannels", 0): lambda core: _dev_labels(core, DeviceType.Galvo),
184
+ ("getGalvoPosition", 0): lambda core: _dev_labels(core, DeviceType.Galvo),
185
+ ("getGalvoXMinimum", 0): lambda core: _dev_labels(core, DeviceType.Galvo),
186
+ ("getGalvoXRange", 0): lambda core: _dev_labels(core, DeviceType.Galvo),
187
+ ("getGalvoYMinimum", 0): lambda core: _dev_labels(core, DeviceType.Galvo),
188
+ ("getGalvoYRange", 0): lambda core: _dev_labels(core, DeviceType.Galvo),
189
+ ("loadGalvoPolygons", 0): lambda core: _dev_labels(core, DeviceType.Galvo),
190
+ ("pointGalvoAndFire", 0): lambda core: _dev_labels(core, DeviceType.Galvo),
191
+ ("runGalvoPolygons", 0): lambda core: _dev_labels(core, DeviceType.Galvo),
192
+ ("runGalvoSequence", 0): lambda core: _dev_labels(core, DeviceType.Galvo),
193
+ ("setGalvoIlluminationState", 0): lambda core: _dev_labels(core, DeviceType.Galvo),
194
+ ("setGalvoPolygonRepetitions", 0): lambda core: _dev_labels(core, DeviceType.Galvo),
195
+ ("setGalvoPosition", 0): lambda core: _dev_labels(core, DeviceType.Galvo),
196
+ ("setGalvoSpotInterval", 0): lambda core: _dev_labels(core, DeviceType.Galvo),
197
+ # --------------------
198
+ ("getExposure", 0): lambda core: _dev_labels(core, DeviceType.Camera),
199
+ ("getExposureSequenceMaxLength", 0): lambda core: _dev_labels(core, DeviceType.Camera), # noqa
200
+ ("getROI", 0): lambda core: _dev_labels(core, DeviceType.Camera),
201
+ ("isExposureSequenceable", 0): lambda core: _dev_labels(core, DeviceType.Camera),
202
+ ("isSequenceRunning", 0): lambda core: _dev_labels(core, DeviceType.Camera),
203
+ ("loadExposureSequence", 0): lambda core: _dev_labels(core, DeviceType.Camera),
204
+ ("prepareSequenceAcquisition", 0): lambda core: _dev_labels(core, DeviceType.Camera), # noqa
205
+ ("setExposure", 0): lambda core: _dev_labels(core, DeviceType.Camera),
206
+ ("setROI", 0): lambda core: _dev_labels(core, DeviceType.Camera),
207
+ ("startExposureSequence", 0): lambda core: _dev_labels(core, DeviceType.Camera),
208
+ ("startSequenceAcquisition", 0): lambda core: _dev_labels(core, DeviceType.Camera),
209
+ ("stopExposureSequence", 0): lambda core: _dev_labels(core, DeviceType.Camera),
210
+ ("stopSequenceAcquisition", 0): lambda core: _dev_labels(core, DeviceType.Camera),
211
+ # --------------------
212
+ ("defineStateLabel", 0): lambda core: _dev_labels(core, DeviceType.State),
213
+ ("getNumberOfStates", 0): lambda core: _dev_labels(core, DeviceType.State),
214
+ ("getState", 0): lambda core: _dev_labels(core, DeviceType.State),
215
+ ("getStateFromLabel", 0): lambda core: _dev_labels(core, DeviceType.State),
216
+ ("getStateLabel", 0): lambda core: _dev_labels(core, DeviceType.State),
217
+ ("getStateLabels", 0): lambda core: _dev_labels(core, DeviceType.State),
218
+ ("setState", 0): lambda core: _dev_labels(core, DeviceType.State),
219
+ ("setStateLabel", 0): lambda core: _dev_labels(core, DeviceType.State),
220
+ # --------------------
221
+ ("getShutterOpen", 0): lambda core: _dev_labels(core, DeviceType.Shutter),
222
+ ("setShutterOpen", 0): lambda core: _dev_labels(core, DeviceType.Shutter),
223
+ # --------------------
224
+ ("getInstalledDeviceDescriptions", 0): lambda core: _dev_labels(core, DeviceType.Hub), # noqa
225
+ ("getInstalledDevices", 0): lambda core: _dev_labels(core, DeviceType.Hub),
226
+ ("getLoadedPeripheralDevices", 0): lambda core: _dev_labels(core, DeviceType.Hub),
227
+ ("getSerialPortAnswer", 0): lambda core: _dev_labels(core, DeviceType.Serial),
228
+ ("readFromSerialPort", 0): lambda core: _dev_labels(core, DeviceType.Serial),
229
+ ("setParentLabel", 1): lambda core: _dev_labels(core, DeviceType.Hub),
230
+
231
+ # ----------------- Config Groups --------------------
232
+ ("deleteConfig", 0): _config_group_names,
233
+ ("deleteConfigGroup", 0): _config_group_names,
234
+ ("getAvailableConfigs", 0): _config_group_names,
235
+ ("getConfigData", 0): _config_group_names,
236
+ ("getConfigGroupState", 0): _config_group_names,
237
+ ("getConfigGroupStateFromCache", 0): _config_group_names,
238
+ ("getConfigState", 0): _config_group_names,
239
+ ("getCurrentConfig", 0): _config_group_names,
240
+ ("getCurrentConfigFromCache", 0): _config_group_names,
241
+ ("renameConfig", 0): _config_group_names,
242
+ ("renameConfigGroup", 0): _config_group_names,
243
+ ("setChannelGroup", 0): _config_group_names,
244
+ ("setConfig", 0): _config_group_names,
245
+ ("waitForConfig", 0): _config_group_names,
246
+ }
247
+ # fmt: on
248
+
249
+
250
+ def _get_prop_names(core: CMMCorePlus, device: str) -> Sequence[SimpleCompletion]:
251
+ """Get the property names for a given device."""
252
+ try:
253
+ return [
254
+ SimpleCompletion(f'"{prop}"')
255
+ for prop in core.getDevicePropertyNames(device)
256
+ ]
257
+ except Exception: # pragma: no cover
258
+ return []
259
+
260
+
261
+ # fmt: off
262
+ # Map of (method_name, arg_index) -> (label arg idx, function that returns completions)
263
+ LABEL_COMPLETERS: dict[tuple[str, int], tuple[int, CoreLabelCompleter]] = {
264
+ ("define", 3): (2, _get_prop_names),
265
+ ("definePixelSizeConfig", 2): (1, _get_prop_names),
266
+ ("deleteConfig", 3): (2, _get_prop_names),
267
+ ("getAllowedPropertyValues", 1): (0, _get_prop_names),
268
+ ("getProperty", 1): (0, _get_prop_names),
269
+ ("getPropertyFromCache", 1): (0, _get_prop_names),
270
+ ("getPropertyLowerLimit", 1): (0, _get_prop_names),
271
+ ("getPropertySequenceMaxLength", 1): (0, _get_prop_names),
272
+ ("getPropertyType", 1): (0, _get_prop_names),
273
+ ("getPropertyUpperLimit", 1): (0, _get_prop_names),
274
+ ("hasProperty", 1): (0, _get_prop_names),
275
+ ("hasPropertyLimits", 1): (0, _get_prop_names),
276
+ ("isPropertyPreInit", 1): (0, _get_prop_names),
277
+ ("isPropertyReadOnly", 1): (0, _get_prop_names),
278
+ ("isPropertySequenceable", 1): (0, _get_prop_names),
279
+ ("loadPropertySequence", 1): (0, _get_prop_names),
280
+ ("setProperty", 1): (0, _get_prop_names),
281
+ ("startPropertySequence", 1): (0, _get_prop_names),
282
+ ("stopPropertySequence", 1): (0, _get_prop_names),
283
+ # ----------------- Config Presets --------------------
284
+ ("deleteConfig", 1): (0, _config_preset_names),
285
+ ("getConfigData", 1): (0, _config_preset_names),
286
+ ("getConfigState", 1): (0, _config_preset_names),
287
+ ("renameConfig", 1): (0, _config_preset_names),
288
+ ("setConfig", 1): (0, _config_preset_names),
289
+ ("waitForConfig", 1): (0, _config_preset_names),
290
+ }
291
+ # fmt: on
292
+
293
+
294
+ @context_matcher() # type: ignore[misc]
295
+ def cmmcoreplus_matcher(ctx: CompletionContext) -> SimpleMatcherResult:
296
+ """
297
+ Offer string completions for CMMCorePlus calls such as.
298
+
299
+ core.setCameraDevice(<TAB>)
300
+ core.setProperty("CAM", <TAB>)
301
+ """
302
+ if not (ip := get_ipython()):
303
+ return _null() # pragma: no cover
304
+ ns = ip.user_ns # live user namespace
305
+
306
+ # 1. Extract the object expression and the *real* attribute name syntactically.
307
+ # e.g.: 'core.setCameraDevice('
308
+ src_to_cursor = ctx.full_text[: ctx.cursor_position]
309
+ if not (m := OBJ_METHOD_RE.search(src_to_cursor)):
310
+ return _null()
311
+
312
+ # 2. Ensure we're dealing with a CMMCorePlus method
313
+ # e.g. ('core', 'setCameraDevice')
314
+ var_expr, method_name = m.group("obj"), m.group("attr")
315
+ try:
316
+ obj = eval(var_expr, ns)
317
+ except Exception:
318
+ return _null()
319
+
320
+ if not isinstance(obj, CMMCorePlus):
321
+ return _null()
322
+
323
+ # 3. Get the argument index for the method call.
324
+ arg_index = _get_argument_index(src_to_cursor, ns)
325
+
326
+ # 4. Check if we have a specific completion for this method name and arg_index.
327
+ key = (method_name, arg_index)
328
+ if (dev_getter := CORE_COMPLETERS.get(key)) and (completions := dev_getter(obj)):
329
+ # If we have a specific suggestion for this method and arg_index, use it.
330
+ return {"completions": completions, "suppress": True}
331
+
332
+ if info := LABEL_COMPLETERS.get(key):
333
+ dev_idx, getter = info
334
+ if dev_label := _get_argument(src_to_cursor, dev_idx):
335
+ if completions := getter(obj, dev_label):
336
+ return {"completions": completions, "suppress": True}
337
+
338
+ return _null()
339
+
340
+
341
+ def _get_argument(src: str, index: int) -> str:
342
+ """Parse the argument at the given index from a method call string.
343
+
344
+ For example:
345
+ >>> _get_argument("core.getProperty('Camera', ", 0)
346
+ 'Camera'
347
+ """
348
+ p0 = src.find("(") + 1
349
+ p1 = src.rfind(")") if ")" in src else None
350
+ if inner := src[slice(p0, p1)].strip():
351
+ args = inner.split(",")
352
+ if index < len(args):
353
+ return args[index].strip().strip("'\"")
354
+ return "" # pragma: no cover
355
+
356
+
357
+ def install_pymmcore_ipy_completion(shell: InteractiveShell | None = None) -> None:
358
+ """Install the CMMCorePlus completion matcher in the current IPython session."""
359
+ if shell is None and not (shell := get_ipython()):
360
+ return # pragma: no cover
361
+
362
+ if cmmcoreplus_matcher not in shell.Completer.custom_matchers:
363
+ shell.Completer.custom_matchers.append(cmmcoreplus_matcher)
@@ -24,7 +24,9 @@ class VersionInfo(NamedTuple):
24
24
  minor: int
25
25
  micro: int
26
26
  device_interface: int
27
- build: int
27
+ build: int = 0
28
28
 
29
29
 
30
- version_info = VersionInfo(*(int(x) for x in re.findall(r"\d+", __version__)))
30
+ # pass no more than 5 parts to VersionInfo
31
+ numbers = re.findall(r"\d+", __version__)[:5]
32
+ version_info = VersionInfo(*(int(i) for i in numbers))
@@ -13,6 +13,7 @@ import pymmcore_plus._pymmcore as pymmcore
13
13
  class Keyword(str, Enum):
14
14
  Name = pymmcore.g_Keyword_Name
15
15
  Description = pymmcore.g_Keyword_Description
16
+
16
17
  CameraName = pymmcore.g_Keyword_CameraName
17
18
  CameraID = pymmcore.g_Keyword_CameraID
18
19
  CameraChannelName = pymmcore.g_Keyword_CameraChannelName
@@ -31,6 +32,7 @@ class Keyword(str, Enum):
31
32
  Offset = pymmcore.g_Keyword_Offset
32
33
  CCDTemperature = pymmcore.g_Keyword_CCDTemperature
33
34
  CCDTemperatureSetPoint = pymmcore.g_Keyword_CCDTemperatureSetPoint
35
+
34
36
  State = pymmcore.g_Keyword_State
35
37
  Label = pymmcore.g_Keyword_Label
36
38
  Position = pymmcore.g_Keyword_Position
@@ -69,12 +71,15 @@ class Keyword(str, Enum):
69
71
  HubID = pymmcore.g_Keyword_HubID
70
72
 
71
73
  # image annotations
72
- Meatdata_Exposure = pymmcore.g_Keyword_Meatdata_Exposure
73
- Metadata_Score = pymmcore.g_Keyword_Metadata_Score
74
+ Metadata_CameraLabel = pymmcore.g_Keyword_Metadata_CameraLabel
75
+ Metadata_Exposure = pymmcore.g_Keyword_Metadata_Exposure
76
+ Metadata_Height = pymmcore.g_Keyword_Metadata_Height
74
77
  Metadata_ImageNumber = pymmcore.g_Keyword_Metadata_ImageNumber
75
78
  Metadata_ROI_X = pymmcore.g_Keyword_Metadata_ROI_X
76
79
  Metadata_ROI_Y = pymmcore.g_Keyword_Metadata_ROI_Y
80
+ Metadata_Score = pymmcore.g_Keyword_Metadata_Score
77
81
  Metadata_TimeInCore = pymmcore.g_Keyword_Metadata_TimeInCore
82
+ Metadata_Width = pymmcore.g_Keyword_Metadata_Width
78
83
 
79
84
  def __str__(self) -> str:
80
85
  return str(self.value)
@@ -137,6 +142,8 @@ class DeviceType(IntEnum):
137
142
  SLMDevice = pymmcore.SLMDevice
138
143
  HubDevice = pymmcore.HubDevice
139
144
  GalvoDevice = pymmcore.GalvoDevice
145
+ PressurePumpDevice = pymmcore.PressurePumpDevice
146
+ VolumetricPumpDevice = pymmcore.VolumetricPumpDevice
140
147
  # aliases for clearer naming (e.g. `DeviceType.Camera`)
141
148
  Unknown = UnknownType
142
149
  Any = AnyType
@@ -155,6 +162,8 @@ class DeviceType(IntEnum):
155
162
  SLM = SLMDevice
156
163
  Hub = HubDevice
157
164
  Galvo = GalvoDevice
165
+ PressurePump = PressurePumpDevice
166
+ VolumetricPump = VolumetricPumpDevice
158
167
 
159
168
  def __str__(self) -> str:
160
169
  return str(self.name).replace("Type", "").replace("Device", "")
@@ -165,7 +174,9 @@ class PropertyType(IntEnum):
165
174
  String = pymmcore.String
166
175
  Float = pymmcore.Float
167
176
  Integer = pymmcore.Integer
177
+
168
178
  Boolean = auto() # not supported in pymmcore
179
+ Enum = auto() # not supported in pymmcore
169
180
 
170
181
  def to_python(self) -> type | None:
171
182
  return {0: None, 1: str, 2: float, 3: int}[self]
@@ -183,7 +194,16 @@ class PropertyType(IntEnum):
183
194
  if value is None:
184
195
  return PropertyType.Undef
185
196
  if isinstance(value, str):
186
- return PropertyType[value.lower().capitalize()]
197
+ if value.lower() in ("int", "integer"):
198
+ return PropertyType.Integer
199
+ if value.lower() in ("float", "double"):
200
+ return PropertyType.Float
201
+ if value.lower() in ("bool", "boolean"):
202
+ return PropertyType.Boolean
203
+ if value.lower() in ("string", "str"):
204
+ return PropertyType.String
205
+ if value.lower() in ("enum", "enumeration"):
206
+ return PropertyType.Enum
187
207
  if isinstance(value, type):
188
208
  if value is float:
189
209
  return PropertyType.Float
@@ -193,6 +213,8 @@ class PropertyType(IntEnum):
193
213
  return PropertyType.String
194
214
  elif value is bool:
195
215
  return PropertyType.Boolean
216
+ elif issubclass(value, Enum):
217
+ return PropertyType.Enum
196
218
 
197
219
  raise TypeError(
198
220
  f"Property type must be a PropertyType enum member, "