Skip to content

Commit b0ff1f8

Browse files
authored
Merge pull request #233 from DiamondLightSource/tidying
Tidying
2 parents 469c501 + fbc354d commit b0ff1f8

File tree

10 files changed

+101
-49
lines changed

10 files changed

+101
-49
lines changed

src/fastcs/attributes.py

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,12 @@ def __init__(
4141
self.description = description
4242

4343
# A callback to use when setting the datatype to a different value, for example
44-
# changing the units on an int. This should be implemented in the backend.
44+
# changing the units on an int.
4545
self._update_datatype_callbacks: list[Callable[[DataType[T]], None]] = []
4646

47-
# Name to be filled in by Controller when the Attribute is bound
48-
self._name = None
47+
# Path and name to be filled in by Controller it is bound to
48+
self._name = ""
49+
self._path = []
4950

5051
@property
5152
def io_ref(self) -> AttributeIORefT:
@@ -68,6 +69,14 @@ def dtype(self) -> type[T]:
6869
def group(self) -> str | None:
6970
return self._group
7071

72+
@property
73+
def name(self) -> str:
74+
return self._name
75+
76+
@property
77+
def path(self) -> list[str]:
78+
return self._path
79+
7180
def add_update_datatype_callback(
7281
self, callback: Callable[[DataType[T]], None]
7382
) -> None:
@@ -82,16 +91,28 @@ def update_datatype(self, datatype: DataType[T]) -> None:
8291
for callback in self._update_datatype_callbacks:
8392
callback(datatype)
8493

85-
def set_name(self, name: list[str]):
94+
def set_name(self, name: str):
8695
if self._name:
87-
raise ValueError(
96+
raise RuntimeError(
8897
f"Attribute is already registered with a controller as {self._name}"
8998
)
9099

91100
self._name = name
92101

102+
def set_path(self, path: list[str]):
103+
if self._path:
104+
raise RuntimeError(
105+
f"Attribute is already registered with a controller at {self._path}"
106+
)
107+
108+
self._path = path
109+
93110
def __repr__(self):
94-
return f"{self.__class__.__name__}({self._name}, {self._datatype})"
111+
name = self.__class__.__name__
112+
path = ".".join(self._path + [self._name]) or None
113+
datatype = self._datatype.__class__.__name__
114+
115+
return f"{name}(path={path}, datatype={datatype}, io_ref={self._io_ref})"
95116

96117

97118
AttrIOUpdateCallback = Callable[["AttrR[T, Any]"], Awaitable[None]]

src/fastcs/control_system.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,7 @@ def _build_controller_api(controller: BaseController, path: list[str]) -> Contro
200200
command_methods=command_methods,
201201
sub_apis={
202202
name: _build_controller_api(sub_controller, path + [name])
203-
for name, sub_controller in controller.get_sub_controllers().items()
203+
for name, sub_controller in controller.sub_controllers.items()
204204
},
205205
description=controller.description,
206206
)

src/fastcs/controller.py

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ def connect_attribute_ios(self) -> None:
6767
if isinstance(attr, AttrR):
6868
attr.set_update_callback(io.update)
6969

70-
for controller in self.get_sub_controllers().values():
70+
for controller in self.sub_controllers.values():
7171
controller.connect_attribute_ios()
7272

7373
@property
@@ -80,6 +80,8 @@ def set_path(self, path: list[str]):
8080
raise ValueError(f"sub controller is already registered under {self.path}")
8181

8282
self._path = path
83+
for attribute in self.attributes.values():
84+
attribute.set_path(path)
8385

8486
def _bind_attrs(self) -> None:
8587
"""Search for `Attributes` and `Methods` to bind them to this instance.
@@ -136,6 +138,7 @@ def add_attribute(self, name, attribute: Attribute):
136138
)
137139

138140
attribute.set_name(name)
141+
attribute.set_path(self.path)
139142
self.attributes[name] = attribute
140143
super().__setattr__(name, attribute)
141144

@@ -158,13 +161,16 @@ def add_sub_controller(self, name: str, sub_controller: Controller):
158161
if isinstance(sub_controller.root_attribute, Attribute):
159162
self.attributes[name] = sub_controller.root_attribute
160163

161-
def get_sub_controllers(self) -> dict[str, Controller]:
164+
@property
165+
def sub_controllers(self) -> dict[str, Controller]:
162166
return self.__sub_controller_tree
163167

164168
def __repr__(self):
165-
return f"""\
166-
{type(self).__name__}({self.path}, {list(self.__sub_controller_tree.keys())})\
167-
"""
169+
name = self.__class__.__name__
170+
path = ".".join(self.path) or None
171+
sub_controllers = list(self.sub_controllers.keys()) or None
172+
173+
return f"{name}(path={path}, sub_controllers={sub_controllers})"
168174

169175
def __setattr__(self, name, value):
170176
if isinstance(value, Attribute):
@@ -179,9 +185,9 @@ class Controller(BaseController):
179185
"""Top-level controller for a device.
180186
181187
This is the primary class for implementing device support in FastCS. Instances of
182-
this class can be loaded into a backend to access its ``Attribute``s. The backend
183-
can then perform a specific function with the set of ``Attributes``, such as
184-
generating a UI or creating parameters for a control system.
188+
this class can be loaded into a FastCS to expose its ``Attribute``s to the transport
189+
layer, which can then perform a specific function with the set of ``Attributes``,
190+
such as generating a UI or creating parameters for a control system.
185191
"""
186192

187193
root_attribute: Attribute | None = None

src/fastcs/controller_api.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,18 +49,23 @@ def get_scan_and_initial_coros(
4949
initial_coros: list[Callable] = []
5050

5151
for controller_api in self.walk_api():
52-
_add_scan_method_tasks(scan_dict, controller_api)
52+
_add_scan_method_tasks(scan_dict, initial_coros, controller_api)
5353
_add_attribute_update_tasks(scan_dict, initial_coros, controller_api)
5454

5555
scan_coros = _get_periodic_scan_coros(scan_dict)
5656
return scan_coros, initial_coros
5757

5858

5959
def _add_scan_method_tasks(
60-
scan_dict: dict[float, list[Callable]], controller_api: ControllerAPI
60+
scan_dict: dict[float, list[Callable]],
61+
initial_coros: list[Callable],
62+
controller_api: ControllerAPI,
6163
):
6264
for method in controller_api.scan_methods.values():
63-
scan_dict[method.period].append(method.fn)
65+
if method.period is ONCE:
66+
initial_coros.append(method.fn)
67+
else:
68+
scan_dict[method.period].append(method.fn)
6469

6570

6671
def _add_attribute_update_tasks(

src/fastcs/datatypes.py

Lines changed: 32 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -67,13 +67,16 @@ class _Numerical(DataType[T_Numerical]):
6767
min_alarm: T_Numerical | None = None
6868
max_alarm: T_Numerical | None = None
6969

70-
def validate(self, value: T_Numerical) -> T_Numerical:
71-
super().validate(value)
72-
if self.min is not None and value < self.min:
73-
raise ValueError(f"Value {value} is less than minimum {self.min}")
74-
if self.max is not None and value > self.max:
75-
raise ValueError(f"Value {value} is greater than maximum {self.max}")
76-
return value
70+
def validate(self, value: Any) -> T_Numerical:
71+
_value = super().validate(value)
72+
73+
if self.min is not None and _value < self.min:
74+
raise ValueError(f"Value {_value} is less than minimum {self.min}")
75+
76+
if self.max is not None and _value > self.max:
77+
raise ValueError(f"Value {_value} is greater than maximum {self.max}")
78+
79+
return _value
7780

7881
@property
7982
def initial_value(self) -> T_Numerical:
@@ -99,11 +102,13 @@ class Float(_Numerical[float]):
99102
def dtype(self) -> type[float]:
100103
return float
101104

102-
def validate(self, value: float) -> float:
103-
super().validate(value)
105+
def validate(self, value: Any) -> float:
106+
_value = super().validate(value)
107+
104108
if self.prec is not None:
105-
value = round(value, self.prec)
106-
return value
109+
_value = round(_value, self.prec)
110+
111+
return _value
107112

108113

109114
@dataclass(frozen=True)
@@ -177,21 +182,24 @@ def initial_value(self) -> np.ndarray:
177182
return np.zeros(self.shape, dtype=self.array_dtype)
178183

179184
def validate(self, value: np.ndarray) -> np.ndarray:
180-
super().validate(value)
181-
if self.array_dtype != value.dtype:
185+
_value = super().validate(value)
186+
187+
if self.array_dtype != _value.dtype:
182188
raise ValueError(
183-
f"Value dtype {value.dtype} is not the same as the array dtype "
189+
f"Value dtype {_value.dtype} is not the same as the array dtype "
184190
f"{self.array_dtype}"
185191
)
186-
if len(self.shape) != len(value.shape) or any(
192+
193+
if len(self.shape) != len(_value.shape) or any(
187194
shape1 > shape2
188-
for shape1, shape2 in zip(value.shape, self.shape, strict=True)
195+
for shape1, shape2 in zip(_value.shape, self.shape, strict=True)
189196
):
190197
raise ValueError(
191-
f"Value shape {value.shape} exceeeds the shape maximum shape "
198+
f"Value shape {_value.shape} exceeeds the shape maximum shape "
192199
f"{self.shape}"
193200
)
194-
return value
201+
202+
return _value
195203

196204

197205
@dataclass(frozen=True)
@@ -207,12 +215,13 @@ def dtype(self) -> type[np.ndarray]:
207215
def initial_value(self) -> np.ndarray:
208216
return np.array([], dtype=self.structured_dtype)
209217

210-
def validate(self, value: np.ndarray) -> np.ndarray:
211-
super().validate(value)
218+
def validate(self, value: Any) -> np.ndarray:
219+
_value = super().validate(value)
212220

213-
if self.structured_dtype != value.dtype:
221+
if self.structured_dtype != _value.dtype:
214222
raise ValueError(
215-
f"Value dtype {value.dtype.descr} is not the same as the structured "
223+
f"Value dtype {_value.dtype.descr} is not the same as the structured "
216224
f"dtype {self.structured_dtype}"
217225
)
218-
return value
226+
227+
return _value

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ def record_metadata_from_datatype(
7171
case Waveform():
7272
if len(datatype.shape) != 1:
7373
raise TypeError(
74-
f"Unsupported shape {datatype.shape}, the EPICS backend only "
74+
f"Unsupported shape {datatype.shape}, the EPICS transport only "
7575
"supports to 1D arrays"
7676
)
7777
arguments["length"] = datatype.shape[0]

src/fastcs/transport/epics/gui.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ def _get_attribute_component(
100100
return None
101101
return SignalRW(
102102
name=name,
103+
description=attribute.description,
103104
write_pv=pv,
104105
write_widget=write_widget,
105106
read_pv=pv + "_RBV",
@@ -109,12 +110,22 @@ def _get_attribute_component(
109110
read_widget = self._get_read_widget(attribute.datatype)
110111
if read_widget is None:
111112
return None
112-
return SignalR(name=name, read_pv=pv, read_widget=read_widget)
113+
return SignalR(
114+
name=name,
115+
description=attribute.description,
116+
read_pv=pv,
117+
read_widget=read_widget,
118+
)
113119
case AttrW():
114120
write_widget = self._get_write_widget(attribute.datatype)
115121
if write_widget is None:
116122
return None
117-
return SignalW(name=name, write_pv=pv, write_widget=write_widget)
123+
return SignalW(
124+
name=name,
125+
description=attribute.description,
126+
write_pv=pv,
127+
write_widget=write_widget,
128+
)
118129
case _:
119130
raise FastCSError(f"Unsupported attribute type: {type(attribute)}")
120131

tests/conftest.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ def run_ioc_as_subprocess(
150150
if not error_queue.empty():
151151
raise error_queue.get()
152152

153-
# close backend caches before the event loop
153+
# close ca caches before the event loop
154154
purge_channel_caches()
155155

156156
error_queue.close()
@@ -224,7 +224,7 @@ def test_controller(tango_system, register_device):
224224
if time.monotonic() - start_time > timeout:
225225
raise TimeoutError("Controller did not start in time")
226226

227-
# close backend caches before the event loop
227+
# close ca caches before the event loop
228228
purge_channel_caches()
229229

230230
# Stop buffer from getting full and blocking the subprocess

tests/test_controller.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ def test_controller_nesting():
1515

1616
assert sub_controller.path == ["a"]
1717
assert sub_sub_controller.path == ["a", "b"]
18-
assert controller.get_sub_controllers() == {"a": sub_controller}
19-
assert sub_controller.get_sub_controllers() == {"b": sub_sub_controller}
18+
assert controller.sub_controllers == {"a": sub_controller}
19+
assert sub_controller.sub_controllers == {"b": sub_sub_controller}
2020

2121
with pytest.raises(ValueError, match=r"existing sub controller"):
2222
controller.a = Controller()

tests/transport/epics/pva/test_p4p.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ async def test_scan_method(p4p_subprocess: tuple[str, Queue]):
7171
e_values = asyncio.Queue()
7272

7373
# While the scan method will update every 0.1 seconds, it takes around that
74-
# time for the p4p backends to update, broadcast, get.
74+
# time for the p4p transport to update, broadcast, get.
7575
latency = 1e8
7676

7777
e_monitor = ctxt.monitor(f"{pv_prefix}:Child1:E", e_values.put)

0 commit comments

Comments
 (0)