Skip to content

Commit a02b447

Browse files
committed
Attribute parsing with Controller class definitions
A
1 parent cec9fda commit a02b447

File tree

7 files changed

+94
-30
lines changed

7 files changed

+94
-30
lines changed

src/fastcs/controller.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ def register_sub_controller(self, name: str, sub_controller: SubController):
4242
def get_sub_controllers(self) -> dict[str, BaseController]:
4343
return self.__sub_controller_tree
4444

45+
def get_attributes(self) -> dict[str, Attribute]:
46+
"""For getting any attributes which aren't defined on the class itself."""
47+
return {}
48+
4549

4650
class Controller(BaseController):
4751
"""Top-level controller for a device.

src/fastcs/mapping.py

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
from collections.abc import Iterator
22
from dataclasses import dataclass
3+
from typing import get_type_hints
34

4-
from .attributes import Attribute
5+
from .attributes import Attribute, AttrR, AttrW, AttrRW
56
from .controller import BaseController, Controller
67
from .cs_methods import Command, Put, Scan
78
from .wrappers import WrappedMethod
@@ -41,7 +42,6 @@ def _get_single_mapping(controller: BaseController) -> SingleMapping:
4142
scan_methods: dict[str, Scan] = {}
4243
put_methods: dict[str, Put] = {}
4344
command_methods: dict[str, Command] = {}
44-
attributes: dict[str, Attribute] = {}
4545
for attr_name in dir(controller):
4646
attr = getattr(controller, attr_name)
4747
match attr:
@@ -51,8 +51,33 @@ def _get_single_mapping(controller: BaseController) -> SingleMapping:
5151
scan_methods[attr_name] = scan_method
5252
case WrappedMethod(fastcs_method=Command(enabled=True) as command_method):
5353
command_methods[attr_name] = command_method
54-
case Attribute(enabled=True):
55-
attributes[attr_name] = attr
54+
55+
attributes: dict[str, Attribute] = {}
56+
for name in list(get_type_hints(type(controller))) + dir(type(controller)):
57+
if (
58+
isinstance(
59+
(attr := getattr(controller, name, None)), AttrRW | AttrR | AttrW
60+
)
61+
and attr.enabled
62+
):
63+
attributes[name] = attr
64+
65+
object_defined_attributes = {
66+
name: attr for name, attr in controller.get_attributes().items() if attr.enabled
67+
}
68+
69+
if conflicting_keys := {
70+
key
71+
for key in object_defined_attributes.keys() & attributes.keys()
72+
if object_defined_attributes[key] is not attributes[key]
73+
}:
74+
raise TypeError(
75+
f"{controller} has conflicting attributes between those passed in"
76+
"`get_attributes` and those obtained from the class definition: "
77+
f"{conflicting_keys}"
78+
)
79+
80+
attributes.update(object_defined_attributes)
5681

5782
return SingleMapping(
5883
controller, scan_methods, put_methods, command_methods, attributes

tests/backends/epics/test_gui.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -49,44 +49,44 @@ def test_get_components(mapping):
4949
children=[
5050
SignalR(
5151
name="ReadInt",
52-
read_pv="DEVICE:SubController01:ReadInt",
52+
read_pv="DEVICE:SubController02:ReadInt",
5353
read_widget=TextRead(),
5454
)
5555
],
5656
),
57-
SignalR(name="BigEnum", read_pv="DEVICE:BigEnum", read_widget=TextRead()),
58-
SignalR(name="ReadBool", read_pv="DEVICE:ReadBool", read_widget=LED()),
5957
SignalR(
6058
name="ReadInt",
6159
read_pv="DEVICE:ReadInt",
6260
read_widget=TextRead(),
6361
),
6462
SignalRW(
65-
name="ReadWriteFloat",
66-
write_pv="DEVICE:ReadWriteFloat",
63+
name="ReadWriteInt",
64+
write_pv="DEVICE:ReadWriteInt",
6765
write_widget=TextWrite(),
68-
read_pv="DEVICE:ReadWriteFloat_RBV",
66+
read_pv="DEVICE:ReadWriteInt_RBV",
6967
read_widget=TextRead(),
7068
),
7169
SignalRW(
72-
name="ReadWriteInt",
73-
write_pv="DEVICE:ReadWriteInt",
70+
name="ReadWriteFloat",
71+
write_pv="DEVICE:ReadWriteFloat",
7472
write_widget=TextWrite(),
75-
read_pv="DEVICE:ReadWriteInt_RBV",
73+
read_pv="DEVICE:ReadWriteFloat_RBV",
7674
read_widget=TextRead(),
7775
),
76+
SignalR(name="ReadBool", read_pv="DEVICE:ReadBool", read_widget=LED()),
77+
SignalW(
78+
name="WriteBool",
79+
write_pv="DEVICE:WriteBool",
80+
write_widget=ToggleButton(),
81+
),
7882
SignalRW(
7983
name="StringEnum",
8084
read_pv="DEVICE:StringEnum_RBV",
8185
read_widget=TextRead(format=TextFormat.string),
8286
write_pv="DEVICE:StringEnum",
8387
write_widget=ComboBox(choices=["red", "green", "blue"]),
8488
),
85-
SignalW(
86-
name="WriteBool",
87-
write_pv="DEVICE:WriteBool",
88-
write_widget=ToggleButton(),
89-
),
89+
SignalR(name="BigEnum", read_pv="DEVICE:BigEnum", read_widget=TextRead()),
9090
SignalX(
9191
name="Go",
9292
write_pv="DEVICE:Go",

tests/backends/epics/test_ioc.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Any
1+
from typing import Any, get_type_hints
22

33
import pytest
44
from pytest_mock import MockerFixture
@@ -401,6 +401,7 @@ def test_long_pv_names_discarded(mocker: MockerFixture):
401401
long_rw_name = "attr_rw_with_a_reallyreally_long_name_that_is_too_long_for_RBV"
402402
assert long_name_controller.attr_rw_short_name.enabled
403403
assert getattr(long_name_controller, long_attr_name).enabled
404+
404405
EpicsIOC(DEVICE, long_name_mapping)
405406
assert long_name_controller.attr_rw_short_name.enabled
406407
assert not getattr(long_name_controller, long_attr_name).enabled

tests/backends/tango/test_dsr.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ def tango_context(self, assertable_controller):
1414
yield proxy
1515

1616
def test_list_attributes(self, tango_context):
17-
assert list(tango_context.get_attribute_list()) == [
17+
assert set(tango_context.get_attribute_list()) == {
1818
"BigEnum",
1919
"ReadBool",
2020
"ReadInt",
@@ -26,7 +26,7 @@ def test_list_attributes(self, tango_context):
2626
"SubController02_ReadInt",
2727
"State",
2828
"Status",
29-
]
29+
}
3030

3131
def test_list_commands(self, tango_context):
3232
assert list(tango_context.get_command_list()) == [

tests/conftest.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -57,15 +57,6 @@ class TestSubController(SubController):
5757

5858

5959
class TestController(Controller):
60-
def __init__(self) -> None:
61-
super().__init__()
62-
63-
self._sub_controllers: list[TestSubController] = []
64-
for index in range(1, 3):
65-
controller = TestSubController()
66-
self._sub_controllers.append(controller)
67-
self.register_sub_controller(f"SubController{index:02d}", controller)
68-
6960
read_int: AttrR = AttrR(Int(), handler=TestUpdater())
7061
read_write_int: AttrRW = AttrRW(Int(), handler=TestHandler())
7162
read_write_float: AttrRW = AttrRW(Float())
@@ -77,6 +68,15 @@ def __init__(self) -> None:
7768
allowed_values=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17],
7869
)
7970

71+
def __init__(self) -> None:
72+
super().__init__()
73+
74+
self._sub_controllers: list[TestSubController] = []
75+
for index in range(1, 3):
76+
controller = TestSubController()
77+
self._sub_controllers.append(controller)
78+
self.register_sub_controller(f"SubController{index:02d}", controller)
79+
8080
initialised = False
8181
connected = False
8282
count = 0

tests/test_controller.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import pytest
22

3+
from fastcs.attributes import Attribute, AttrR
34
from fastcs.controller import Controller, SubController
5+
from fastcs.datatypes import Int
46
from fastcs.mapping import _get_single_mapping, _walk_mappings
57

68

@@ -29,3 +31,35 @@ def test_controller_nesting():
2931
ValueError, match=r"SubController is already registered under .*"
3032
):
3133
controller.register_sub_controller("c", sub_controller)
34+
35+
36+
def test_attribute_parsing():
37+
runtime_attribute = AttrR(Int())
38+
39+
class SomeController(Controller):
40+
annotated_attr = AttrR(Int())
41+
annotated_attr_not_defined_in_init: AttrR[int]
42+
equal_attr = AttrR(Int())
43+
annotated_and_equal_attr: AttrR[int] = AttrR(Int())
44+
45+
def get_attributes(self) -> dict[str, Attribute]:
46+
return {"get_attributes_attr": runtime_attribute}
47+
48+
def __init__(self):
49+
self.annotated_attr = AttrR(Int())
50+
super().__init__()
51+
52+
controller = SomeController()
53+
mapping = next(_walk_mappings(controller))
54+
assert mapping.attributes == {
55+
"get_attributes_attr": runtime_attribute,
56+
"annotated_attr": controller.annotated_attr,
57+
"equal_attr": controller.equal_attr,
58+
"annotated_and_equal_attr": controller.annotated_and_equal_attr,
59+
}
60+
61+
assert SomeController.equal_attr is not controller.equal_attr
62+
assert (
63+
SomeController.annotated_and_equal_attr
64+
is not controller.annotated_and_equal_attr
65+
)

0 commit comments

Comments
 (0)