ophyd-async 0.3.1a1__py3-none-any.whl → 0.3.3__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.
ophyd_async/_version.py CHANGED
@@ -12,5 +12,5 @@ __version__: str
12
12
  __version_tuple__: VERSION_TUPLE
13
13
  version_tuple: VERSION_TUPLE
14
14
 
15
- __version__ = version = '0.3.1a1'
16
- __version_tuple__ = version_tuple = (0, 3, 1)
15
+ __version__ = version = '0.3.3'
16
+ __version_tuple__ = version_tuple = (0, 3, 3)
@@ -50,7 +50,7 @@ from .signal import (
50
50
  soft_signal_rw,
51
51
  wait_for_value,
52
52
  )
53
- from .signal_backend import SignalBackend
53
+ from .signal_backend import RuntimeSubsetEnum, SignalBackend, SubsetEnum
54
54
  from .soft_signal_backend import SoftSignalBackend
55
55
  from .standard_readable import ConfigSignal, HintedSignal, StandardReadable
56
56
  from .utils import (
@@ -68,66 +68,69 @@ from .utils import (
68
68
  )
69
69
 
70
70
  __all__ = [
71
- "get_mock_put",
72
- "callback_on_mock_put",
73
- "mock_puts_blocked",
74
- "set_mock_values",
75
- "reset_mock_put_calls",
76
- "SignalBackend",
77
- "SoftSignalBackend",
71
+ "AsyncStatus",
72
+ "CalculatableTimeout",
73
+ "CalculateTimeout",
74
+ "Callback",
75
+ "ConfigSignal",
76
+ "DEFAULT_TIMEOUT",
78
77
  "DetectorControl",
79
- "MockSignalBackend",
80
78
  "DetectorTrigger",
81
79
  "DetectorWriter",
82
- "StandardDetector",
83
80
  "Device",
84
81
  "DeviceCollector",
85
82
  "DeviceVector",
86
- "Signal",
87
- "SignalR",
88
- "SignalW",
89
- "SignalRW",
90
- "SignalX",
91
- "soft_signal_r_and_setter",
92
- "soft_signal_rw",
93
- "observe_value",
94
- "set_and_wait_for_value",
95
- "set_mock_put_proceeds",
96
- "set_mock_value",
97
- "wait_for_value",
98
- "AsyncStatus",
99
- "WatchableAsyncStatus",
100
83
  "DirectoryInfo",
101
84
  "DirectoryProvider",
85
+ "HardwareTriggeredFlyable",
86
+ "HintedSignal",
87
+ "MockSignalBackend",
102
88
  "NameProvider",
89
+ "NotConnected",
90
+ "ReadingValueCallback",
91
+ "RuntimeSubsetEnum",
92
+ "SubsetEnum",
103
93
  "ShapeProvider",
104
- "StaticDirectoryProvider",
94
+ "Signal",
95
+ "SignalBackend",
96
+ "SignalR",
97
+ "SignalRW",
98
+ "SignalW",
99
+ "SignalX",
100
+ "SoftSignalBackend",
101
+ "StandardDetector",
105
102
  "StandardReadable",
106
- "ConfigSignal",
107
- "HintedSignal",
103
+ "StaticDirectoryProvider",
104
+ "T",
108
105
  "TriggerInfo",
109
106
  "TriggerLogic",
110
- "HardwareTriggeredFlyable",
111
- "CalculateTimeout",
112
- "CalculatableTimeout",
113
- "DEFAULT_TIMEOUT",
114
- "Callback",
115
- "NotConnected",
116
- "ReadingValueCallback",
117
- "T",
107
+ "WatchableAsyncStatus",
108
+ "assert_configuration",
109
+ "assert_emitted",
110
+ "assert_mock_put_called_with",
111
+ "assert_reading",
112
+ "assert_value",
113
+ "callback_on_mock_put",
118
114
  "get_dtype",
119
- "get_unique",
120
- "merge_gathered_dicts",
121
- "wait_for_connection",
115
+ "get_mock_put",
122
116
  "get_signal_values",
117
+ "get_unique",
118
+ "load_device",
123
119
  "load_from_yaml",
120
+ "merge_gathered_dicts",
121
+ "mock_puts_blocked",
122
+ "observe_value",
123
+ "reset_mock_put_calls",
124
+ "save_device",
124
125
  "save_to_yaml",
126
+ "set_and_wait_for_value",
127
+ "set_mock_put_proceeds",
128
+ "set_mock_value",
129
+ "set_mock_values",
125
130
  "set_signal_values",
131
+ "soft_signal_r_and_setter",
132
+ "soft_signal_rw",
133
+ "wait_for_connection",
134
+ "wait_for_value",
126
135
  "walk_rw_signals",
127
- "load_device",
128
- "save_device",
129
- "assert_reading",
130
- "assert_value",
131
- "assert_configuration",
132
- "assert_emitted",
133
136
  ]
@@ -1,6 +1,5 @@
1
1
  from enum import Enum
2
- from functools import partial
3
- from typing import Any, Callable, Dict, Generator, List, Optional, Sequence, Union
2
+ from typing import Any, Callable, Dict, Generator, List, Optional, Sequence
4
3
 
5
4
  import numpy as np
6
5
  import numpy.typing as npt
@@ -8,13 +7,10 @@ import yaml
8
7
  from bluesky.plan_stubs import abs_set, wait
9
8
  from bluesky.protocols import Location
10
9
  from bluesky.utils import Msg
11
- from epicscorelibs.ca.dbr import ca_array, ca_float, ca_int, ca_str
12
10
 
13
11
  from .device import Device
14
12
  from .signal import SignalRW
15
13
 
16
- CaType = Union[ca_float, ca_int, ca_str, ca_array]
17
-
18
14
 
19
15
  def ndarray_representer(dumper: yaml.Dumper, array: npt.NDArray[Any]) -> yaml.Node:
20
16
  return dumper.represent_sequence(
@@ -22,19 +18,6 @@ def ndarray_representer(dumper: yaml.Dumper, array: npt.NDArray[Any]) -> yaml.No
22
18
  )
23
19
 
24
20
 
25
- def ca_dbr_representer(dumper: yaml.Dumper, value: CaType) -> yaml.Node:
26
- # if it's an array, just call ndarray_representer...
27
- represent_array = partial(ndarray_representer, dumper)
28
-
29
- representers: Dict[CaType, Callable[[CaType], yaml.Node]] = {
30
- ca_float: dumper.represent_float,
31
- ca_int: dumper.represent_int,
32
- ca_str: dumper.represent_str,
33
- ca_array: represent_array,
34
- }
35
- return representers[type(value)](value)
36
-
37
-
38
21
  class OphydDumper(yaml.Dumper):
39
22
  def represent_data(self, data: Any) -> Any:
40
23
  if isinstance(data, Enum):
@@ -152,11 +135,6 @@ def save_to_yaml(phases: Sequence[Dict[str, Any]], save_path: str) -> None:
152
135
 
153
136
  yaml.add_representer(np.ndarray, ndarray_representer, Dumper=yaml.Dumper)
154
137
 
155
- yaml.add_representer(ca_float, ca_dbr_representer, Dumper=yaml.Dumper)
156
- yaml.add_representer(ca_int, ca_dbr_representer, Dumper=yaml.Dumper)
157
- yaml.add_representer(ca_str, ca_dbr_representer, Dumper=yaml.Dumper)
158
- yaml.add_representer(ca_array, ca_dbr_representer, Dumper=yaml.Dumper)
159
-
160
138
  with open(save_path, "w") as file:
161
139
  yaml.dump(phases, file, Dumper=OphydDumper, default_flow_style=False)
162
140
 
@@ -31,7 +31,7 @@ from ophyd_async.protocols import AsyncConfigurable, AsyncReadable, AsyncStageab
31
31
  from .async_status import AsyncStatus
32
32
  from .device import Device
33
33
  from .signal_backend import SignalBackend
34
- from .soft_signal_backend import SoftSignalBackend
34
+ from .soft_signal_backend import SignalMetadata, SoftSignalBackend
35
35
  from .utils import DEFAULT_TIMEOUT, CalculatableTimeout, CalculateTimeout, Callback, T
36
36
 
37
37
 
@@ -57,7 +57,7 @@ class Signal(Device, Generic[T]):
57
57
 
58
58
  def __init__(
59
59
  self,
60
- backend: SignalBackend[T],
60
+ backend: Optional[SignalBackend[T]] = None,
61
61
  timeout: Optional[float] = DEFAULT_TIMEOUT,
62
62
  name: str = "",
63
63
  ) -> None:
@@ -66,13 +66,24 @@ class Signal(Device, Generic[T]):
66
66
  super().__init__(name)
67
67
 
68
68
  async def connect(
69
- self, mock=False, timeout=DEFAULT_TIMEOUT, force_reconnect: bool = False
69
+ self,
70
+ mock=False,
71
+ timeout=DEFAULT_TIMEOUT,
72
+ force_reconnect: bool = False,
73
+ backend: Optional[SignalBackend[T]] = None,
70
74
  ):
75
+ if backend:
76
+ if self._initial_backend and backend is not self._initial_backend:
77
+ raise ValueError(
78
+ "Backend at connection different from initialised one."
79
+ )
80
+ self._backend = backend
71
81
  if mock and not isinstance(self._backend, MockSignalBackend):
72
82
  # Using a soft backend, look to the initial value
73
- self._backend = MockSignalBackend(
74
- initial_backend=self._initial_backend,
75
- )
83
+ self._backend = MockSignalBackend(initial_backend=self._backend)
84
+
85
+ if self._backend is None:
86
+ raise RuntimeError("`connect` called on signal without backend")
76
87
  self.log.debug(f"Connecting to {self.source}")
77
88
  await self._backend.connect(timeout=timeout)
78
89
 
@@ -261,9 +272,17 @@ def soft_signal_rw(
261
272
  datatype: Optional[Type[T]] = None,
262
273
  initial_value: Optional[T] = None,
263
274
  name: str = "",
275
+ units: str | None = None,
276
+ precision: int | None = None,
264
277
  ) -> SignalRW[T]:
265
- """Creates a read-writable Signal with a SoftSignalBackend"""
266
- signal = SignalRW(SoftSignalBackend(datatype, initial_value), name=name)
278
+ """Creates a read-writable Signal with a SoftSignalBackend.
279
+ May pass metadata, which are propagated into describe.
280
+ """
281
+ metadata = SignalMetadata(units=units, precision=precision)
282
+ signal = SignalRW(
283
+ SoftSignalBackend(datatype, initial_value, metadata=metadata),
284
+ name=name,
285
+ )
267
286
  return signal
268
287
 
269
288
 
@@ -271,27 +290,31 @@ def soft_signal_r_and_setter(
271
290
  datatype: Optional[Type[T]] = None,
272
291
  initial_value: Optional[T] = None,
273
292
  name: str = "",
293
+ units: str | None = None,
294
+ precision: int | None = None,
274
295
  ) -> Tuple[SignalR[T], Callable[[T], None]]:
275
296
  """Returns a tuple of a read-only Signal and a callable through
276
- which the signal can be internally modified within the device. Use
277
- soft_signal_rw if you want a device that is externally modifiable
297
+ which the signal can be internally modified within the device.
298
+ May pass metadata, which are propagated into describe.
299
+ Use soft_signal_rw if you want a device that is externally modifiable
278
300
  """
279
- backend = SoftSignalBackend(datatype, initial_value)
301
+ metadata = SignalMetadata(units=units, precision=precision)
302
+ backend = SoftSignalBackend(datatype, initial_value, metadata=metadata)
280
303
  signal = SignalR(backend, name=name)
281
304
 
282
305
  return (signal, backend.set_value)
283
306
 
284
307
 
285
308
  def _generate_assert_error_msg(
286
- name: str, expected_result: str, actuall_result: str
309
+ name: str, expected_result: str, actual_result: str
287
310
  ) -> str:
288
311
  WARNING = "\033[93m"
289
312
  FAIL = "\033[91m"
290
313
  ENDC = "\033[0m"
291
314
  return (
292
315
  f"Expected {WARNING}{name}{ENDC} to produce"
293
- + f"\n{FAIL}{actuall_result}{ENDC}"
294
- + f"\nbut actually got \n{FAIL}{expected_result}{ENDC}"
316
+ + f"\n{FAIL}{expected_result}{ENDC}"
317
+ + f"\nbut actually got \n{FAIL}{actual_result}{ENDC}"
295
318
  )
296
319
 
297
320
 
@@ -313,7 +336,9 @@ async def assert_value(signal: SignalR[T], value: Any) -> None:
313
336
  """
314
337
  actual_value = await signal.get_value()
315
338
  assert actual_value == value, _generate_assert_error_msg(
316
- signal.name, value, actual_value
339
+ name=signal.name,
340
+ expected_result=value,
341
+ actual_result=actual_value,
317
342
  )
318
343
 
319
344
 
@@ -338,7 +363,9 @@ async def assert_reading(
338
363
  """
339
364
  actual_reading = await readable.read()
340
365
  assert expected_reading == actual_reading, _generate_assert_error_msg(
341
- readable.name, expected_reading, actual_reading
366
+ name=readable.name,
367
+ expected_result=expected_reading,
368
+ actual_result=actual_reading,
342
369
  )
343
370
 
344
371
 
@@ -364,7 +391,9 @@ async def assert_configuration(
364
391
  """
365
392
  actual_configurable = await configurable.read_configuration()
366
393
  assert configuration == actual_configurable, _generate_assert_error_msg(
367
- configurable.name, configuration, actual_configurable
394
+ name=configurable.name,
395
+ expected_result=configuration,
396
+ actual_result=actual_configurable,
368
397
  )
369
398
 
370
399
 
@@ -386,11 +415,15 @@ def assert_emitted(docs: Mapping[str, list[dict]], **numbers: int):
386
415
  resource=1, datum=1, event=1, stop=1)
387
416
  """
388
417
  assert list(docs) == list(numbers), _generate_assert_error_msg(
389
- "documents", list(numbers), list(docs)
418
+ name="documents",
419
+ expected_result=list(numbers),
420
+ actual_result=list(docs),
390
421
  )
391
422
  actual_numbers = {name: len(d) for name, d in docs.items()}
392
423
  assert actual_numbers == numbers, _generate_assert_error_msg(
393
- "emitted", numbers, actual_numbers
424
+ name="emitted",
425
+ expected_result=numbers,
426
+ actual_result=actual_numbers,
394
427
  )
395
428
 
396
429
 
@@ -1,5 +1,13 @@
1
1
  from abc import abstractmethod
2
- from typing import Generic, Optional, Type
2
+ from typing import (
3
+ TYPE_CHECKING,
4
+ ClassVar,
5
+ Generic,
6
+ Literal,
7
+ Optional,
8
+ Tuple,
9
+ Type,
10
+ )
3
11
 
4
12
  from bluesky.protocols import DataKey, Reading
5
13
 
@@ -45,3 +53,41 @@ class SignalBackend(Generic[T]):
45
53
  @abstractmethod
46
54
  def set_callback(self, callback: Optional[ReadingValueCallback[T]]) -> None:
47
55
  """Observe changes to the current value, timestamp and severity"""
56
+
57
+
58
+ class _RuntimeSubsetEnumMeta(type):
59
+ def __str__(cls):
60
+ if hasattr(cls, "choices"):
61
+ return f"SubsetEnum{list(cls.choices)}"
62
+ return "SubsetEnum"
63
+
64
+ def __getitem__(cls, _choices):
65
+ if isinstance(_choices, str):
66
+ _choices = (_choices,)
67
+ else:
68
+ if not isinstance(_choices, tuple) or not all(
69
+ isinstance(c, str) for c in _choices
70
+ ):
71
+ raise TypeError(
72
+ "Choices must be a str or a tuple of str, " f"not {type(_choices)}."
73
+ )
74
+ if len(set(_choices)) != len(_choices):
75
+ raise TypeError("Duplicate elements in runtime enum choices.")
76
+
77
+ class _RuntimeSubsetEnum(cls):
78
+ choices = _choices
79
+
80
+ return _RuntimeSubsetEnum
81
+
82
+
83
+ class RuntimeSubsetEnum(metaclass=_RuntimeSubsetEnumMeta):
84
+ choices: ClassVar[Tuple[str, ...]]
85
+
86
+ def __init__(self):
87
+ raise RuntimeError("SubsetEnum cannot be instantiated")
88
+
89
+
90
+ if TYPE_CHECKING:
91
+ SubsetEnum = Literal
92
+ else:
93
+ SubsetEnum = RuntimeSubsetEnum
@@ -3,14 +3,23 @@ from __future__ import annotations
3
3
  import inspect
4
4
  import time
5
5
  from collections import abc
6
- from dataclasses import dataclass
7
6
  from enum import Enum
8
- from typing import Dict, Generic, Optional, Type, Union, cast, get_origin
7
+ from typing import (
8
+ Dict,
9
+ Generic,
10
+ Optional,
11
+ Tuple,
12
+ Type,
13
+ TypedDict,
14
+ Union,
15
+ cast,
16
+ get_origin,
17
+ )
9
18
 
10
19
  import numpy as np
11
20
  from bluesky.protocols import DataKey, Dtype, Reading
12
21
 
13
- from .signal_backend import SignalBackend
22
+ from .signal_backend import RuntimeSubsetEnum, SignalBackend
14
23
  from .utils import DEFAULT_TIMEOUT, ReadingValueCallback, T, get_dtype
15
24
 
16
25
  primitive_dtypes: Dict[type, Dtype] = {
@@ -21,6 +30,11 @@ primitive_dtypes: Dict[type, Dtype] = {
21
30
  }
22
31
 
23
32
 
33
+ class SignalMetadata(TypedDict):
34
+ units: str | None = None
35
+ precision: int | None = None
36
+
37
+
24
38
  class SoftConverter(Generic[T]):
25
39
  def value(self, value: T) -> T:
26
40
  return value
@@ -35,7 +49,8 @@ class SoftConverter(Generic[T]):
35
49
  alarm_severity=-1 if severity > 2 else severity,
36
50
  )
37
51
 
38
- def get_datakey(self, source: str, value) -> DataKey:
52
+ def get_datakey(self, source: str, value, **metadata) -> DataKey:
53
+ dk = {"source": source, "shape": [], **metadata}
39
54
  dtype = type(value)
40
55
  if np.issubdtype(dtype, np.integer):
41
56
  dtype = int
@@ -44,8 +59,8 @@ class SoftConverter(Generic[T]):
44
59
  assert (
45
60
  dtype in primitive_dtypes
46
61
  ), f"invalid converter for value of type {type(value)}"
47
- dtype_name = primitive_dtypes[dtype]
48
- return {"source": source, "dtype": dtype_name, "shape": []}
62
+ dk["dtype"] = primitive_dtypes[dtype]
63
+ return dk
49
64
 
50
65
  def make_initial_value(self, datatype: Optional[Type[T]]) -> T:
51
66
  if datatype is None:
@@ -55,8 +70,8 @@ class SoftConverter(Generic[T]):
55
70
 
56
71
 
57
72
  class SoftArrayConverter(SoftConverter):
58
- def get_datakey(self, source: str, value) -> DataKey:
59
- return {"source": source, "dtype": "array", "shape": [len(value)]}
73
+ def get_datakey(self, source: str, value, **metadata) -> DataKey:
74
+ return {"source": source, "dtype": "array", "shape": [len(value)], **metadata}
60
75
 
61
76
  def make_initial_value(self, datatype: Optional[Type[T]]) -> T:
62
77
  if datatype is None:
@@ -68,31 +83,45 @@ class SoftArrayConverter(SoftConverter):
68
83
  return cast(T, datatype(shape=0)) # type: ignore
69
84
 
70
85
 
71
- @dataclass
72
86
  class SoftEnumConverter(SoftConverter):
73
- enum_class: Type[Enum]
87
+ choices: Tuple[str, ...]
74
88
 
75
- def write_value(self, value: Union[Enum, str]) -> Enum:
89
+ def __init__(self, datatype: Union[RuntimeSubsetEnum, Enum]):
90
+ if issubclass(datatype, Enum):
91
+ self.choices = tuple(v.value for v in datatype)
92
+ else:
93
+ self.choices = datatype.choices
94
+
95
+ def write_value(self, value: Union[Enum, str]) -> str:
76
96
  if isinstance(value, Enum):
97
+ return value.value
98
+ else: # Runtime enum
77
99
  return value
78
- else:
79
- return self.enum_class(value)
80
100
 
81
- def get_datakey(self, source: str, value) -> DataKey:
82
- choices = [e.value for e in self.enum_class]
83
- return {"source": source, "dtype": "string", "shape": [], "choices": choices} # type: ignore
101
+ def get_datakey(self, source: str, value, **metadata) -> DataKey:
102
+ return {
103
+ "source": source,
104
+ "dtype": "string",
105
+ "shape": [],
106
+ "choices": self.choices,
107
+ **metadata,
108
+ }
84
109
 
85
110
  def make_initial_value(self, datatype: Optional[Type[T]]) -> T:
86
111
  if datatype is None:
87
112
  return cast(T, None)
88
113
 
89
- return cast(T, list(datatype.__members__.values())[0]) # type: ignore
114
+ if issubclass(datatype, Enum):
115
+ return cast(T, list(datatype.__members__.values())[0]) # type: ignore
116
+ return cast(T, self.choices[0])
90
117
 
91
118
 
92
119
  def make_converter(datatype):
93
120
  is_array = get_dtype(datatype) is not None
94
121
  is_sequence = get_origin(datatype) == abc.Sequence
95
- is_enum = issubclass(datatype, Enum) if inspect.isclass(datatype) else False
122
+ is_enum = inspect.isclass(datatype) and (
123
+ issubclass(datatype, Enum) or issubclass(datatype, RuntimeSubsetEnum)
124
+ )
96
125
 
97
126
  if is_array or is_sequence:
98
127
  return SoftArrayConverter()
@@ -114,9 +143,11 @@ class SoftSignalBackend(SignalBackend[T]):
114
143
  self,
115
144
  datatype: Optional[Type[T]],
116
145
  initial_value: Optional[T] = None,
146
+ metadata: SignalMetadata = None,
117
147
  ) -> None:
118
148
  self.datatype = datatype
119
149
  self._initial_value = initial_value
150
+ self._metadata = metadata or {}
120
151
  self.converter: SoftConverter = make_converter(datatype)
121
152
  if self._initial_value is None:
122
153
  self._initial_value = self.converter.make_initial_value(self.datatype)
@@ -155,7 +186,7 @@ class SoftSignalBackend(SignalBackend[T]):
155
186
  self.callback(reading, self._value)
156
187
 
157
188
  async def get_datakey(self, source: str) -> DataKey:
158
- return self.converter.get_datakey(source, self._value)
189
+ return self.converter.get_datakey(source, self._value, **self._metadata)
159
190
 
160
191
  async def get_reading(self) -> Reading:
161
192
  return self.converter.reading(self._value, self._timestamp, self._severity)