diff --git a/src/fastcs/controllers/base_controller.py b/src/fastcs/controllers/base_controller.py index 11c90e86d..8b29ed6d2 100755 --- a/src/fastcs/controllers/base_controller.py +++ b/src/fastcs/controllers/base_controller.py @@ -98,10 +98,27 @@ def _find_type_hints(self): elif isinstance(hint, type) and issubclass(hint, Method): self.__hinted_methods[name] = hint + @classmethod + def _walk_mro(cls): + """Return ordered attribute names from the class MRO. + + Traverses MRO from base to subclass, collecting attribute names in definition + order while skipping duplicates (subclass overrides take precedence) and private + attributes. + """ + class_dir = [] + seen = set() + for base in reversed(cls.__mro__): + for key in base.__dict__: + if not key.startswith("_") and key not in seen: + seen.add(key) + class_dir.append(key) + return class_dir + def _bind_attrs(self) -> None: - """Search for Attributes and Methods to bind them to this instance. + """Bind Attributes and Methods to this instance. - This method will search the attributes of this controller class to bind them to + This method will bind the attributes of this controller class to this specific instance. For Attributes, this is just a case of copying and re-assigning to ``self`` to make it unique across multiple instances of this controller class. For Methods, this requires creating a bound method from a @@ -109,8 +126,7 @@ class method and a controller instance, so that it can be called from any context with the controller instance passed as the ``self`` argument. """ - # Using a dictionary instead of a set to maintain order. - class_dir = {key: None for key in dir(type(self)) if not key.startswith("_")} + class_dir = dict.fromkeys(self._walk_mro()) class_type_hints = { key: value for key, value in get_type_hints(type(self)).items() diff --git a/tests/transports/epics/ca/test_gui.py b/tests/transports/epics/ca/test_gui.py index 39485a527..72931b862 100644 --- a/tests/transports/epics/ca/test_gui.py +++ b/tests/transports/epics/ca/test_gui.py @@ -125,16 +125,17 @@ def test_get_components(controller_api): ) ], ), - SignalR(name="ReadBool", read_pv="DEVICE:ReadBool", read_widget=LED()), SignalR( name="ReadInt", read_pv="DEVICE:ReadInt", read_widget=TextRead(), ), SignalRW( - name="ReadString", - read_pv="DEVICE:ReadString_RBV", - write_pv="DEVICE:ReadString", + name="ReadWriteInt", + write_pv="DEVICE:ReadWriteInt", + write_widget=TextWrite(), + read_pv="DEVICE:ReadWriteInt_RBV", + read_widget=TextRead(), ), SignalRW( name="ReadWriteFloat", @@ -143,18 +144,21 @@ def test_get_components(controller_api): read_pv="DEVICE:ReadWriteFloat_RBV", read_widget=TextRead(), ), - SignalRW( - name="ReadWriteInt", - write_pv="DEVICE:ReadWriteInt", - write_widget=TextWrite(), - read_pv="DEVICE:ReadWriteInt_RBV", - read_widget=TextRead(), + SignalR( + name="ReadBool", + read_pv="DEVICE:ReadBool", + read_widget=LED(), ), SignalW( name="WriteBool", write_pv="DEVICE:WriteBool", write_widget=ToggleButton(), ), + SignalRW( + name="ReadString", + read_pv="DEVICE:ReadString_RBV", + write_pv="DEVICE:ReadString", + ), SignalX( name="Go", write_pv="DEVICE:Go", diff --git a/tests/transports/tango/test_dsr.py b/tests/transports/tango/test_dsr.py index b269ce5a8..3b599c624 100644 --- a/tests/transports/tango/test_dsr.py +++ b/tests/transports/tango/test_dsr.py @@ -69,15 +69,15 @@ def tango_context( def test_list_attributes(self, tango_context: DeviceProxy): assert list(tango_context.get_attribute_list()) == [ - "Enum", - "OneDWaveform", - "ReadBool", "ReadInt", - "ReadString", - "ReadWriteFloat", "ReadWriteInt", - "TwoDWaveform", + "ReadWriteFloat", + "ReadBool", "WriteBool", + "ReadString", + "Enum", + "OneDWaveform", + "TwoDWaveform", "SubController01_ReadInt", "SubController02_ReadInt", "State",