Skip to content

Commit 9f724d2

Browse files
authored
signal walking added for debugging (#917)
* signal walking added for debuggin@ * respond to review * add test walk devices * update tests as expected * another test for signal sources * lint * make an explicit test of signal_walk * fix imports
1 parent d38115e commit 9f724d2

File tree

3 files changed

+91
-21
lines changed

3 files changed

+91
-21
lines changed

src/ophyd_async/core/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,9 @@
5656
soft_signal_rw,
5757
wait_for_value,
5858
walk_config_signals,
59+
walk_devices,
5960
walk_rw_signals,
61+
walk_signal_sources,
6062
)
6163
from ._signal_backend import (
6264
Array1D,
@@ -147,6 +149,8 @@
147149
"set_and_wait_for_other_value",
148150
"walk_rw_signals",
149151
"walk_config_signals",
152+
"walk_devices",
153+
"walk_signal_sources",
150154
# Readable
151155
"StandardReadable",
152156
"StandardReadableFormat",

src/ophyd_async/core/_signal.py

Lines changed: 38 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -688,7 +688,7 @@ async def set_and_wait_for_value(
688688
)
689689

690690

691-
def walk_rw_signals(device: Device, path_prefix: str = "") -> dict[str, SignalRW[Any]]:
691+
def walk_rw_signals(device: Device) -> dict[str, SignalRW[Any]]:
692692
"""Retrieve all SignalRWs from a device.
693693
694694
Stores retrieved signals with their dotted attribute paths in a dictionary. Used as
@@ -700,48 +700,67 @@ def walk_rw_signals(device: Device, path_prefix: str = "") -> dict[str, SignalRW
700700
A dictionary matching the string attribute path of a SignalRW with the
701701
signal itself.
702702
"""
703-
signals: dict[str, SignalRW[Any]] = {}
704-
705-
for attr_name, attr in device.children():
706-
dot_path = f"{path_prefix}{attr_name}"
707-
if type(attr) is SignalRW:
708-
signals[dot_path] = attr
709-
attr_signals = walk_rw_signals(attr, path_prefix=dot_path + ".")
710-
signals.update(attr_signals)
711-
return signals
703+
all_devices = walk_devices(device)
704+
return {path: dev for path, dev in all_devices.items() if type(dev) is SignalRW}
712705

713706

714707
async def walk_config_signals(
715-
device: Device, path_prefix: str = ""
708+
device: Device,
716709
) -> dict[str, SignalRW[Any]]:
717710
"""Retrieve all configuration signals from a device.
718711
719712
Stores retrieved signals with their dotted attribute paths in a dictionary. Used as
720713
part of saving and loading a device.
721714
722715
:param device: Device to retrieve configuration signals from.
723-
:param path_prefix: For internal use, leave blank when calling the method.
724716
:return:
725717
A dictionary matching the string attribute path of a SignalRW with the
726718
signal itself.
727719
"""
728-
signals: dict[str, SignalRW[Any]] = {}
729720
config_names: list[str] = []
730721
if isinstance(device, Configurable):
731722
configuration = device.read_configuration()
732723
if inspect.isawaitable(configuration):
733724
configuration = await configuration
734725
config_names = list(configuration.keys())
735-
for attr_name, attr in device.children():
736-
dot_path = f"{path_prefix}{attr_name}"
737-
if isinstance(attr, SignalRW) and attr.name in config_names:
738-
signals[dot_path] = attr
739-
signals.update(await walk_config_signals(attr, path_prefix=dot_path + "."))
740726

741-
return signals
727+
all_devices = walk_devices(device)
728+
return {
729+
path: dev
730+
for path, dev in all_devices.items()
731+
if isinstance(dev, SignalRW) and dev.name in config_names
732+
}
742733

743734

744735
class Ignore:
745736
"""Annotation to ignore a signal when connecting a device."""
746737

747738
pass
739+
740+
741+
def walk_devices(device: Device, path_prefix: str = "") -> dict[str, Device]:
742+
"""Recursively retrieve all Devices from a device tree.
743+
744+
:param device: Root device to start from.
745+
:param path_prefix: For internal use, leave blank when calling the method.
746+
:return: A dictionary mapping dotted attribute paths to Device instances.
747+
"""
748+
devices: dict[str, Device] = {}
749+
for attr_name, attr in device.children():
750+
dot_path = f"{path_prefix}{attr_name}"
751+
devices[dot_path] = attr
752+
devices.update(walk_devices(attr, path_prefix=dot_path + "."))
753+
return devices
754+
755+
756+
def walk_signal_sources(device: Device) -> dict[str, str]:
757+
"""Recursively gather the `source` field from every Signal in a device tree.
758+
759+
:param device: Root device to start from.
760+
:param path_prefix: For internal use, leave blank when calling the method.
761+
:return: A dictionary mapping dotted attribute paths to Signal source strings.
762+
"""
763+
all_devices = walk_devices(device)
764+
return {
765+
path: dev.source for path, dev in all_devices.items() if isinstance(dev, Signal)
766+
}

tests/core/test_signal.py

Lines changed: 49 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,15 @@
2727
soft_signal_r_and_setter,
2828
soft_signal_rw,
2929
wait_for_value,
30+
walk_devices,
31+
walk_signal_sources,
32+
)
33+
from ophyd_async.core import (
34+
StandardReadableFormat as Format,
35+
)
36+
from ophyd_async.core._signal import (
37+
_SignalCache, # noqa: PLC2701
3038
)
31-
from ophyd_async.core import StandardReadableFormat as Format
32-
from ophyd_async.core._signal import _SignalCache # noqa: PLC2701
3339
from ophyd_async.epics.core import epics_signal_r, epics_signal_rw
3440
from ophyd_async.epics.core._signal import get_signal_backend_type # noqa: PLC2701
3541
from ophyd_async.testing import (
@@ -1021,3 +1027,44 @@ def test_remove_non_existing_listener():
10211027
signal_rw = soft_signal_rw(int, initial_value=4)
10221028
cbs = []
10231029
assert signal_rw.clear_sub(cbs.append) is None
1030+
1031+
1032+
async def test_walk_devices_returns_all_devices(mock_readable: DummyReadableArray):
1033+
"""
1034+
Test that walk_devices returns all child devices with correct dotted paths.
1035+
"""
1036+
1037+
# Get all devices in the tree
1038+
devices = walk_devices(mock_readable)
1039+
1040+
assert devices == {
1041+
"int_value": mock_readable.int_value,
1042+
"int_array": mock_readable.int_array,
1043+
"float_array": mock_readable.float_array,
1044+
"str_value": mock_readable.str_value,
1045+
"strictEnum_value": mock_readable.strictEnum_value,
1046+
}
1047+
1048+
# All returned objects should be Device instances
1049+
for dev in devices.values():
1050+
assert isinstance(dev, Device)
1051+
1052+
1053+
async def test_walk_signal_sources_returns_signal_sources(
1054+
mock_readable: DummyReadableArray,
1055+
):
1056+
"""
1057+
Test that walk_signal_sources returns correct mapping
1058+
of dotted paths to Signal sources.
1059+
"""
1060+
sources = walk_signal_sources(mock_readable)
1061+
1062+
expected_sources = {
1063+
"int_value": mock_readable.int_value.source,
1064+
"int_array": mock_readable.int_array.source,
1065+
"float_array": mock_readable.float_array.source,
1066+
"str_value": mock_readable.str_value.source,
1067+
"strictEnum_value": mock_readable.strictEnum_value.source,
1068+
}
1069+
1070+
assert sources == expected_sources

0 commit comments

Comments
 (0)