Skip to content

Commit

Permalink
Allow PVI devices to be initialized before connection (#241)
Browse files Browse the repository at this point in the history
* allow a signal's backend to be passed in at `connect`, also allow `DeviceVector` to be pre-initialised in pvi device
  • Loading branch information
noemifrisina authored Jun 10, 2024
1 parent 389194f commit cbbb295
Show file tree
Hide file tree
Showing 4 changed files with 131 additions and 11 deletions.
21 changes: 16 additions & 5 deletions src/ophyd_async/core/signal.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ class Signal(Device, Generic[T]):

def __init__(
self,
backend: SignalBackend[T],
backend: Optional[SignalBackend[T]] = None,
timeout: Optional[float] = DEFAULT_TIMEOUT,
name: str = "",
) -> None:
Expand All @@ -66,13 +66,24 @@ def __init__(
super().__init__(name)

async def connect(
self, mock=False, timeout=DEFAULT_TIMEOUT, force_reconnect: bool = False
self,
mock=False,
timeout=DEFAULT_TIMEOUT,
force_reconnect: bool = False,
backend: Optional[SignalBackend[T]] = None,
):
if backend:
if self._initial_backend and backend is not self._initial_backend:
raise ValueError(
"Backend at connection different from initialised one."
)
self._backend = backend
if mock and not isinstance(self._backend, MockSignalBackend):
# Using a soft backend, look to the initial value
self._backend = MockSignalBackend(
initial_backend=self._initial_backend,
)
self._backend = MockSignalBackend(initial_backend=self._backend)

if self._backend is None:
raise RuntimeError("`connect` called on signal without backend")
self.log.debug(f"Connecting to {self.source}")
await self._backend.connect(timeout=timeout)

Expand Down
32 changes: 26 additions & 6 deletions src/ophyd_async/epics/pvi/pvi.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,15 @@ def _mock_common_blocks(device: Device, stripped_type: Optional[Type] = None):
sub_device_2 = device_cls(SoftSignalBackend(signal_dtype))
sub_device = DeviceVector({1: sub_device_1, 2: sub_device_2})
else:
sub_device = DeviceVector({1: device_cls(), 2: device_cls()})
if hasattr(device, device_name):
sub_device = getattr(device, device_name)
else:
sub_device = DeviceVector(
{
1: device_cls(),
2: device_cls(),
}
)

for sub_device_in_vector in sub_device.values():
_mock_common_blocks(sub_device_in_vector, stripped_type=device_cls)
Expand Down Expand Up @@ -296,7 +304,9 @@ async def fill_pvi_entries(


def create_children_from_annotations(
device: Device, included_optional_fields: Tuple[str, ...] = ()
device: Device,
included_optional_fields: Tuple[str, ...] = (),
device_vectors: Optional[Dict[str, int]] = None,
):
"""For intializing blocks at __init__ of ``device``."""
for name, device_type in get_type_hints(type(device)).items():
Expand All @@ -307,12 +317,22 @@ def create_children_from_annotations(
continue
is_device_vector, device_type = _strip_device_vector(device_type)
if (
is_device_vector
(is_device_vector and (not device_vectors or name not in device_vectors))
or ((origin := get_origin(device_type)) and issubclass(origin, Signal))
or (isclass(device_type) and issubclass(device_type, Signal))
):
continue

sub_device = device_type()
setattr(device, name, sub_device)
create_children_from_annotations(sub_device)
if is_device_vector:
n_device_vector = DeviceVector(
{i: device_type() for i in range(1, device_vectors[name] + 1)}
)
setattr(device, name, n_device_vector)
for sub_device in n_device_vector.values():
create_children_from_annotations(
sub_device, device_vectors=device_vectors
)
else:
sub_device = device_type()
setattr(device, name, sub_device)
create_children_from_annotations(sub_device, device_vectors=device_vectors)
31 changes: 31 additions & 0 deletions tests/core/test_signal.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
ConfigSignal,
DeviceCollector,
HintedSignal,
MockSignalBackend,
Signal,
SignalR,
SignalRW,
SoftSignalBackend,
Expand Down Expand Up @@ -50,6 +52,35 @@ async def test_signals_equality_raises():
s1 > 4


async def test_signal_can_be_given_backend_on_connect():
sim_signal = SignalR()
backend = MockSignalBackend(int)
assert sim_signal._backend is None
await sim_signal.connect(mock=False, backend=backend)
assert await sim_signal.get_value() == 0


async def test_signal_connect_fails_with_different_backend_on_connection():
sim_signal = Signal(MockSignalBackend(str))

with pytest.raises(ValueError):
await sim_signal.connect(mock=True, backend=MockSignalBackend(int))

with pytest.raises(ValueError):
await sim_signal.connect(mock=True, backend=SoftSignalBackend(str))


async def test_signal_connect_fails_if_different_backend_but_same_by_value():
initial_backend = MockSignalBackend(str)
sim_signal = Signal(initial_backend)

with pytest.raises(ValueError) as exc:
await sim_signal.connect(mock=False, backend=MockSignalBackend(str))
assert str(exc.value) == "Backend at connection different from initialised one."

await sim_signal.connect(mock=False, backend=initial_backend)


async def time_taken_by(coro) -> float:
start = time.monotonic()
await coro
Expand Down
58 changes: 58 additions & 0 deletions tests/epics/test_pvi.py
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,61 @@ async def test_device_create_children_from_annotations(
assert device.device is block_2_device
assert device.device.device is block_1_device
assert device.signal_device is top_block_1_device


@pytest.fixture
def pvi_test_device_with_device_vectors_t():
"""A fixture since pytest discourages init in test case classes"""

class TestBlock(Device):
device_vector: DeviceVector[Block1]
device: Optional[Block1]
signal_x: SignalX
signal_rw: Optional[SignalRW[int]]

class TestDevice(TestBlock):
def __init__(self, prefix: str, name: str = ""):
self._prefix = prefix
create_children_from_annotations(
self,
included_optional_fields=("device", "signal_rw"),
device_vectors={"device_vector": 2},
)
super().__init__(name)

async def connect(
self, mock: bool = False, timeout: float = DEFAULT_TIMEOUT
) -> None:
await fill_pvi_entries(
self, self._prefix + "PVI", timeout=timeout, mock=mock
)

await super().connect(mock=mock)

yield TestDevice


async def test_device_create_children_from_annotations_with_device_vectors(
pvi_test_device_with_device_vectors_t,
):
device = pvi_test_device_with_device_vectors_t("PREFIX:", name="test_device")

assert device.device_vector[1].name == "test_device-device_vector-1"
assert device.device_vector[2].name == "test_device-device_vector-2"
block_1_device = device.device
block_2_device_vector = device.device_vector

# create_children_from_annotiations should have made DeviceVectors
# and an optional Block, but no signals
assert hasattr(device, "device_vector")
assert not hasattr(device, "signal_rw")
assert isinstance(block_2_device_vector, DeviceVector)
assert isinstance(block_2_device_vector[1], Block1)
assert len(device.device_vector) == 2
assert isinstance(block_1_device, Block1)

await device.connect(mock=True)

# The memory addresses have not changed
assert device.device is block_1_device
assert device.device_vector is block_2_device_vector

0 comments on commit cbbb295

Please sign in to comment.