1313
1414
1515class BaseController (Tracer ):
16- """Base class for controller."""
16+ """Base class for controllers
1717
18- #: Attributes passed from the device at runtime.
19- attributes : dict [str , Attribute ]
20- root_attribute : Attribute | None = None
18+ Instances of this class can be loaded into FastCS to expose its Attributes to
19+ the transport layer, which can then perform a specific function such as generating a
20+ UI or creating parameters for a control system.
21+
22+ This class is public for type hinting purposes, but should not be inherited to
23+ implement device drivers. Use either ``Controller`` or ``ControllerVector`` instead.
24+
25+ """
2126
27+ # These class attributes can be overridden on child classes to define default
28+ # behaviour of instantiated controllers
29+ root_attribute : Attribute | None = None
2230 description : str | None = None
2331
2432 def __init__ (
@@ -29,17 +37,17 @@ def __init__(
2937 ) -> None :
3038 super ().__init__ ()
3139
32- if (
33- description is not None
34- ): # Use the argument over the one class defined description.
40+ if description is not None :
41+ # Use the argument over the one class defined description.
3542 self .description = description
3643
37- if not hasattr (self , "attributes" ):
38- self .attributes = {}
3944 self ._path : list [str ] = path or []
40- self .__sub_controller_tree : dict [str , BaseController ] = {}
4145
46+ # Internal state that should not be accessed directly by base classes
47+ self .__attributes : dict [str , Attribute ] = {}
48+ self .__sub_controllers : dict [str , BaseController ] = {}
4249 self .__hinted_attributes = self ._parse_attribute_type_hints ()
50+
4351 self ._bind_attrs ()
4452
4553 ios = ios or []
@@ -62,11 +70,73 @@ def _parse_attribute_type_hints(
6270
6371 return hinted_attributes
6472
73+ def _bind_attrs (self ) -> None :
74+ """Search for Attributes and Methods to bind them to this instance.
75+
76+ This method will search the attributes of this controller class to bind them to
77+ this specific instance. For Attributes, this is just a case of copying and
78+ re-assigning to ``self`` to make it unique across multiple instances of this
79+ controller class. For Methods, this requires creating a bound method from a
80+ class method and a controller instance, so that it can be called from any
81+ context with the controller instance passed as the ``self`` argument.
82+
83+ """
84+ # Lazy import to avoid circular references
85+ from fastcs .cs_methods import UnboundCommand , UnboundScan
86+
87+ # Using a dictionary instead of a set to maintain order.
88+ class_dir = {key : None for key in dir (type (self )) if not key .startswith ("_" )}
89+ class_type_hints = {
90+ key : value
91+ for key , value in get_type_hints (type (self )).items ()
92+ if not key .startswith ("_" )
93+ }
94+
95+ for attr_name in {** class_dir , ** class_type_hints }:
96+ if attr_name == "root_attribute" :
97+ continue
98+
99+ attr = getattr (self , attr_name , None )
100+ if isinstance (attr , Attribute ):
101+ setattr (self , attr_name , deepcopy (attr ))
102+ elif isinstance (attr , UnboundScan | UnboundCommand ):
103+ setattr (self , attr_name , attr .bind (self ))
104+
105+ def _validate_io (self , ios : Sequence [AttributeIO [T , AttributeIORefT ]]):
106+ """Validate that there is exactly one AttributeIO class registered to the
107+ controller for each type of AttributeIORef belonging to the attributes of the
108+ controller"""
109+ for ref_type , count in Counter ([io .ref_type for io in ios ]).items ():
110+ if count > 1 :
111+ raise RuntimeError (
112+ f"More than one AttributeIO class handles { ref_type .__name__ } "
113+ )
114+
115+ def __repr__ (self ):
116+ name = self .__class__ .__name__
117+ path = "." .join (self .path ) or None
118+ sub_controllers = list (self .sub_controllers .keys ()) or None
119+
120+ return f"{ name } (path={ path } , sub_controllers={ sub_controllers } )"
121+
122+ def __setattr__ (self , name , value ):
123+ if isinstance (value , Attribute ):
124+ self .add_attribute (name , value )
125+ elif isinstance (value , Controller ):
126+ self .add_sub_controller (name , value )
127+ else :
128+ super ().__setattr__ (name , value )
129+
65130 async def initialise (self ):
66- """Hook to dynamically add attributes before building the API"""
131+ """Hook for subclasses to dynamically add attributes before building the API"""
67132 pass
68133
69- def validate_hinted_attributes (self ):
134+ def post_initialise (self ):
135+ """Hook to call after all attributes added, before serving the application"""
136+ self ._validate_hinted_attributes ()
137+ self ._connect_attribute_ios ()
138+
139+ def _validate_hinted_attributes (self ):
70140 """Validate ``Attribute`` type-hints were introspected during initialisation"""
71141 for name in self .__hinted_attributes :
72142 attr = getattr (self , name , None )
@@ -77,11 +147,11 @@ def validate_hinted_attributes(self):
77147 )
78148
79149 for subcontroller in self .sub_controllers .values ():
80- subcontroller .validate_hinted_attributes () # noqa: SLF001
150+ subcontroller ._validate_hinted_attributes () # noqa: SLF001
81151
82- def connect_attribute_ios (self ) -> None :
152+ def _connect_attribute_ios (self ) -> None :
83153 """Connect ``Attribute`` callbacks to ``AttributeIO``s"""
84- for attr in self .attributes .values ():
154+ for attr in self .__attributes .values ():
85155 ref = attr .io_ref if attr .has_io_ref () else None
86156 if ref is None :
87157 continue
@@ -99,7 +169,7 @@ def connect_attribute_ios(self) -> None:
99169 attr .set_update_callback (io .update )
100170
101171 for controller in self .sub_controllers .values ():
102- controller .connect_attribute_ios ()
172+ controller ._connect_attribute_ios () # noqa: SLF001
103173
104174 @property
105175 def path (self ) -> list [str ]:
@@ -111,57 +181,15 @@ def set_path(self, path: list[str]):
111181 raise ValueError (f"sub controller is already registered under { self .path } " )
112182
113183 self ._path = path
114- for attribute in self .attributes .values ():
184+ for attribute in self .__attributes .values ():
115185 attribute .set_path (path )
116186
117- def _bind_attrs (self ) -> None :
118- """Search for `Attributes` and `Methods` to bind them to this instance.
119-
120- This method will search the attributes of this controller class to bind them to
121- this specific instance. For `Attribute`s, this is just a case of copying and
122- re-assigning to `self` to make it unique across multiple instances of this
123- controller class. For `Method`s, this requires creating a bound method from a
124- class method and a controller instance, so that it can be called from any
125- context with the controller instance passed as the `self` argument.
126-
127- """
128- # Lazy import to avoid circular references
129- from fastcs .cs_methods import UnboundCommand , UnboundScan
130-
131- # Using a dictionary instead of a set to maintain order.
132- class_dir = {key : None for key in dir (type (self )) if not key .startswith ("_" )}
133- class_type_hints = {
134- key : value
135- for key , value in get_type_hints (type (self )).items ()
136- if not key .startswith ("_" )
137- }
138-
139- for attr_name in {** class_dir , ** class_type_hints }:
140- if attr_name == "root_attribute" :
141- continue
142-
143- attr = getattr (self , attr_name , None )
144- if isinstance (attr , Attribute ):
145- setattr (self , attr_name , deepcopy (attr ))
146- elif isinstance (attr , UnboundScan | UnboundCommand ):
147- setattr (self , attr_name , attr .bind (self ))
148-
149- def _validate_io (self , ios : Sequence [AttributeIO [T , AttributeIORefT ]]):
150- """Validate that there is exactly one AttributeIO class registered to the
151- controller for each type of AttributeIORef belonging to the attributes of the
152- controller"""
153- for ref_type , count in Counter ([io .ref_type for io in ios ]).items ():
154- if count > 1 :
155- raise RuntimeError (
156- f"More than one AttributeIO class handles { ref_type .__name__ } "
157- )
158-
159187 def add_attribute (self , name , attr : Attribute ):
160- if name in self .attributes :
188+ if name in self .__attributes :
161189 raise ValueError (
162190 f"Cannot add attribute { attr } . "
163191 f"Controller { self } has has existing attribute { name } : "
164- f"{ self .attributes [name ]} "
192+ f"{ self .__attributes [name ]} "
165193 )
166194 elif name in self .__hinted_attributes :
167195 attr_class , attr_dtype = self .__hinted_attributes [name ]
@@ -178,67 +206,50 @@ def add_attribute(self, name, attr: Attribute):
178206 f"Expected '{ attr_dtype .__name__ } ', "
179207 f"got '{ attr .datatype .dtype .__name__ } '."
180208 )
181- elif name in self .__sub_controller_tree .keys ():
209+ elif name in self .__sub_controllers .keys ():
182210 raise ValueError (
183211 f"Cannot add attribute { attr } . "
184212 f"Controller { self } has existing sub controller { name } : "
185- f"{ self .__sub_controller_tree [name ]} "
213+ f"{ self .__sub_controllers [name ]} "
186214 )
187215
188216 attr .set_name (name )
189217 attr .set_path (self .path )
190- self .attributes [name ] = attr
218+ self .__attributes [name ] = attr
191219 super ().__setattr__ (name , attr )
192220
221+ @property
222+ def attributes (self ) -> dict [str , Attribute ]:
223+ return self .__attributes
224+
193225 def add_sub_controller (self , name : str , sub_controller : BaseController ):
194- if name in self .__sub_controller_tree .keys ():
226+ if name in self .__sub_controllers .keys ():
195227 raise ValueError (
196228 f"Cannot add sub controller { sub_controller } . "
197229 f"Controller { self } has existing sub controller { name } : "
198- f"{ self .__sub_controller_tree [name ]} "
230+ f"{ self .__sub_controllers [name ]} "
199231 )
200- elif name in self .attributes :
232+ elif name in self .__attributes :
201233 raise ValueError (
202234 f"Cannot add sub controller { sub_controller } . "
203235 f"Controller { self } has existing attribute { name } : "
204- f"{ self .attributes [name ]} "
236+ f"{ self .__attributes [name ]} "
205237 )
206238
207239 sub_controller .set_path (self .path + [name ])
208- self .__sub_controller_tree [name ] = sub_controller
240+ self .__sub_controllers [name ] = sub_controller
209241 super ().__setattr__ (name , sub_controller )
210242
211243 if isinstance (sub_controller .root_attribute , Attribute ):
212- self .attributes [name ] = sub_controller .root_attribute
244+ self .__attributes [name ] = sub_controller .root_attribute
213245
214246 @property
215247 def sub_controllers (self ) -> dict [str , BaseController ]:
216- return self .__sub_controller_tree
217-
218- def __repr__ (self ):
219- name = self .__class__ .__name__
220- path = "." .join (self .path ) or None
221- sub_controllers = list (self .sub_controllers .keys ()) or None
222-
223- return f"{ name } (path={ path } , sub_controllers={ sub_controllers } )"
224-
225- def __setattr__ (self , name , value ):
226- if isinstance (value , Attribute ):
227- self .add_attribute (name , value )
228- elif isinstance (value , Controller ):
229- self .add_sub_controller (name , value )
230- else :
231- super ().__setattr__ (name , value )
248+ return self .__sub_controllers
232249
233250
234251class Controller (BaseController ):
235- """Top-level controller for a device.
236-
237- This is the primary class for implementing device support in FastCS. Instances of
238- this class can be loaded into a FastCS to expose its ``Attribute``s to the transport
239- layer, which can then perform a specific function with the set of ``Attributes``,
240- such as generating a UI or creating parameters for a control system.
241- """
252+ """Controller containing Attributes and named sub Controllers"""
242253
243254 def __init__ (
244255 self ,
@@ -263,8 +274,12 @@ async def disconnect(self) -> None:
263274
264275
265276class ControllerVector (MutableMapping [int , Controller ], BaseController ):
266- """A controller with a collection of identical sub controllers distinguished
267- by a numeric value"""
277+ """Controller containing Attributes and indexed sub Controllers
278+
279+ The sub controllers registered with this Controller should be instances of the same
280+ Controller type, distinguished only by an integer index. The indexes do not need
281+ to be continiguous.
282+ """
268283
269284 def __init__ (
270285 self ,
0 commit comments