pymmcore-plus 0.15.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 (113) hide show
  1. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/.gitignore +1 -0
  2. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/PKG-INFO +2 -2
  3. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/pyproject.toml +3 -4
  4. {pymmcore_plus-0.15.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.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/core/_constants.py +4 -0
  7. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/core/_mmcore_plus.py +15 -16
  8. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/core/_sequencing.py +1 -1
  9. pymmcore_plus-0.15.2/src/pymmcore_plus/core/events/_deprecated.py +67 -0
  10. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/core/events/_protocol.py +15 -5
  11. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/core/events/_psygnal.py +33 -4
  12. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/core/events/_qsignals.py +34 -6
  13. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/experimental/unicore/__init__.py +7 -3
  14. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/experimental/unicore/_device_manager.py +1 -1
  15. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/experimental/unicore/core/_sequence_buffer.py +23 -27
  16. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/experimental/unicore/core/_unicore.py +90 -23
  17. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/experimental/unicore/devices/_camera.py +10 -5
  18. pymmcore_plus-0.15.2/src/pymmcore_plus/experimental/unicore/devices/_generic_device.py +12 -0
  19. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/experimental/unicore/devices/_properties.py +1 -1
  20. pymmcore_plus-0.15.2/src/pymmcore_plus/experimental/unicore/devices/_shutter.py +30 -0
  21. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/experimental/unicore/devices/_slm.py +1 -1
  22. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/experimental/unicore/devices/_stage.py +1 -1
  23. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/experimental/unicore/devices/_state.py +1 -1
  24. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/tests/test_bench.py +7 -5
  25. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/tests/test_events.py +85 -4
  26. pymmcore_plus-0.15.2/tests/test_ipy_completions.py +164 -0
  27. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/tests/unicore/test_camera.py +15 -6
  28. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/tests/unicore/test_sequence_buffer.py +23 -23
  29. pymmcore_plus-0.15.2/tests/unicore/test_shutter.py +56 -0
  30. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/tests/unicore/test_unicore.py +4 -4
  31. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/LICENSE +0 -0
  32. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/README.md +0 -0
  33. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/_accumulator.py +0 -0
  34. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/_benchmark.py +0 -0
  35. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/_build.py +0 -0
  36. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/_cli.py +0 -0
  37. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/_logger.py +0 -0
  38. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/_pymmcore.py +0 -0
  39. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/_util.py +0 -0
  40. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/core/__init__.py +0 -0
  41. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/core/_adapter.py +0 -0
  42. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/core/_config.py +0 -0
  43. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/core/_config_group.py +0 -0
  44. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/core/_device.py +0 -0
  45. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/core/_metadata.py +0 -0
  46. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/core/_property.py +0 -0
  47. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/core/events/__init__.py +0 -0
  48. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/core/events/_device_signal_view.py +0 -0
  49. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/core/events/_norm_slot.py +0 -0
  50. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/core/events/_prop_event_mixin.py +0 -0
  51. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/experimental/__init__.py +0 -0
  52. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/experimental/unicore/_proxy.py +0 -0
  53. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/experimental/unicore/core/__init__.py +0 -0
  54. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/experimental/unicore/devices/__init__.py +0 -0
  55. /pymmcore_plus-0.15.0/src/pymmcore_plus/experimental/unicore/devices/_device.py → /pymmcore_plus-0.15.2/src/pymmcore_plus/experimental/unicore/devices/_device_base.py +0 -0
  56. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/install.py +0 -0
  57. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/mda/__init__.py +0 -0
  58. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/mda/_engine.py +0 -0
  59. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/mda/_protocol.py +0 -0
  60. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/mda/_runner.py +0 -0
  61. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/mda/_thread_relay.py +0 -0
  62. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/mda/events/__init__.py +0 -0
  63. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/mda/events/_protocol.py +0 -0
  64. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/mda/events/_psygnal.py +0 -0
  65. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/mda/events/_qsignals.py +0 -0
  66. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/mda/handlers/_5d_writer_base.py +0 -0
  67. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/mda/handlers/__init__.py +0 -0
  68. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/mda/handlers/_img_sequence_writer.py +0 -0
  69. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/mda/handlers/_ome_tiff_writer.py +0 -0
  70. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/mda/handlers/_ome_zarr_writer.py +0 -0
  71. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/mda/handlers/_tensorstore_handler.py +0 -0
  72. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/mda/handlers/_util.py +0 -0
  73. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/metadata/__init__.py +0 -0
  74. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/metadata/functions.py +0 -0
  75. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/metadata/schema.py +0 -0
  76. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/metadata/serialize.py +0 -0
  77. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/mocks.py +0 -0
  78. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/model/__init__.py +0 -0
  79. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/model/_config_file.py +0 -0
  80. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/model/_config_group.py +0 -0
  81. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/model/_core_device.py +0 -0
  82. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/model/_core_link.py +0 -0
  83. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/model/_device.py +0 -0
  84. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/model/_microscope.py +0 -0
  85. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/model/_pixel_size_config.py +0 -0
  86. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/model/_property.py +0 -0
  87. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/py.typed +0 -0
  88. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/src/pymmcore_plus/seq_tester.py +0 -0
  89. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/tests/__init__.py +0 -0
  90. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/tests/conftest.py +0 -0
  91. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/tests/io/test_image_sequence_writer.py +0 -0
  92. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/tests/io/test_ome_tiff.py +0 -0
  93. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/tests/io/test_zarr_writers.py +0 -0
  94. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/tests/local_config.cfg +0 -0
  95. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/tests/test_accumulators.py +0 -0
  96. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/tests/test_adapter_class.py +0 -0
  97. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/tests/test_cli.py +0 -0
  98. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/tests/test_config_group_class.py +0 -0
  99. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/tests/test_core.py +0 -0
  100. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/tests/test_device_class.py +0 -0
  101. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/tests/test_mda.py +0 -0
  102. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/tests/test_metadata.py +0 -0
  103. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/tests/test_misc.py +0 -0
  104. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/tests/test_model.py +0 -0
  105. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/tests/test_pixel_config_class.py +0 -0
  106. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/tests/test_property_class.py +0 -0
  107. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/tests/test_sequencing.py +0 -0
  108. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/tests/test_slm_image.py +0 -0
  109. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/tests/test_thread_relay.py +0 -0
  110. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/tests/unicore/conftest.py +0 -0
  111. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/tests/unicore/test_slm.py +0 -0
  112. {pymmcore_plus-0.15.0 → pymmcore_plus-0.15.2}/tests/unicore/test_state.py +0 -0
  113. {pymmcore_plus-0.15.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.15.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,12 +74,12 @@ 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 = [{ include-group = 'test' }, "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]"]
@@ -87,7 +87,6 @@ PySide2 = [{ include-group = 'test-qt' }, "pymmcore-plus[PySide2]"]
87
87
  dev = [
88
88
  # { include-group = "docs" },
89
89
  { include-group = "PyQt6" },
90
- "ipython>=8.18.1",
91
90
  "pdbpp>=0.11.6 ; sys_platform != 'win32'",
92
91
  "mypy>=1.14.1",
93
92
  "pre-commit>=4.1.0",
@@ -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)
@@ -142,6 +142,8 @@ class DeviceType(IntEnum):
142
142
  SLMDevice = pymmcore.SLMDevice
143
143
  HubDevice = pymmcore.HubDevice
144
144
  GalvoDevice = pymmcore.GalvoDevice
145
+ PressurePumpDevice = pymmcore.PressurePumpDevice
146
+ VolumetricPumpDevice = pymmcore.VolumetricPumpDevice
145
147
  # aliases for clearer naming (e.g. `DeviceType.Camera`)
146
148
  Unknown = UnknownType
147
149
  Any = AnyType
@@ -160,6 +162,8 @@ class DeviceType(IntEnum):
160
162
  SLM = SLMDevice
161
163
  Hub = HubDevice
162
164
  Galvo = GalvoDevice
165
+ PressurePump = PressurePumpDevice
166
+ VolumetricPump = VolumetricPumpDevice
163
167
 
164
168
  def __str__(self) -> str:
165
169
  return str(self.name).replace("Type", "").replace("Device", "")
@@ -335,7 +335,7 @@ class CMMCorePlus(pymmcore.CMMCore):
335
335
  if hasattr(self, "_weak_clean"):
336
336
  atexit.unregister(self._weak_clean)
337
337
  try:
338
- super().registerCallback(None) # type: ignore
338
+ super().registerCallback(None)
339
339
  self.reset()
340
340
  # clean up logging
341
341
  self.setPrimaryLogFile("")
@@ -1626,7 +1626,7 @@ class CMMCorePlus(pymmcore.CMMCore):
1626
1626
  self.events.propertyChanged.emit(self.getShutterDevice(), "State", True)
1627
1627
  try:
1628
1628
  self._do_snap_image()
1629
- self.events.imageSnapped.emit()
1629
+ self.events.imageSnapped.emit(self.getCameraDevice())
1630
1630
  finally:
1631
1631
  if autoshutter:
1632
1632
  self.events.propertyChanged.emit(
@@ -2037,26 +2037,27 @@ class CMMCorePlus(pymmcore.CMMCore):
2037
2037
  self.events.autoShutterSet.emit(state)
2038
2038
 
2039
2039
  @overload
2040
- def setShutterOpen(self, state: bool) -> None: ...
2041
-
2040
+ def setShutterOpen(self, state: bool, /) -> None: ...
2042
2041
  @overload
2043
- def setShutterOpen(self, shutterLabel: str, state: bool) -> None: ...
2044
-
2045
- def setShutterOpen(self, *args: Any, **kwargs: Any) -> None:
2042
+ def setShutterOpen(self, shutterLabel: str, state: bool, /) -> None: ...
2043
+ def setShutterOpen(self, *args: Any) -> None:
2046
2044
  """Open or close the currently selected or `shutterLabel` shutter.
2047
2045
 
2048
2046
  **Why Override?** To emit a `propertyChanged` event.
2049
2047
  """
2050
- super().setShutterOpen(*args, **kwargs)
2051
- shutterLabel, state = kwargs.get("shutterLabel"), kwargs.get("state")
2052
2048
  if len(args) == 2:
2053
2049
  shutterLabel, state = args
2054
2050
  elif len(args) == 1:
2055
2051
  shutterLabel = super().getShutterDevice()
2056
2052
  state = args[0]
2053
+ self._do_shutter_open(shutterLabel, state)
2057
2054
  state = str(int(bool(state)))
2058
2055
  self.events.propertyChanged.emit(shutterLabel, "State", state)
2059
2056
 
2057
+ def _do_shutter_open(self, shutterLabel: str, state: bool, /) -> None:
2058
+ """Open or close the shutter."""
2059
+ super().setShutterOpen(shutterLabel, state)
2060
+
2060
2061
  @overload
2061
2062
  def deleteConfig(self, groupName: str, configName: str) -> None: ...
2062
2063
 
@@ -2384,8 +2385,9 @@ class CMMCorePlus(pymmcore.CMMCore):
2384
2385
  try:
2385
2386
  before = [self.getProperty(device, p) for p in properties]
2386
2387
  except Exception as e:
2387
- logger.error(
2388
- "Error getting properties %s on %s: %s. Cannot ensure signal emission",
2388
+ logger.warning(
2389
+ "Error getting properties %s on %s: %s. "
2390
+ "Cannot ensure propertyChanged signal emission",
2389
2391
  properties,
2390
2392
  device,
2391
2393
  e,
@@ -2562,12 +2564,9 @@ class _MMCallbackRelay(pymmcore.MMEventCallback):
2562
2564
  return reemit
2563
2565
 
2564
2566
 
2567
+ MMCORE_SIGNAL_NAMES = {n for n in dir(pymmcore.MMEventCallback) if n.startswith("on")}
2565
2568
  MMCallbackRelay = type(
2566
2569
  "MMCallbackRelay",
2567
2570
  (_MMCallbackRelay,),
2568
- {
2569
- n: _MMCallbackRelay.make_reemitter(n)
2570
- for n in dir(pymmcore.MMEventCallback)
2571
- if n.startswith("on")
2572
- },
2571
+ {n: _MMCallbackRelay.make_reemitter(n) for n in MMCORE_SIGNAL_NAMES},
2573
2572
  )
@@ -66,7 +66,7 @@ class SequencedEvent(MDAEvent):
66
66
  slm_sequence: tuple[bytes, ...] = Field(default_factory=tuple)
67
67
 
68
68
  # re-defining this from MDAEvent to circumvent a strange issue with pydantic 2.11
69
- sequence: Optional[MDASequence] = Field(default=None, repr=False) # noqa: UP007
69
+ sequence: Optional[MDASequence] = Field(default=None, repr=False) # noqa: UP045
70
70
 
71
71
  # all other property sequences
72
72
  property_sequences: dict[tuple[str, str], list[str]] = Field(default_factory=dict)
@@ -0,0 +1,67 @@
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ import warnings
5
+ from typing import TYPE_CHECKING, Any, Callable
6
+
7
+ if TYPE_CHECKING:
8
+ from ._protocol import PSignalInstance
9
+
10
+
11
+ class DeprecatedSignalProxy:
12
+ def __init__(
13
+ self,
14
+ signal: PSignalInstance,
15
+ current_n_args: int,
16
+ deprecated_posargs: tuple[Any, ...],
17
+ ) -> None:
18
+ self._signal = signal
19
+ self._current_n_args = current_n_args
20
+ self._deprecated_posargs = deprecated_posargs
21
+ self._shims: dict[Callable, Callable] = {}
22
+
23
+ def connect(self, slot: Callable) -> Any:
24
+ """Connect slot to this signal."""
25
+ min_pos_args = sum(
26
+ 1
27
+ for p in inspect.signature(slot).parameters.values()
28
+ if p.kind
29
+ in {
30
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
31
+ inspect.Parameter.POSITIONAL_ONLY,
32
+ }
33
+ and p.default is inspect.Parameter.empty
34
+ )
35
+
36
+ num_extra_pos_args = min_pos_args - self._current_n_args
37
+ if num_extra_pos_args > 0:
38
+ extra_args = self._deprecated_posargs[:num_extra_pos_args]
39
+ warnings.warn(
40
+ f"Callback {slot.__name__!r} requires {min_pos_args} positional "
41
+ f"arguments, but this signal only supports {self._current_n_args}. "
42
+ "Fake arguments will be added, but this will be an exception in the "
43
+ "future. Please update your callback.",
44
+ FutureWarning,
45
+ stacklevel=2,
46
+ )
47
+
48
+ def _shim(*args: Any) -> Any:
49
+ slot(*args[: self._current_n_args], *extra_args)
50
+
51
+ self._shims[slot] = _shim
52
+ self._signal.connect(_shim)
53
+ else:
54
+ self._signal.connect(slot)
55
+
56
+ def disconnect(self, slot: Callable | None = None) -> Any:
57
+ """Disconnect slot from this signal.
58
+
59
+ If `None`, all slots should be disconnected.
60
+ """
61
+ if slot in self._shims:
62
+ slot = self._shims.pop(slot)
63
+ return self._signal.disconnect(slot)
64
+
65
+ def emit(self, *args: Any) -> Any:
66
+ """Emits the signal with the given arguments."""
67
+ return self._signal.emit(*args[: self._current_n_args])