pymmcore-plus 0.9.4__py3-none-any.whl → 0.13.0__py3-none-any.whl

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 (68) hide show
  1. pymmcore_plus/__init__.py +7 -4
  2. pymmcore_plus/_benchmark.py +203 -0
  3. pymmcore_plus/_build.py +6 -1
  4. pymmcore_plus/_cli.py +131 -31
  5. pymmcore_plus/_logger.py +19 -10
  6. pymmcore_plus/_pymmcore.py +12 -0
  7. pymmcore_plus/_util.py +133 -30
  8. pymmcore_plus/core/__init__.py +5 -0
  9. pymmcore_plus/core/_config.py +6 -4
  10. pymmcore_plus/core/_config_group.py +4 -3
  11. pymmcore_plus/core/_constants.py +135 -10
  12. pymmcore_plus/core/_device.py +4 -4
  13. pymmcore_plus/core/_metadata.py +3 -3
  14. pymmcore_plus/core/_mmcore_plus.py +254 -170
  15. pymmcore_plus/core/_property.py +6 -6
  16. pymmcore_plus/core/_sequencing.py +370 -233
  17. pymmcore_plus/core/events/__init__.py +6 -6
  18. pymmcore_plus/core/events/_device_signal_view.py +8 -6
  19. pymmcore_plus/core/events/_norm_slot.py +2 -4
  20. pymmcore_plus/core/events/_prop_event_mixin.py +7 -4
  21. pymmcore_plus/core/events/_protocol.py +5 -2
  22. pymmcore_plus/core/events/_psygnal.py +2 -2
  23. pymmcore_plus/experimental/__init__.py +0 -0
  24. pymmcore_plus/experimental/unicore/__init__.py +14 -0
  25. pymmcore_plus/experimental/unicore/_device_manager.py +173 -0
  26. pymmcore_plus/experimental/unicore/_proxy.py +127 -0
  27. pymmcore_plus/experimental/unicore/_unicore.py +703 -0
  28. pymmcore_plus/experimental/unicore/devices/__init__.py +0 -0
  29. pymmcore_plus/experimental/unicore/devices/_device.py +269 -0
  30. pymmcore_plus/experimental/unicore/devices/_properties.py +400 -0
  31. pymmcore_plus/experimental/unicore/devices/_stage.py +221 -0
  32. pymmcore_plus/install.py +16 -11
  33. pymmcore_plus/mda/__init__.py +1 -1
  34. pymmcore_plus/mda/_engine.py +320 -148
  35. pymmcore_plus/mda/_protocol.py +6 -4
  36. pymmcore_plus/mda/_runner.py +62 -51
  37. pymmcore_plus/mda/_thread_relay.py +5 -3
  38. pymmcore_plus/mda/events/__init__.py +2 -2
  39. pymmcore_plus/mda/events/_protocol.py +10 -2
  40. pymmcore_plus/mda/events/_psygnal.py +2 -2
  41. pymmcore_plus/mda/handlers/_5d_writer_base.py +106 -15
  42. pymmcore_plus/mda/handlers/__init__.py +7 -1
  43. pymmcore_plus/mda/handlers/_img_sequence_writer.py +11 -6
  44. pymmcore_plus/mda/handlers/_ome_tiff_writer.py +8 -4
  45. pymmcore_plus/mda/handlers/_ome_zarr_writer.py +82 -9
  46. pymmcore_plus/mda/handlers/_tensorstore_handler.py +374 -0
  47. pymmcore_plus/mda/handlers/_util.py +1 -1
  48. pymmcore_plus/metadata/__init__.py +36 -0
  49. pymmcore_plus/metadata/functions.py +353 -0
  50. pymmcore_plus/metadata/schema.py +472 -0
  51. pymmcore_plus/metadata/serialize.py +120 -0
  52. pymmcore_plus/mocks.py +51 -0
  53. pymmcore_plus/model/_config_file.py +5 -6
  54. pymmcore_plus/model/_config_group.py +29 -2
  55. pymmcore_plus/model/_core_device.py +12 -1
  56. pymmcore_plus/model/_core_link.py +2 -1
  57. pymmcore_plus/model/_device.py +39 -8
  58. pymmcore_plus/model/_microscope.py +39 -3
  59. pymmcore_plus/model/_pixel_size_config.py +27 -4
  60. pymmcore_plus/model/_property.py +13 -3
  61. pymmcore_plus/seq_tester.py +1 -1
  62. {pymmcore_plus-0.9.4.dist-info → pymmcore_plus-0.13.0.dist-info}/METADATA +22 -11
  63. pymmcore_plus-0.13.0.dist-info/RECORD +71 -0
  64. {pymmcore_plus-0.9.4.dist-info → pymmcore_plus-0.13.0.dist-info}/WHEEL +1 -1
  65. pymmcore_plus/core/_state.py +0 -244
  66. pymmcore_plus-0.9.4.dist-info/RECORD +0 -55
  67. {pymmcore_plus-0.9.4.dist-info → pymmcore_plus-0.13.0.dist-info}/entry_points.txt +0 -0
  68. {pymmcore_plus-0.9.4.dist-info → pymmcore_plus-0.13.0.dist-info}/licenses/LICENSE +0 -0
