Skip to content

Commit 019ae15

Browse files
authored
Merge pull request #235 from DiamondLightSource/controller-setattr
Improve Controller Attribute and sub controller creation with __setattr__
2 parents c70ae18 + 46deaa7 commit 019ae15

File tree

17 files changed

+137
-128
lines changed

17 files changed

+137
-128
lines changed

docs/conf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
("py:class", "fastcs.logging._graylog.GraylogStaticFields"),
9595
("py:class", "fastcs.logging._graylog.GraylogEnvFields"),
9696
("py:obj", "fastcs.launch.build_controller_api"),
97+
("py:obj", "fastcs.transport.epics.util.controller_pv_prefix"),
9798
("docutils", "fastcs.demo.controllers.TemperatureControllerSettings"),
9899
# TypeVar without docstrings still give warnings
99100
("py:class", "fastcs.datatypes.T_Numerical"),

docs/snippets/dynamic.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ async def initialise(self):
126126
idx + 1, ramp_parameters, self._io
127127
)
128128
await ramp_controller.initialise()
129-
self.register_sub_controller(f"Ramp{idx + 1:02d}", ramp_controller)
129+
self.add_sub_controller(f"Ramp{idx + 1:02d}", ramp_controller)
130130

131131
await self._connection.close()
132132

docs/snippets/static10.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ def __init__(self, ramp_count: int, settings: IPConnectionSettings):
7171
for index in range(1, ramp_count + 1):
7272
controller = TemperatureRampController(index, self._connection)
7373
self._ramp_controllers.append(controller)
74-
self.register_sub_controller(f"R{index}", controller)
74+
self.add_sub_controller(f"R{index}", controller)
7575

7676
async def connect(self):
7777
await self._connection.connect(self._ip_settings)

docs/snippets/static11.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ def __init__(self, ramp_count: int, settings: IPConnectionSettings):
7878
for index in range(1, ramp_count + 1):
7979
controller = TemperatureRampController(index, self._connection)
8080
self._ramp_controllers.append(controller)
81-
self.register_sub_controller(f"R{index}", controller)
81+
self.add_sub_controller(f"R{index}", controller)
8282

8383
async def connect(self):
8484
await self._connection.connect(self._ip_settings)

docs/snippets/static12.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ def __init__(self, ramp_count: int, settings: IPConnectionSettings):
8383
for index in range(1, ramp_count + 1):
8484
controller = TemperatureRampController(index, self._connection)
8585
self._ramp_controllers.append(controller)
86-
self.register_sub_controller(f"R{index}", controller)
86+
self.add_sub_controller(f"R{index}", controller)
8787

8888
async def connect(self):
8989
await self._connection.connect(self._ip_settings)

docs/snippets/static13.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ def __init__(self, ramp_count: int, settings: IPConnectionSettings):
8484
for index in range(1, ramp_count + 1):
8585
controller = TemperatureRampController(index, self._connection)
8686
self._ramp_controllers.append(controller)
87-
self.register_sub_controller(f"R{index}", controller)
87+
self.add_sub_controller(f"R{index}", controller)
8888

8989
async def connect(self):
9090
await self._connection.connect(self._ip_settings)

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: 35 additions & 21 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,22 +153,39 @@ def _validate_io(self, ios: Sequence[AttributeIO[T, AttributeIORefT]]):
164153
f"{attr.io_ref.__class__.__name__}"
165154
)
166155

167-
def register_sub_controller(self, name: str, sub_controller: Controller):
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+
172+
def add_sub_controller(self, name: str, sub_controller: Controller):
168173
if name in self.__sub_controller_tree.keys():
169174
raise ValueError(
170-
f"Controller {self} already has a sub controller registered as {name}"
175+
f"Cannot add sub controller {name}. "
176+
f"Controller {self} has existing sub controller {name}"
177+
)
178+
elif name in self.attributes:
179+
raise ValueError(
180+
f"Cannot add sub controller {name}. "
181+
f"Controller {self} has existing attribute {name}"
171182
)
172183

173-
self.__sub_controller_tree[name] = sub_controller
174184
sub_controller.set_path(self.path + [name])
185+
self.__sub_controller_tree[name] = sub_controller
186+
super().__setattr__(name, sub_controller)
175187

176188
if isinstance(sub_controller.root_attribute, Attribute):
177-
if name in self.attributes:
178-
raise TypeError(
179-
f"Cannot set sub controller `{name}` root attribute "
180-
f"on the parent controller `{type(self).__name__}` "
181-
f"as it already has an attribute of that name."
182-
)
183189
self.attributes[name] = sub_controller.root_attribute
184190

185191
def get_sub_controllers(self) -> dict[str, Controller]:
@@ -190,6 +196,14 @@ def __repr__(self):
190196
{type(self).__name__}({self.path}, {list(self.__sub_controller_tree.keys())})\
191197
"""
192198

199+
def __setattr__(self, name, value):
200+
if isinstance(value, Attribute):
201+
self.add_attribute(name, value)
202+
elif isinstance(value, Controller):
203+
self.add_sub_controller(name, value)
204+
else:
205+
super().__setattr__(name, value)
206+
193207

194208
class Controller(BaseController):
195209
"""Top-level controller for a device.

