ophyd-async 0.14.1__py3-none-any.whl → 0.14.2__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
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '0.14.1'
32
- __version_tuple__ = version_tuple = (0, 14, 1)
31
+ __version__ = version = '0.14.2'
32
+ __version_tuple__ = version_tuple = (0, 14, 2)
33
33
 
34
34
  __commit_id__ = commit_id = None
@@ -1,5 +1,15 @@
1
- from collections.abc import Awaitable, Callable
2
- from typing import Any, Generic, get_args, get_origin, get_type_hints, is_typeddict
1
+ import functools
2
+ from collections.abc import Awaitable, Callable, Mapping
3
+ from inspect import Parameter, signature
4
+ from typing import (
5
+ Any,
6
+ Generic,
7
+ TypeVar,
8
+ get_args,
9
+ get_origin,
10
+ get_type_hints,
11
+ is_typeddict,
12
+ )
3
13
 
4
14
  from bluesky.protocols import Locatable
5
15
 
@@ -53,12 +63,13 @@ class DerivedSignalFactory(Generic[TransformT]):
53
63
  # Populate expected parameters and types
54
64
  expected = {
55
65
  **{k: f.annotation for k, f in transform_cls.model_fields.items()},
56
- **{
57
- k: v
58
- for k, v in get_type_hints(transform_cls.raw_to_derived).items()
59
- if k not in {"self", "return"}
60
- },
66
+ **_get_params_types_dict(transform_cls.raw_to_derived),
61
67
  }
68
+ if empty_keys := [k for k, v in expected.items() if v == Parameter.empty]:
69
+ raise TypeError(
70
+ f"{transform_cls.raw_to_derived} is missing a type "
71
+ f"hint for arguments: {empty_keys}"
72
+ )
62
73
 
63
74
  # Populate received parameters and types
64
75
  # Use Primitive's type, Signal's datatype,
@@ -76,7 +87,19 @@ class DerivedSignalFactory(Generic[TransformT]):
76
87
  f"Expected the following to be passed as keyword arguments "
77
88
  f"{expected}, got {received}"
78
89
  )
79
- raise TypeError(msg)
90
+ if set(expected.keys()) - set(received.keys()):
91
+ raise TypeError(msg)
92
+
93
+ for k in set(expected.keys()):
94
+ if isinstance(expected[k], type):
95
+ if not issubclass(received[k], expected[k]):
96
+ raise TypeError(msg)
97
+ elif isinstance(expected[k], TypeVar):
98
+ bound = expected[k].__bound__
99
+ if isinstance(bound, type) and not issubclass(
100
+ received[k], bound
101
+ ):
102
+ raise TypeError(msg)
80
103
  self._set_derived_takes_dict = (
81
104
  is_typeddict(_get_first_arg_datatype(set_derived)) if set_derived else False
82
105
  )
