Skip to content

Commit 66b7f5c

Browse files
committed
Allow binding Attribute to Controller via __setattr__
1 parent dcd7b5e commit 66b7f5c

File tree

3 files changed

+40
-27
lines changed

3 files changed

+40
-27
lines changed

src/fastcs/attributes.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ def __init__(
4747
# changing the units on an int. This should be implemented in the backend.
4848
self._update_datatype_callbacks: list[Callable[[DataType[T]], None]] = []
4949

50+
# Name to be filled in by Controller when the Attribute is bound
51+
self._name = None
52+
5053
@property
5154
def io_ref(self) -> AttributeIORefT:
5255
if self._io_ref is None:
@@ -82,8 +85,16 @@ def update_datatype(self, datatype: DataType[T]) -> None:
8285
for callback in self._update_datatype_callbacks:
8386
callback(datatype)
8487

88+
def set_name(self, name: list[str]):
89+
if self._name:
90+
raise ValueError(
91+
f"Attribute is already registered with a controller as {self._name}"
92+
)
93+
94+
self._name = name
95+
8596
def __repr__(self):
86-
return f"{self.__class__.__name__}({self._datatype})"
97+
return f"{self.__class__.__name__}({self._name}, {self._datatype})"
8798

8899

89100
class AttrR(Attribute[T, AttributeIORefT]):

src/fastcs/controller.py

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -131,18 +131,7 @@ class method and a controller instance, so that it can be called from any
131131

132132
attr = getattr(self, attr_name, None)
133133
if isinstance(attr, Attribute):
134-
if (
135-
attr_name in self.attributes
136-
and self.attributes[attr_name] is not attr
137-
):
138-
raise ValueError(
139-
f"`{type(self).__name__}` has conflicting attribute "
140-
f"`{attr_name}` already present in the attributes dict."
141-
)
142-
143-
new_attribute = deepcopy(attr)
144-
setattr(self, attr_name, new_attribute)
145-
self.attributes[attr_name] = new_attribute
134+
setattr(self, attr_name, deepcopy(attr))
146135
elif isinstance(attr, UnboundPut | UnboundScan | UnboundCommand):
147136
setattr(self, attr_name, attr.bind(self))
148137

@@ -164,6 +153,22 @@ def _validate_io(self, ios: Sequence[AttributeIO[T, AttributeIORefT]]):
164153
f"{attr.io_ref.__class__.__name__}"
165154
)
166155

156+
def add_attribute(self, name, attribute: Attribute):
157+
if name in self.attributes and attribute is not self.attributes[name]:
158+
raise ValueError(
159+
f"Cannot add attribute {name}. "
160+
f"Controller {self} has has existing attribute {name}"
161+
)
162+
elif name in self.__sub_controller_tree.keys():
163+
raise ValueError(
164+
f"Cannot add attribute {name}. "
165+
f"Controller {self} has existing sub controller {name}"
166+
)
167+
168+
attribute.set_name(name)
169+
self.attributes[name] = attribute
170+
super().__setattr__(name, attribute)
171+
167172
def register_sub_controller(self, name: str, sub_controller: Controller):
168173
if name in self.__sub_controller_tree.keys():
169174
raise ValueError(
@@ -190,6 +195,12 @@ def __repr__(self):
190195
{type(self).__name__}({self.path}, {list(self.__sub_controller_tree.keys())})\
191196
"""
192197

198+
def __setattr__(self, name, value):
199+
if isinstance(value, Attribute):
200+
self.add_attribute(name, value)
201+
else:
202+
super().__setattr__(name, value)
203+
193204

194205
class Controller(BaseController):
195206
"""Top-level controller for a device.

tests/test_controller.py

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -39,21 +39,18 @@ def __init__(self):
3939

4040

4141
class SomeController(Controller):
42-
annotated_attr: AttrR
4342
annotated_attr_not_defined_in_init: AttrR[int]
4443
equal_attr = AttrR(Int())
4544
annotated_and_equal_attr: AttrR[int] = AttrR(Int())
4645

4746
def __init__(self, sub_controller: Controller):
48-
self.attributes = {}
47+
super().__init__()
4948

50-
self.annotated_attr = AttrR(Int())
5149
self.attr_on_object = AttrR(Int())
5250

5351
self.attributes["_attributes_attr"] = AttrR(Int())
5452
self.attributes["_attributes_attr_equal"] = self.equal_attr
5553

56-
super().__init__()
5754
self.register_sub_controller("sub_controller", sub_controller)
5855

5956

@@ -63,7 +60,7 @@ def test_attribute_parsing():
6360

6461
assert set(controller.attributes.keys()) == {
6562
"_attributes_attr",
66-
"annotated_attr",
63+
"attr_on_object",
6764
"_attributes_attr_equal",
6865
"annotated_and_equal_attr",
6966
"equal_attr",
@@ -81,21 +78,15 @@ def test_attribute_parsing():
8178
}
8279

8380

84-
def test_attribute_in_both_class_and_get_attributes():
81+
def test_attribute_in_both_class_and_get_attributes_fails():
8582
class FailingController(Controller):
8683
duplicate_attribute = AttrR(Int())
8784

8885
def __init__(self):
89-
self.attributes = {"duplicate_attribute": AttrR(Int())}
9086
super().__init__()
87+
self.duplicate_attribute = AttrR(Int())
9188

92-
with pytest.raises(
93-
ValueError,
94-
match=(
95-
"`FailingController` has conflicting attribute `duplicate_attribute` "
96-
"already present in the attributes dict."
97-
),
98-
):
89+
with pytest.raises(ValueError, match="existing attribute"):
9990
FailingController()
10091

10192

0 commit comments

Comments
 (0)