Skip to content

Commit 779073f

Browse files
committed
Added root_attribute
Added a method to `SubController` to allow passing of a top level `Attribute` from the `SubController` to the parent controller. The `Attribute` has the same name as the sub controller in the parent controller.
1 parent 93af3e4 commit 779073f

File tree

4 files changed

+110
-24
lines changed

4 files changed

+110
-24
lines changed

src/fastcs/controller.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
class BaseController:
99
def __init__(self, path: list[str] | None = None) -> None:
1010
self._path: list[str] = path or []
11-
self.__sub_controller_tree: dict[str, BaseController] = {}
11+
self.__sub_controller_tree: dict[str, SubController] = {}
1212

1313
self._bind_attrs()
1414

@@ -25,6 +25,8 @@ def set_path(self, path: list[str]):
2525

2626
def _bind_attrs(self) -> None:
2727
for attr_name in dir(self):
28+
if attr_name == "root_attribute":
29+
continue
2830
attr = getattr(self, attr_name)
2931
if isinstance(attr, Attribute):
3032
new_attribute = copy(attr)
@@ -39,7 +41,7 @@ def register_sub_controller(self, name: str, sub_controller: SubController):
3941
self.__sub_controller_tree[name] = sub_controller
4042
sub_controller.set_path(self.path + [name])
4143

42-
def get_sub_controllers(self) -> dict[str, BaseController]:
44+
def get_sub_controllers(self) -> dict[str, SubController]:
4345
return self.__sub_controller_tree
4446

4547
def get_attributes(self) -> dict[str, Attribute]:
@@ -75,3 +77,7 @@ class SubController(BaseController):
7577

7678
def __init__(self) -> None:
7779
super().__init__()
80+
81+
@property
82+
def root_attribute(self) -> Attribute | None:
83+
return None

src/fastcs/mapping.py

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
from dataclasses import dataclass
33
from typing import get_type_hints
44

5-
from .attributes import Attribute, AttrR, AttrW, AttrRW
5+
from .attributes import Attribute, AttrR, AttrRW, AttrW
66
from .controller import BaseController, Controller
77
from .cs_methods import Command, Put, Scan
88
from .wrappers import WrappedMethod
@@ -54,6 +54,9 @@ def _get_single_mapping(controller: BaseController) -> SingleMapping:
5454

5555
attributes: dict[str, Attribute] = {}
5656
for name in list(get_type_hints(type(controller))) + dir(type(controller)):
57+
if name == "root_attribute":
58+
continue
59+
5760
if (
5861
isinstance(
5962
(attr := getattr(controller, name, None)), AttrRW | AttrR | AttrW
@@ -71,14 +74,26 @@ def _get_single_mapping(controller: BaseController) -> SingleMapping:
7174
for key in object_defined_attributes.keys() & attributes.keys()
7275
if object_defined_attributes[key] is not attributes[key]
7376
}:
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}"
77+
raise ValueError(
78+
f"`{type(controller).__name__}` has conflicting attributes between "
79+
"those passed in `get_attributes` and those obtained from the "
80+
f"class definition: {conflicting_keys}"
7881
)
7982

8083
attributes.update(object_defined_attributes)
8184

85+
for sub_controller_name, sub_controller in controller.get_sub_controllers().items():
86+
root_attribute = sub_controller.root_attribute
87+
if root_attribute is None:
88+
continue
89+
90+
if sub_controller_name in attributes:
91+
raise ValueError(
92+
f"sub_controller `{sub_controller_name}` has a `root_attribute` "
93+
f"already defined defined in parent controller {sub_controller_name}"
94+
)
95+
attributes[sub_controller_name] = root_attribute
96+
8297
return SingleMapping(
8398
controller, scan_methods, put_methods, command_methods, attributes
8499
)

tests/backends/epics/test_ioc.py

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

33
import pytest
44
from pytest_mock import MockerFixture

tests/test_controller.py