@@ -195,28 +218,28 @@ def _get_return_datatype(func: Callable[..., SignalDatatypeT]) -> type[SignalDat
195
218
  def _get_first_arg_datatype(
196
219
  func: Callable[[SignalDatatypeT], Any],
197
220
  ) -> type[SignalDatatypeT]:
198
- args = get_type_hints(func)
199
- args.pop("return", None)
221
+ args = _get_params_types_dict(func)
200
222
  if not args:
201
223
  msg = f"{func} does not have a type hinted argument"
202
224
  raise TypeError(msg)
203
225
  return list(args.values())[0]
204
226
 
205
227
 
228
+ def _get_params_types_dict(inspected_function: Callable) -> Mapping[str, Any]:
229
+ sig = signature(inspected_function, eval_str=True)
230
+ exclude_keys = {"self", "args", "kwargs", "cls"}
231
+ return {k: v.annotation for k, v in sig.parameters.items() if k not in exclude_keys}
232
+
233
+
206
234
  def _make_factory(
207
- raw_to_derived: Callable[..., SignalDatatypeT] | None = None,
235
+ raw_to_derived_func: Callable[..., SignalDatatypeT] | None = None,
208
236
  set_derived: Callable[[SignalDatatypeT], Awaitable[None]] | None = None,
209
237
  raw_devices_and_constants: dict[str, Device | Primitive] | None = None,
210
238
  ) -> DerivedSignalFactory:
211
- if raw_to_derived:
239
+ if raw_to_derived_func:
212
240
 
213
241
  class DerivedTransform(Transform):
214
- def raw_to_derived(self, **kwargs) -> dict[str, SignalDatatypeT]:
215
- return {"value": raw_to_derived(**kwargs)}
216
-
217
- # Update the signature for raw_to_derived to match what we are passed as this
218
- # will be checked in DerivedSignalFactory
219
- DerivedTransform.raw_to_derived.__annotations__ = get_type_hints(raw_to_derived)
242
+ raw_to_derived = _dict_wrapper(raw_to_derived_func)
220
243
 
221
244
  return DerivedSignalFactory(
222
245
  DerivedTransform,
@@ -245,7 +268,7 @@ def derived_signal_r(
245
268
  The names of these arguments must match the arguments of raw_to_derived.
246
269
  """
247
270
  factory = _make_factory(
248
- raw_to_derived=raw_to_derived,
271
+ raw_to_derived_func=raw_to_derived,
249
272
  raw_devices_and_constants=raw_devices_and_constants,
250
273
  )
251
274
  return factory.derived_signal_r(
@@ -278,16 +301,16 @@ def derived_signal_rw(
278
301
  The names of these arguments must match the arguments of raw_to_derived.
279
302
  """
280
303
  raw_to_derived_datatype = _get_return_datatype(raw_to_derived)
281
- set_derived_datatype = _get_first_arg_datatype(set_derived)
282
- if raw_to_derived_datatype != set_derived_datatype:
304
+ set_derived_arg_datatype = _get_first_arg_datatype(set_derived)
305
+ if raw_to_derived_datatype != set_derived_arg_datatype:
283
306
  msg = (
284
307
  f"{raw_to_derived} has datatype {raw_to_derived_datatype} "
285
- f"!= {set_derived_datatype} datatype {set_derived_datatype}"
308
+ f"!= {set_derived_arg_datatype} datatype {set_derived_arg_datatype}"
286
309
  )
287
310
  raise TypeError(msg)
288
311
 
289
312
  factory = _make_factory(
290
- raw_to_derived=raw_to_derived,
313
+ raw_to_derived_func=raw_to_derived,
291
314
  set_derived=set_derived,
292
315
  raw_devices_and_constants=raw_devices_and_constants,
293
316
  )
@@ -343,3 +366,13 @@ def _partition_by_keys(data: dict, keys: set) -> tuple[dict, dict]:
343
366
  else:
344
367
  group_excluded[k] = v
345
368
  return group_excluded, group_included
369
+
370
+
371
+ def _dict_wrapper(
372
+ fn: Callable[..., SignalDatatypeT],
373
+ ) -> Callable[..., dict[str, SignalDatatypeT]]:
374
+ @functools.wraps(fn)
375
+ def wrapped(self, **kwargs):
376
+ return {"value": fn(**kwargs)}
377
+
378
+ return wrapped
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import types
3
4
  from abc import abstractmethod
4
5
  from collections.abc import Callable, Iterator, Sequence
5
6
  from typing import (
@@ -9,8 +10,10 @@ from typing import (
9
10
  NoReturn,
10
11
  Protocol,
11
12
  TypeVar,
13
+ Union,
12
14
  cast,
13
15
  get_args,
16
+ get_origin,
14
17
  get_type_hints,
15
18
  runtime_checkable,
16
19
  )
@@ -76,6 +79,7 @@ class DeviceFiller(Generic[SignalBackendT, DeviceConnectorT]):
76
79
  self._extras: dict[UniqueName, Sequence[Any]] = {}
77
80
  self._signal_datatype: dict[LogicalName, type | None] = {}
78
81
  self._vector_device_type: dict[LogicalName, type[Device] | None] = {}
82
+ self._optional_devices: set[str] = set()
79
83
  self.ignored_signals: set[str] = set()
80
84
  # Backends and Connectors stored ready for the connection phase
81
85
  self._unfilled_backends: dict[
@@ -121,6 +125,20 @@ class DeviceFiller(Generic[SignalBackendT, DeviceConnectorT]):
121
125
  self.ignored_signals.add(attr_name)
122
126
  name = UniqueName(attr_name)
123
127
  origin = get_origin_class(annotation)
128
+ args = get_args(annotation)
129
+
130
+ if (
131
+ get_origin(annotation) is Union
132
+ and types.NoneType in args
133
+ and len(args) == 2
134
+ ):
135
+ # Annotation is an Union with two arguments, one of which is None
136
+ # Make this signal an optional parameter and set origin to T
137
+ # so the device is added to unfilled_connectors
138
+ self._optional_devices.add(name)
139
+ (annotation,) = [x for x in args if x is not types.NoneType]
140
+ origin = get_origin_class(annotation)
141
+
124
142
  if (
125
143
  name == "parent"
126
144
  or name.startswith("_")
@@ -241,10 +259,17 @@ class DeviceFiller(Generic[SignalBackendT, DeviceConnectorT]):
241
259
  :param source: The source of the data that should have done the filling, for
242
260
  reporting as an error message
243
261
  """
244
- unfilled = sorted(set(self._unfilled_connectors).union(self._unfilled_backends))
245
- if unfilled:
262
+ unfilled = set(self._unfilled_connectors).union(self._unfilled_backends)
263
+ unfilled_optional = sorted(unfilled.intersection(self._optional_devices))
264
+
265
+ for name in unfilled_optional:
266
+ setattr(self._device, name, None)
267
+
268
+ required = sorted(unfilled.difference(unfilled_optional))
269
+
270
+ if required:
246
271
  raise RuntimeError(
247
- f"{self._device.name}: cannot provision {unfilled} from {source}"
272
+ f"{self._device.name}: cannot provision {required} from {source}"
248
273
  )
249
274
 
250
275
  def _ensure_device_vector(self, name: LogicalName) -> DeviceVector:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ophyd-async
3
- Version: 0.14.1
3
+ Version: 0.14.2
4
4
  Summary: Asynchronous Bluesky hardware abstraction code, compatible with control systems like EPICS and Tango
5
5
  Author-email: Tom Cobb <tom.cobb@diamond.ac.uk>
6
6
  License: BSD 3-Clause License
@@ -1,14 +1,14 @@
1
1
  ophyd_async/__init__.py,sha256=dcAA3qsj1nNIMe5l-v2tlduZ_ypwBmyuHe45Lsq4k4w,206
2
2
  ophyd_async/__main__.py,sha256=n_U4O9bgm97OuboUB_9eK7eFiwy8BZSgXJ0OzbE0DqU,481
3
3
  ophyd_async/_docs_parser.py,sha256=gPYrigfSbYCF7QoSf2UvE-cpQu4snSssl7ZWN-kKDzI,352
4
- ophyd_async/_version.py,sha256=a3VJZDtDsD7dO22j4y92zbdkUlJwzXf_QabiVquJm1Y,706
4
+ ophyd_async/_version.py,sha256=q0j1EdwWPylm9K8EEqyfgP0MBcuxBTAPKVxhFXx4oI8,706
5
5
  ophyd_async/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  ophyd_async/core/__init__.py,sha256=GJxLNTxXD3Qlghq6CNe-mCeludgBBpHkHXkbDFojdHQ,6084
7
- ophyd_async/core/_derived_signal.py,sha256=TuZza_j3J1Bw4QSqBYB9Ta2FyQP5BycO3nSHVtJ890Q,13015
7
+ ophyd_async/core/_derived_signal.py,sha256=9eJw_gNgr0mswrhbN7vK3hwwfXVzwpkumK7Ufxyhvds,14078
8
8
  ophyd_async/core/_derived_signal_backend.py,sha256=Ibce9JHghiI5Ir8w0pUYULHL2qWkobeUYc0-CDrsO2E,12615
9
9
  ophyd_async/core/_detector.py,sha256=9fYbBPmRnMGADcDTYkspDAL2uzhtNNiKCEeBUU0oKaY,14942
10
10
  ophyd_async/core/_device.py,sha256=tm-khZLMy-Q7nn86GYHkbXRJOc83DgkbrdQ6WCjZhUs,17638
11
- ophyd_async/core/_device_filler.py,sha256=MDz8eQQ-eEAwo-UEMxfqPfpcBuMG01tLCGR6utwVnmE,14825
11
+ ophyd_async/core/_device_filler.py,sha256=6fj439jT0_INBe95_VBBdtwIx13mFDASjG07flhq7h8,15745
12
12
  ophyd_async/core/_enums.py,sha256=2vh6x0rZ6SLiw2xxq1xVIn-GpbLDFc8wZoVdA55QiE8,370
13
13
  ophyd_async/core/_flyer.py,sha256=8zKyU5aQOr_t59GIUwsYeb8NSabdvBp0swwuRe4v5VQ,3457
14
14
  ophyd_async/core/_hdf_dataset.py,sha256=0bIX_ZbFSMdXqDwRtEvV-0avHnwXhjPddE5GVNmo7H8,2608
@@ -154,8 +154,8 @@ ophyd_async/testing/_one_of_everything.py,sha256=U9ui7B-iNHDM3H3hIWUuaCb8Gc2eLlU
154
154
  ophyd_async/testing/_single_derived.py,sha256=5-HOTzgePcZ354NK_ssVpyIbJoJmKyjVQCxSwQXUC-4,2730
155
155
  ophyd_async/testing/_utils.py,sha256=zClRo5ve8RGia7wQnby41W-Zprj-slOA5da1LfYnuhw,45
156
156
  ophyd_async/testing/_wait_for_pending.py,sha256=YZAR48n-CW0GsPey3zFRzMJ4byDAr3HvMIoawjmTrHw,732
157
- ophyd_async-0.14.1.dist-info/licenses/LICENSE,sha256=pU5shZcsvWgz701EbT7yjFZ8rMvZcWgRH54CRt8ld_c,1517
158
- ophyd_async-0.14.1.dist-info/METADATA,sha256=I2Y_2-lFXNeZYNYDhNxPvrubLM8bd8d5OzPbPzdn0mM,5703
159
- ophyd_async-0.14.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
160
- ophyd_async-0.14.1.dist-info/top_level.txt,sha256=-hjorMsv5Rmjo3qrgqhjpal1N6kW5vMxZO3lD4iEaXs,12
161
- ophyd_async-0.14.1.dist-info/RECORD,,
157
+ ophyd_async-0.14.2.dist-info/licenses/LICENSE,sha256=pU5shZcsvWgz701EbT7yjFZ8rMvZcWgRH54CRt8ld_c,1517
158
+ ophyd_async-0.14.2.dist-info/METADATA,sha256=vbWuD-k0gWAz7cp4386pSDtBDzBlU_c2wXer3MzmltU,5703
159
+ ophyd_async-0.14.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
160
+ ophyd_async-0.14.2.dist-info/top_level.txt,sha256=-hjorMsv5Rmjo3qrgqhjpal1N6kW5vMxZO3lD4iEaXs,12
161
+ ophyd_async-0.14.2.dist-info/RECORD,,