src/fastcs/demo/controllers.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ def __init__(self, settings: TemperatureControllerSettings) -> None:
8282
for index in range(1, settings.num_ramp_controllers + 1):
8383
controller = TemperatureRampController(index, self.connection)
8484
self._ramp_controllers.append(controller)
85-
self.register_sub_controller(f"R{index}", controller)
85+
self.add_sub_controller(f"R{index}", controller)
8686

8787
@command()
8888
async def cancel_all(self) -> None:

src/fastcs/transport/epics/ca/ioc.py

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
record_metadata_from_datatype,
2121
)
2222
from fastcs.transport.epics.options import EpicsIOCOptions
23+
from fastcs.transport.epics.util import controller_pv_prefix
2324
from fastcs.util import snake_to_pascal
2425

2526
EPICS_MAX_NAME_LENGTH = 60
@@ -111,27 +112,26 @@ def _add_sub_controller_pvi_info(pv_prefix: str, parent: ControllerAPI):
111112
parent: Controller to add PVI refs for
112113
113114
"""
114-
parent_pvi = ":".join([pv_prefix] + parent.path + ["PVI"])
115+
parent_pvi = f"{controller_pv_prefix(pv_prefix, parent)}:PVI"
115116

116117
for child in parent.sub_apis.values():
117-
child_pvi = ":".join([pv_prefix] + child.path + ["PVI"])
118+
child_pvi = f"{controller_pv_prefix(pv_prefix, child)}:PVI"
118119
child_name = child.path[-1].lower()
119120

120121
_add_pvi_info(child_pvi, parent_pvi, child_name)
121-
122122
_add_sub_controller_pvi_info(pv_prefix, child)
123123

124124

125125
def _create_and_link_attribute_pvs(
126-
pv_prefix: str, root_controller_api: ControllerAPI
126+
root_pv_prefix: str, root_controller_api: ControllerAPI
127127
) -> None:
128128
for controller_api in root_controller_api.walk_api():
129-
path = controller_api.path
129+
pv_prefix = controller_pv_prefix(root_pv_prefix, controller_api)
130+
130131
for attr_name, attribute in controller_api.attributes.items():
131132
pv_name = snake_to_pascal(attr_name)
132-
_pv_prefix = ":".join([pv_prefix] + path)
133-
full_pv_name_length = len(f"{_pv_prefix}:{pv_name}")
134133

134+
full_pv_name_length = len(f"{pv_prefix}:{pv_name}")
135135
if full_pv_name_length > EPICS_MAX_NAME_LENGTH:
136136
attribute.enabled = False
137137
print(
@@ -152,15 +152,15 @@ def _create_and_link_attribute_pvs(
152152
attribute.enabled = False
153153
else:
154154
_create_and_link_read_pv(
155-
_pv_prefix, f"{pv_name}_RBV", attr_name, attribute
155+
pv_prefix, f"{pv_name}_RBV", attr_name, attribute
156156
)
157157
_create_and_link_write_pv(
158-
_pv_prefix, pv_name, attr_name, attribute
158+
pv_prefix, pv_name, attr_name, attribute
159159
)
160160
case AttrR():
161-
_create_and_link_read_pv(_pv_prefix, pv_name, attr_name, attribute)
161+
_create_and_link_read_pv(pv_prefix, pv_name, attr_name, attribute)
162162
case AttrW():
163-
_create_and_link_write_pv(_pv_prefix, pv_name, attr_name, attribute)
163+
_create_and_link_write_pv(pv_prefix, pv_name, attr_name, attribute)
164164

165165

166166
def _create_and_link_read_pv(
@@ -224,32 +224,31 @@ async def async_write_display(value: T):
224224

225225
record.set(cast_to_epics_type(attribute.datatype, value), process=False)
226226

227-
record = _make_record(
228-
f"{pv_prefix}:{pv_name}", attribute, on_update=on_update, out_record=True
229-
)
227+
record = _make_record(pv, attribute, on_update=on_update, out_record=True)
230228

231229
_add_attr_pvi_info(record, pv_prefix, attr_name, "w")
232230

233231
attribute.add_write_display_callback(async_write_display)
234232

235233

236234
def _create_and_link_command_pvs(
237-
pv_prefix: str, root_controller_api: ControllerAPI
235+
root_pv_prefix: str, root_controller_api: ControllerAPI
238236
) -> None:
239237
for controller_api in root_controller_api.walk_api():
240-
path = controller_api.path
238+
pv_prefix = controller_pv_prefix(root_pv_prefix, controller_api)
239+
241240
for attr_name, method in controller_api.command_methods.items():
242241
pv_name = snake_to_pascal(attr_name)
243-
_pv_prefix = ":".join([pv_prefix] + path)
244-
if len(f"{_pv_prefix}:{pv_name}") > EPICS_MAX_NAME_LENGTH:
242+
243+
if len(f"{pv_prefix}:{pv_name}") > EPICS_MAX_NAME_LENGTH:
245244
print(
246245
f"Not creating PV for {attr_name} as full name would exceed"
247246
f" {EPICS_MAX_NAME_LENGTH} characters"
248247
)
249248
method.enabled = False
250249
else:
251250
_create_and_link_command_pv(
252-
_pv_prefix,
251+
pv_prefix,
253252
pv_name,
254253
attr_name,
255254
method,

0 commit comments

Comments
 (0)