Lines changed: 81 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -33,33 +33,98 @@ def test_controller_nesting():
3333
controller.register_sub_controller("c", sub_controller)
3434

3535

36-
def test_attribute_parsing():
37-
runtime_attribute = AttrR(Int())
36+
class SomeSubController(SubController):
37+
def __init__(self):
38+
self._root_attribute = AttrR(Int())
39+
super().__init__()
3840

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())
41+
sub_attribute = AttrR(Int())
4442

45-
def get_attributes(self) -> dict[str, Attribute]:
46-
return {"get_attributes_attr": runtime_attribute}
43+
@property
44+
def root_attribute(self):
45+
return self._root_attribute
4746

48-
def __init__(self):
49-
self.annotated_attr = AttrR(Int())
50-
super().__init__()
5147

52-
controller = SomeController()
53-
mapping = next(_walk_mappings(controller))
54-
assert mapping.attributes == {
55-
"get_attributes_attr": runtime_attribute,
48+
class SomeController(Controller):
49+
annotated_attr = AttrR(Int())
50+
annotated_attr_not_defined_in_init: AttrR[int]
51+
equal_attr = AttrR(Int())
52+
annotated_and_equal_attr: AttrR[int] = AttrR(Int())
53+
54+
def __init__(self, sub_controller: SubController):
55+
self.get_attributes_attribute = AttrR(Int())
56+
self.annotated_attr = AttrR(Int())
57+
self.attr_not_walked = AttrR(Int())
58+
59+
super().__init__()
60+
61+
self.register_sub_controller("sub_controller", sub_controller)
62+
63+
def get_attributes(self) -> dict[str, Attribute]:
64+
return {
65+
"get_attributes_attr": self.get_attributes_attribute,
66+
"equal_attr": self.equal_attr,
67+
}
68+
69+
70+
def test_attribute_parsing():
71+
sub_controller = SomeSubController()
72+
controller = SomeController(sub_controller)
73+
74+
mapping_walk = _walk_mappings(controller)
75+
76+
controller_mapping = next(mapping_walk)
77+
assert controller_mapping.attributes == {
78+
"get_attributes_attr": controller.get_attributes_attribute,
5679
"annotated_attr": controller.annotated_attr,
5780
"equal_attr": controller.equal_attr,
5881
"annotated_and_equal_attr": controller.annotated_and_equal_attr,
82+
"sub_controller": sub_controller.root_attribute,
5983
}
6084

6185
assert SomeController.equal_attr is not controller.equal_attr
6286
assert (
6387
SomeController.annotated_and_equal_attr
6488
is not controller.annotated_and_equal_attr
6589
)
90+
91+
sub_controller_mapping = next(mapping_walk)
92+
assert sub_controller_mapping.attributes == {
93+
"sub_attribute": sub_controller.sub_attribute,
94+
}
95+
96+
97+
def test_root_attribute():
98+
class FailingController(SomeController):
99+
def get_attributes(self) -> dict[str, Attribute]:
100+
return {"sub_controller": self.get_attributes_attribute}
101+
102+
with pytest.raises(
103+
ValueError,
104+
match=(
105+
"sub_controller `sub_controller` has a `root_attribute` already "
106+
"defined defined in parent controller sub_controller"
107+
),
108+
):
109+
next(_walk_mappings(FailingController(SomeSubController())))
110+
111+
112+
def test_attribute_in_both_class_and_get_attributes():
113+
class FailingController(Controller):
114+
duplicate_attribute = AttrR(Int())
115+
116+
def __init__(self):
117+
super().__init__()
118+
119+
def get_attributes(self) -> dict[str, Attribute]:
120+
return {"duplicate_attribute": AttrR(Int())}
121+
122+
with pytest.raises(
123+
ValueError,
124+
match=(
125+
"`FailingController` has conflicting attributes between those passed "
126+
"in `get_attributes` and those obtained from the class definition: "
127+
"{'duplicate_attribute'}"
128+
),
129+
):
130+
next(_walk_mappings(FailingController()))

0 commit comments

Comments
 (0)