File without changes
@@ -0,0 +1,269 @@
1
+ from __future__ import annotations
2
+
3
+ import threading
4
+ from abc import ABC
5
+ from collections import ChainMap
6
+ from typing import TYPE_CHECKING, Any, Callable, ClassVar, Generic, TypeVar, final
7
+
8
+ from pymmcore_plus.core import DeviceType
9
+ from pymmcore_plus.core._constants import PropertyType
10
+ from pymmcore_plus.experimental.unicore.devices._properties import (
11
+ PropertyController,
12
+ PropertyInfo,
13
+ )
14
+
15
+ if TYPE_CHECKING:
16
+ from collections.abc import KeysView, Sequence
17
+
18
+ from typing_extensions import Any, Self
19
+
20
+ from pymmcore_plus.experimental.unicore._proxy import CMMCoreProxy
21
+
22
+ from ._properties import PropArg, TDev, TProp
23
+
24
+
25
+ class _Lockable:
26
+ """Mixin to make an object lockable."""
27
+
28
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
29
+ super().__init__(*args, **kwargs)
30
+ self._lock = threading.Lock()
31
+
32
+ def __enter__(self) -> Self:
33
+ self._lock.acquire()
34
+ return self
35
+
36
+ def __exit__(self, *args: Any) -> None:
37
+ self._lock.release()
38
+
39
+ def lock(self, blocking: bool = True, timeout: float = -1) -> bool:
40
+ return self._lock.acquire(blocking, timeout) # pragma: no cover
41
+
42
+ def unlock(self) -> None:
43
+ self._lock.release() # pragma: no cover
44
+
45
+ def locked(self) -> bool:
46
+ return self._lock.locked() # pragma: no cover
47
+
48
+
49
+ class Device(_Lockable, ABC):
50
+ """ABC for all Devices."""
51
+
52
+ _TYPE: ClassVar[DeviceType] = DeviceType.UnknownType
53
+ _cls_prop_controllers: ClassVar[dict[str, PropertyController]]
54
+
55
+ def __init__(self) -> None:
56
+ super().__init__()
57
+
58
+ # NOTE: The following attributes are here for the core to manipulate.
59
+ # Device Adapter subclasses should not touch these attributes.
60
+ self._label_: str = ""
61
+ self._initialized_: bool | BaseException = False
62
+ self._prop_controllers_ = ChainMap[str, PropertyController](
63
+ {}, self._cls_prop_controllers
64
+ )
65
+ self._core_proxy_: CMMCoreProxy | None = None
66
+
67
+ @property
68
+ def core(self) -> CMMCoreProxy:
69
+ """The device may use this to access a restricted subset of the Core API."""
70
+ if self._core_proxy_ is None:
71
+ raise AttributeError("CoreProxy not set. Has this device been loaded?")
72
+ return self._core_proxy_
73
+
74
+ def __init_subclass__(cls) -> None:
75
+ """Initialize the property controllers."""
76
+ cls._cls_prop_controllers = {
77
+ p.property.name: p
78
+ for p in cls.__dict__.values()
79
+ if isinstance(p, PropertyController)
80
+ }
81
+ return super().__init_subclass__()
82
+
83
+ def register_property(
84
+ self,
85
+ name: str,
86
+ *,
87
+ default_value: TProp | None = None,
88
+ getter: Callable[[TDev], TProp] | None = None,
89
+ setter: Callable[[TDev, TProp], None] | None = None,
90
+ limits: tuple[int | float, int | float] | None = None,
91
+ sequence_max_length: int = 0,
92
+ allowed_values: Sequence[TProp] | None = None,
93
+ is_read_only: bool = False,
94
+ is_pre_init: bool = False,
95
+ property_type: PropArg = None,
96
+ ) -> None:
97
+ """Manually register a property.
98
+
99
+ This is an alternative to using the `@pymm_property` decorator. It can be used
100
+ to register properties that are not defined in the class body. This is useful
101
+ for pure "user-side" properties that are not used by the adapter, but which the
102
+ adapter may want to access (such as a preference or a configuration setting
103
+ that doesn't affect the device's behavior, but which the adapter may want to
104
+ read).
105
+
106
+ Properties defined this way are not accessible as class attributes.
107
+ """
108
+ if property_type is None and default_value is not None:
109
+ property_type = type(default_value)
110
+
111
+ prop_info = PropertyInfo(
112
+ name=name,
113
+ default_value=default_value,
114
+ last_value=default_value,
115
+ limits=limits,
116
+ sequence_max_length=sequence_max_length,
117
+ description="",
118
+ allowed_values=allowed_values,
119
+ is_read_only=is_read_only,
120
+ is_pre_init=is_pre_init,
121
+ type=PropertyType.create(property_type),
122
+ )
123
+ controller = PropertyController(property=prop_info, fget=getter, fset=setter)
124
+ self._prop_controllers_[name] = controller
125
+
126
+ def initialize(self) -> None:
127
+ """Initialize the device."""
128
+
129
+ def shutdown(self) -> None:
130
+ """Shutdown the device."""
131
+
132
+ @final # may not be overridden
133
+ def get_label(self) -> str:
134
+ return self._label_
135
+
136
+ @final # may not be overridden
137
+ @classmethod
138
+ def type(cls) -> DeviceType:
139
+ """Return the type of the device."""
140
+ return cls._TYPE
141
+
142
+ @classmethod
143
+ def name(cls) -> str:
144
+ """Return the name of the device."""
145
+ return f"{cls.__name__}"
146
+
147
+ def description(self) -> str:
148
+ """Return a description of the device."""
149
+ return self.__doc__ or ""
150
+
151
+ def busy(self) -> bool:
152
+ """Return `True` if the device is busy."""
153
+ return False
154
+
155
+ # PROPERTIES
156
+
157
+ def get_property_names(self) -> KeysView[str]:
158
+ """Return the names of the properties."""
159
+ return self._prop_controllers_.keys()
160
+
161
+ def property(self, prop_name: str) -> PropertyInfo:
162
+ """Return the property controller for a property."""
163
+ return self._prop_controllers_[prop_name].property
164
+
165
+ def get_property_value(self, prop_name: str) -> Any:
166
+ """Return the value of a property."""
167
+ # TODO: catch errors
168
+ ctrl = self._prop_controllers_[prop_name]
169
+ if ctrl.fget is None:
170
+ return ctrl.property.last_value
171
+ return self._prop_controllers_[prop_name].__get__(self, self.__class__)
172
+
173
+ def set_property_value(self, prop_name: str, value: Any) -> None:
174
+ """Set the value of a property."""
175
+ # TODO: catch errors
176
+ ctrl = self._prop_controllers_[prop_name]
177
+ if ctrl.is_read_only:
178
+ raise ValueError(f"Property {prop_name!r} is read-only.")
179
+ if ctrl.fset is not None:
180
+ ctrl.__set__(self, value)
181
+ else:
182
+ ctrl.property.last_value = ctrl.validate(value)
183
+
184
+ def load_property_sequence(self, prop_name: str, sequence: Sequence[Any]) -> None:
185
+ """Load a sequence into a property."""
186
+ self._prop_controllers_[prop_name].load_sequence(self, sequence)
187
+
188
+ def start_property_sequence(self, prop_name: str) -> None:
189
+ """Start a sequence of a property."""
190
+ self._prop_controllers_[prop_name].start_sequence(self)
191
+
192
+ def stop_property_sequence(self, prop_name: str) -> None:
193
+ """Stop a sequence of a property."""
194
+ self._prop_controllers_[prop_name].stop_sequence(self)
195
+
196
+ def set_property_allowed_values(
197
+ self, prop_name: str, allowed_values: Sequence[Any]
198
+ ) -> None:
199
+ """Set the allowed values of a property."""
200
+ self._prop_controllers_[prop_name].property.allowed_values = allowed_values
201
+
202
+ def set_property_limits(
203
+ self, prop_name: str, limits: tuple[float, float] | None
204
+ ) -> None:
205
+ """Set the limits of a property."""
206
+ self._prop_controllers_[prop_name].property.limits = limits
207
+
208
+ def set_property_sequence_max_length(self, prop_name: str, max_length: int) -> None:
209
+ """Set the sequence max length of a property."""
210
+ self._prop_controllers_[prop_name].property.sequence_max_length = max_length
211
+
212
+ def is_property_sequenceable(self, prop_name: str) -> bool:
213
+ """Return `True` if the property is sequenceable."""
214
+ return self._prop_controllers_[prop_name].is_sequenceable
215
+
216
+ def is_property_read_only(self, prop_name: str) -> bool:
217
+ """Return `True` if the property is read-only."""
218
+ return self._prop_controllers_[prop_name].is_read_only
219
+
220
+
221
+ SeqT = TypeVar("SeqT")
222
+
223
+
224
+ class SequenceableDevice(Device, Generic[SeqT]):
225
+ """ABC For devices that can handle sequences of values.
226
+
227
+ This is used for *most* device classes (XYStage, State, etc...). We
228
+ convert the core `is<>Sequenceable` methods into a call to the
229
+ `is_property_sequenceable` method on the "current" device of that type.
230
+ """
231
+
232
+ def __init__(self, *args: Any, **kwargs: Any) -> None:
233
+ super().__init__(*args, **kwargs)
234
+
235
+ @final
236
+ def is_sequenceable(self) -> bool:
237
+ """Return `True` if the device is sequenceable. Default is `False`."""
238
+ if self.get_sequence_max_length() == 0:
239
+ return False
240
+
241
+ # A device is sequenceable if it returns a max sequence length > 0 AND
242
+ # has reimplemented the send_sequence and start_sequence methods.
243
+ # we climb the method resolution chain and make sure that at least one base
244
+ # class has reimplemented these methods.
245
+ mro = self.__class__.mro()
246
+ send_sequence_definer = next(b for b in mro if "send_sequence" in b.__dict__)
247
+ start_sequence_definer = next(b for b in mro if "start_sequence" in b.__dict__)
248
+ return (
249
+ send_sequence_definer is not SequenceableDevice
250
+ and start_sequence_definer is not SequenceableDevice
251
+ )
252
+
253
+ def get_sequence_max_length(self) -> int:
254
+ """Return the sequence."""
255
+ return 0
256
+
257
+ def send_sequence(self, sequence: tuple[SeqT, ...]) -> None:
258
+ """Signal that we are done appending sequence values.
259
+
260
+ So that the adapter can send the whole sequence to the device
261
+ """
262
+ raise NotImplementedError("This device has not been made sequenceable.")
263
+
264
+ def start_sequence(self) -> None:
265
+ """Start the sequence."""
266
+ raise NotImplementedError("This device has not been made sequenceable.")
267
+
268
+ def stop_sequence(self) -> None:
269
+ """Stop the sequence."""
@@ -0,0 +1,400 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from dataclasses import dataclass
5
+ from typing import (
6
+ TYPE_CHECKING,
7
+ Any,
8
+ Callable,
9
+ Generic,
10
+ Literal,
11
+ TypeVar,
12
+ Union,
13
+ cast,
14
+ overload,
15
+ )
16
+
17
+ from pymmcore_plus.core._constants import PropertyType
18
+
19
+ if TYPE_CHECKING:
20
+ from collections.abc import Sequence
21
+
22
+ from typing_extensions import Self, TypeAlias
23
+
24
+ from ._device import Device
25
+
26
+ PropArg: TypeAlias = (
27
+ PropertyType | type | Literal["float", "integer", "string", "boolean"] | None
28
+ )
29
+
30
+ TDev = TypeVar("TDev", bound="Device")
31
+ TProp = TypeVar("TProp")
32
+ TLim = TypeVar("TLim", bound=Union[int, float])
33
+
34
+
35
+ slots_true = {"slots": True} if sys.version_info >= (3, 10) else {}
36
+ kw_only_true = {"kw_only": True} if sys.version_info >= (3, 10) else {}
37
+
38
+
39
+ @dataclass(**kw_only_true, **slots_true)
40
+ class PropertyInfo(Generic[TProp]):
41
+ """State of a property of a device.
42
+
43
+ Attributes
44
+ ----------
45
+ name : str
46
+ The name of the property.
47
+ default_value : TProp, optional
48
+ The default value of the property, by default None.
49
+ last_value : TProp, optional
50
+ The last value seen from the device, by default None.
51
+ limits : tuple[int | float, int | float], optional
52
+ The minimum and maximum values of the property, by default None.
53
+ sequence_max_length : int
54
+ The maximum length of a sequence of property values.
55
+ description : str, optional
56
+ A description of the property, by default None.
57
+ type : PropertyType
58
+ The type of the property.
59
+ allowed_values : Sequence[TProp], optional
60
+ The allowed values of the property, by default None.
61
+ is_read_only : bool
62
+ Whether the property is read-only.
63
+ is_pre_init : bool
64
+ Whether the property must be set before initialization.
65
+ """
66
+
67
+ name: str
68
+ default_value: TProp | None = None # could be used for a "reset"?
69
+ last_value: TProp | None = None # the last value we saw from the device
70
+ limits: tuple[int | float, int | float] | None = None
71
+ sequence_max_length: int = 0
72
+ description: str | None = None
73
+ type: PropertyType = PropertyType.Undef
74
+
75
+ allowed_values: Sequence[TProp] | None = None
76
+ is_read_only: bool | None = None
77
+ is_pre_init: bool = False
78
+
79
+ @property
80
+ def is_sequenceable(self) -> bool:
81
+ """Return True if the property is sequenceable."""
82
+ return self.sequence_max_length > 0
83
+
84
+ def __post_init__(self) -> None:
85
+ """Ensure sound property configuration."""
86
+ if self.allowed_values and self.limits: # pragma: no cover
87
+ raise ValueError(
88
+ f"Property {self.name!r} cannot have both allowed values and limits. "
89
+ "Please choose one or the other."
90
+ )
91
+
92
+ def __setattr__(self, name: str, value: Any) -> None:
93
+ """Perform additional checks when setting attributes."""
94
+ object.__setattr__(self, name, value) # slots dataclass has no super()...
95
+ # setting allowed values also removes the limits
96
+ if name == "allowed_values" and value is not None:
97
+ self.limits = None
98
+
99
+
100
+ class PropertyController(Generic[TDev, TProp]):
101
+ """Controls the state of a property connected to a device.
102
+
103
+ PropertyController instances are descriptors (i.e. they behave like @property),
104
+ that can get and set the value of a property on a Device instance using the
105
+ getter and setter methods provided at initialization. They can also load and
106
+ start sequences of property values, if the property is sequenceable (i.e. it has
107
+ a non-zero sequence_max_length and has been provided with `sequence_loader` and
108
+ `sequence_starter` methods).
109
+
110
+ Device subclasses maintain PropertyController instances in a ClassVar dictionary,
111
+ `_prop_controllers`, where the keys are the property names and the values are the
112
+ PropertyController instances.
113
+ """
114
+
115
+ def __init__(
116
+ self,
117
+ property: PropertyInfo[TProp],
118
+ fget: Callable[[TDev], TProp] | None = None,
119
+ fset: Callable[[TDev, TProp], None] | None = None,
120
+ fseq_load: Callable[[TDev, Sequence[TProp]], None] | None = None,
121
+ fseq_start: Callable[[TDev], None] | None = None,
122
+ fseq_stop: Callable[[TDev], None] | None = None,
123
+ doc: str | None = None,
124
+ ) -> None:
125
+ self.property = property
126
+ self.fget = fget
127
+ self.fset = fset
128
+ self.fseq_load = fseq_load
129
+ self.fseq_start = fseq_start
130
+ self.fseq_stop = fseq_stop
131
+ self.doc = doc
132
+
133
+ # same as "Property::Update" in CMMCore
134
+ @overload
135
+ def __get__(
136
+ self, instance: None, owner: type[TDev]
137
+ ) -> PropertyController[TDev, TProp]: ...
138
+ @overload
139
+ def __get__(self, instance: TDev, owner: type[TDev]) -> TProp: ...
140
+ def __get__(
141
+ self, instance: TDev | None, owner: type[TDev]
142
+ ) -> TProp | PropertyController[TDev, TProp]:
143
+ """Update the property value by calling the getter on the Device instance."""
144
+ if instance is None: # pragma: no cover
145
+ return self
146
+ if self.fget is None: # pragma: no cover
147
+ raise AttributeError("Unreadable property")
148
+ val = self.fget(instance) # cache the value
149
+ object.__setattr__(self.property, "last_value", val)
150
+ return val
151
+
152
+ # same as "Property::Apply" in CMMCore
153
+ def __set__(self, instance: TDev, value: TProp) -> None:
154
+ """Update the property value by calling the setter on the Device instance."""
155
+ if self.fset is None: # pragma: no cover
156
+ raise AttributeError("Unsettable property")
157
+ value = self.validate(value)
158
+ self.fset(instance, value)
159
+
160
+ def validate(self, value: Any) -> TProp:
161
+ """Validate a property value."""
162
+ if self.property.allowed_values and value not in self.property.allowed_values:
163
+ raise ValueError(
164
+ f"Value '{value}' is not allowed for property '{self.property.name}'. "
165
+ f"Allowed values: {list(self.property.allowed_values)}."
166
+ )
167
+ if self.property.limits:
168
+ try:
169
+ value = float(value)
170
+ except (ValueError, TypeError) as e:
171
+ raise ValueError(
172
+ f"Non-numeric value {value!r} cannot be compared to the limits "
173
+ f"of property {self.property.name!r}: {self.property.limits}."
174
+ ) from e
175
+ min_, max_ = self.property.limits
176
+ if not min_ <= cast(float, value) <= max_:
177
+ raise ValueError(
178
+ f"Value {value!r} is not within the allowed range of property "
179
+ f"{self.property.name!r}: {self.property.limits}."
180
+ )
181
+ return cast("TProp", value)
182
+
183
+ @property
184
+ def is_sequenceable(self) -> bool:
185
+ """Return True if the property is sequenceable."""
186
+ return (
187
+ self.property.is_sequenceable
188
+ and self.fseq_load is not None
189
+ and self.fseq_start is not None
190
+ )
191
+
192
+ @property
193
+ def is_read_only(self) -> bool:
194
+ """Return True if the property is read-only.
195
+
196
+ We consider a property read-only either if the device has explicitly set it as
197
+ such, or if the property has a getter but no setter.
198
+ If it has *neither* a getter nor a setter, and is not explicitly marked as
199
+ read-only, it is considered writeable: this is assumed to be a "configuration"
200
+ property that the device adapter cares about, but which is likely never sent
201
+ to the device itself.
202
+ """
203
+ return self.property.is_read_only is True or (
204
+ self.fset is None and self.fget is not None
205
+ )
206
+
207
+ def load_sequence(self, instance: TDev, sequence: Sequence[TProp]) -> None:
208
+ """Send a sequence of property values to the device."""
209
+ if self.fseq_load is None:
210
+ raise RuntimeError(
211
+ f"Property {self.property.name!r} is not sequenceable. "
212
+ "No sequence loader is defined."
213
+ )
214
+ if (seq_len := len(sequence)) > (max_len := self.property.sequence_max_length):
215
+ raise ValueError(
216
+ f"Sequence length {seq_len} exceeds the maximum allowed length "
217
+ f"of property {self.property.name!r}: {max_len}."
218
+ )
219
+ seq = [self.validate(val) for val in sequence]
220
+ self.fseq_load(instance, seq)
221
+
222
+ def start_sequence(self, instance: TDev) -> None:
223
+ """Tell the device to start the previously loaded sequence."""
224
+ if self.fseq_start is None:
225
+ raise RuntimeError(
226
+ f"Property {self.property.name!r} is not sequenceable. "
227
+ "No sequence starter is defined."
228
+ )
229
+ self.fseq_start(instance)
230
+
231
+ def stop_sequence(self, instance: TDev) -> None:
232
+ """Stop the sequence."""
233
+ # it's not an error if there is no stopper
234
+ if self.fseq_stop is not None:
235
+ self.fseq_stop(instance)
236
+
237
+ # ------------------------- Decorators -------------------------
238
+
239
+ def setter(self, fset: Callable[[TDev, TProp], None]) -> Self:
240
+ """Decorate a method to set the property on the device."""
241
+ self.fset = fset
242
+ if self.property.is_read_only is None:
243
+ self.property.is_read_only = False
244
+ return self
245
+
246
+ def sequence_loader(
247
+ self, fseq_load: Callable[[TDev, Sequence[TProp]], None]
248
+ ) -> Self:
249
+ """Decorate a method that sends a property value sequence to the device."""
250
+ self.fseq_load = fseq_load
251
+ return self
252
+
253
+ def sequence_starter(self, fseq_start: Callable[[TDev], None]) -> Self:
254
+ """Decorate a method that starts a (previously loaded) property sequence."""
255
+ self.fseq_start = fseq_start
256
+ return self
257
+
258
+ def sequence_stopper(self, fseq_stop: Callable[[TDev], None]) -> Self:
259
+ """Decorate a method that stops the currently running property sequence."""
260
+ self.fseq_stop = fseq_stop
261
+ return self
262
+
263
+
264
+ @overload # when used as a decorator
265
+ def pymm_property(fget: Callable[[TDev], TProp]) -> PropertyController[TDev, TProp]: ...
266
+ @overload # when used with keyword arguments including allowed_values
267
+ def pymm_property(
268
+ *,
269
+ allowed_values: Sequence[TProp] | None, # cannot be combined with limits
270
+ sequence_max_length: int = ...,
271
+ is_read_only: bool | None = ...,
272
+ is_pre_init: bool = ...,
273
+ name: str | None = ...,
274
+ property_type: PropArg = ...,
275
+ ) -> Callable[[Callable[[TDev], TProp]], PropertyController[TDev, TProp]]: ...
276
+ @overload # when used with keyword arguments including limits
277
+ def pymm_property(
278
+ *,
279
+ limits: tuple[TLim, TLim] | None, # cannot be combined with allowed_values
280
+ sequence_max_length: int = ...,
281
+ is_read_only: bool | None = ...,
282
+ is_pre_init: bool = ...,
283
+ name: str | None = ...,
284
+ property_type: PropArg = ...,
285
+ ) -> Callable[[Callable[[TDev], TLim]], PropertyController[TDev, TLim]]: ...
286
+ @overload # when used with keyword arguments without allowed_values or limits
287
+ def pymm_property(
288
+ *,
289
+ sequence_max_length: int = ...,
290
+ is_read_only: bool | None = ...,
291
+ is_pre_init: bool = ...,
292
+ name: str | None = ...,
293
+ property_type: PropArg = ...,
294
+ ) -> Callable[[Callable[[TDev], TLim]], PropertyController[TDev, TLim]]: ...
295
+ def pymm_property(
296
+ fget: Callable[[TDev], TProp] | None = None,
297
+ *,
298
+ limits: tuple[TLim, TLim] | None = None,
299
+ sequence_max_length: int = 0,
300
+ allowed_values: Sequence[TProp] | None = None,
301
+ is_read_only: bool | None = None,
302
+ is_pre_init: bool = False,
303
+ name: str | None = None, # taken from fget if None
304
+ property_type: PropArg = None,
305
+ ) -> (
306
+ PropertyController[TDev, TProp]
307
+ | Callable[[Callable[[TDev], TProp]], PropertyController[TDev, TProp]]
308
+ ):
309
+ """Decorates a (getter) method to create a device property.
310
+
311
+ The returned PropertyController instance can be additionally used (similar to
312
+ `@property`) to decorate `setter`, `sequence_loader`, `sequence_starter`, and/or
313
+ `sequence_stopper` methods.
314
+
315
+ Properties can have limits, allowed values, but may not have both.
316
+
317
+ Properties will only be considered "sequenceable" (i.e. they support hardware
318
+ triggering) if they have a non-zero sequence_max_length AND have decorated
319
+ `sequence_loader` and `sequence_starter` methods.
320
+
321
+ Parameters
322
+ ----------
323
+ fget : Callable[[TDev], TProp], optional
324
+ The getter method for the property, by default None.
325
+ limits : tuple[float, float], optional
326
+ The minimum and maximum values of the property, by default None. Cannot be
327
+ combined with `allowed_values`.
328
+ sequence_max_length : int, optional
329
+ The maximum length of a sequence of property values, by default 0.
330
+ allowed_values : Sequence[TProp], optional
331
+ The allowed values of the property, by default None. Cannot be combined with
332
+ `limits`.
333
+ is_read_only : bool, optional
334
+ Whether the property is read-only, by default False.
335
+ is_pre_init : bool, optional
336
+ Whether the property must be set before initialization, by default False.
337
+ name : str, optional
338
+ The name of the property, by default, the name of the getter method is used.
339
+ prop_type : PropArg, optional
340
+ The type of the property, by default the return annotation of the getter method
341
+ is used (but must be one of `float`, `int`, or `str`).
342
+
343
+
344
+ Examples
345
+ --------
346
+ ```python
347
+ class MyDevice(Device):
348
+ @pymm_property(limits=(0, 100), sequence_max_length=10)
349
+ def position(self) -> float:
350
+ # get position from device
351
+ return 42.0
352
+
353
+ @position.setter
354
+ def position(self, value: float) -> None:
355
+ print(f"Setting position to {value}")
356
+
357
+ @position.sequence_loader
358
+ def load_position_sequence(self, sequence: Sequence[float]) -> None:
359
+ print(f"Loading position sequence: {sequence}")
360
+
361
+ @position.sequence_starter
362
+ def start_position_sequence(self) -> None:
363
+ print("Starting position sequence")
364
+
365
+ @pymm_property(is_read_only=True)
366
+ def pressure(self) -> float:
367
+ return 1.0
368
+
369
+ @pymm_property(allowed_values=["low", "medium", "high"])
370
+ def speed(self) -> str:
371
+ # get speed from device
372
+ return "medium"
373
+
374
+ @speed.setter
375
+ def speed(self, value: str) -> None:
376
+ print(f"Setting speed to {value}")
377
+ ```
378
+ """
379
+
380
+ def _inner(
381
+ fget: Callable[[TDev], TProp], _pt: PropArg = property_type
382
+ ) -> PropertyController[TDev, TProp]:
383
+ prop = PropertyInfo(
384
+ name=name or fget.__name__,
385
+ description=fget.__doc__,
386
+ limits=limits,
387
+ sequence_max_length=sequence_max_length,
388
+ allowed_values=allowed_values,
389
+ # all @pymm_property properties are read-only by default
390
+ # until they are decorated with a setter
391
+ # this does not apply to properties that are manually registered
392
+ # with Device.register_property.
393
+ is_read_only=is_read_only,
394
+ is_pre_init=is_pre_init,
395
+ type=PropertyType.create(_pt or fget.__annotations__.get("return", None)),
396
+ )
397
+
398
+ return PropertyController(property=prop, fget=fget)
399
+
400
+ return _inner if fget is None else _inner(fget)