core: factor out accessibles from init

* factor out adding of accessibles

Change-Id: I02c9d5ebc234f37be33ff0803248d05c65440c0a
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/32206
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
Reviewed-by: Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
Reviewed-by: Alexander Zaft <a.zaft@fz-juelich.de>
This commit is contained in:
Alexander Zaft
2023-09-25 16:29:10 +02:00
parent d7ab35a461
commit 7d85d85963

View File

@ -327,10 +327,6 @@ class Module(HasAccessibles):
NoneOr(FloatRange(0)), export=False, default=None) NoneOr(FloatRange(0)), export=False, default=None)
enablePoll = True enablePoll = True
# properties, parameters and commands are auto-merged upon subclassing
parameters = {}
commands = {}
# reference to the dispatcher (used for sending async updates) # reference to the dispatcher (used for sending async updates)
DISPATCHER = None DISPATCHER = None
pollInfo = None pollInfo = None
@ -351,13 +347,20 @@ class Module(HasAccessibles):
self.updateLock = threading.RLock() # for announceUpdate self.updateLock = threading.RLock() # for announceUpdate
self.polledModules = [] # modules polled by thread started in self.startModules self.polledModules = [] # modules polled by thread started in self.startModules
self.attachedModules = {} self.attachedModules = {}
errors = [] self.errors = []
self._isinitialized = False self._isinitialized = False
# handle module properties # handle module properties
# 1) make local copies of properties # 1) make local copies of properties
super().__init__() super().__init__()
# conversion from exported names to internal attribute names
self.accessiblename2attr = {}
self.writeDict = {} # values of parameters to be written
# properties, parameters and commands are auto-merged upon subclassing
self.parameters = {}
self.commands = {}
# 2) check and apply properties specified in cfgdict as # 2) check and apply properties specified in cfgdict as
# '<propertyname> = <propertyvalue>' # '<propertyname> = <propertyvalue>'
# pylint: disable=consider-using-dict-items # pylint: disable=consider-using-dict-items
@ -370,15 +373,13 @@ class Module(HasAccessibles):
else: else:
self.setProperty(key, value) self.setProperty(key, value)
except BadValueError: except BadValueError:
errors.append(f'{key}: value {value!r} does not match {self.propertyDict[key].datatype!r}!') self.errors.append(f'{key}: value {value!r} does not match {self.propertyDict[key].datatype!r}!')
# 3) set automatic properties # 3) set automatic properties
mycls, = self.__class__.__bases__ # skip the wrapper class mycls, = self.__class__.__bases__ # skip the wrapper class
myclassname = f'{mycls.__module__}.{mycls.__name__}' myclassname = f'{mycls.__module__}.{mycls.__name__}'
self.implementation = myclassname self.implementation = myclassname
# list of all 'secop' modules
# self.interface_classes = [
# b.__name__ for b in mycls.__mro__ if b.__module__.startswith('frappy.modules')]
# list of only the 'highest' secop module class # list of only the 'highest' secop module class
self.interface_classes = [ self.interface_classes = [
b.__name__ for b in mycls.__mro__ if b.__name__ in SECoP_BASE_CLASSES][:1] b.__name__ for b in mycls.__mro__ if b.__name__ in SECoP_BASE_CLASSES][:1]
@ -390,87 +391,22 @@ class Module(HasAccessibles):
# 1) make local copies of parameter objects # 1) make local copies of parameter objects
# they need to be individual per instance since we use them also # they need to be individual per instance since we use them also
# to cache the current value + qualifiers... # to cache the current value + qualifiers...
accessibles = {} # do not re-use self.accessibles as this is the same for all instances
# conversion from exported names to internal attribute names accessibles = self.accessibles
accessiblename2attr = {} self.accessibles = {}
for aname, aobj in self.accessibles.items(): for aname, aobj in accessibles.items():
# make a copy of the Parameter/Command object # make a copy of the Parameter/Command object
aobj = aobj.copy() aobj = aobj.copy()
if not self.export: # do not export parameters of a module not exported acfg = cfgdict.pop(aname, None)
aobj.export = False self._add_accessible(aname, aobj, cfg=acfg)
if aobj.export:
accessiblename2attr[aobj.export] = aname
accessibles[aname] = aobj
# do not re-use self.accessibles as this is the same for all instances
self.accessibles = accessibles
self.accessiblename2attr = accessiblename2attr
# provide properties to 'filter' out the parameters/commands
self.parameters = {k: v for k, v in accessibles.items() if isinstance(v, Parameter)}
self.commands = {k: v for k, v in accessibles.items() if isinstance(v, Command)}
# 2) check and apply parameter_properties
bad = []
for aname, cfg in cfgdict.items():
aobj = self.accessibles.get(aname, None)
if aobj:
try:
for propname, propvalue in cfg.items():
aobj.setProperty(propname, propvalue)
except KeyError:
errors.append(f"'{aname}' has no property '{propname}'")
except BadValueError as e:
errors.append(f'{aname}.{propname}: {str(e)}')
else:
bad.append(aname)
# 3) complain about names not found as accessible or property names # 3) complain about names not found as accessible or property names
if bad: if cfgdict:
errors.append( self.errors.append(
f"{', '.join(bad)} does not exist (use one of {', '.join(list(self.accessibles) + list(self.propertyDict))})") f"{', '.join(cfgdict.keys())} does not exist (use one of"
# 4) register value for writing, if given f" {', '.join(list(self.accessibles) + list(self.propertyDict))})")
# apply default when no value is given (in cfg or as Parameter argument)
# or complain, when cfg is needed
self.writeDict = {} # values of parameters to be written
for pname, pobj in self.parameters.items():
self.valueCallbacks[pname] = []
self.errorCallbacks[pname] = []
if isinstance(pobj, Limit): # 5) ensure consistency of all accessibles added here
basepname = pname.rpartition('_')[0]
baseparam = self.parameters.get(basepname)
if not baseparam:
errors.append(f'limit {pname!r} is given, but not {basepname!r}')
continue
if baseparam.datatype is None:
continue # an error will be reported on baseparam
pobj.set_datatype(baseparam.datatype)
if not pobj.hasDatatype():
errors.append(f'{pname} needs a datatype')
continue
if pobj.value is None:
if pobj.needscfg:
errors.append(f'{pname!r} has no default value and was not given in config!')
if pobj.default is None:
# we do not want to call the setter for this parameter for now,
# this should happen on the first read
pobj.readerror = ConfigError(f'parameter {pname!r} not initialized')
# above error will be triggered on activate after startup,
# when not all hardware parameters are read because of startup timeout
pobj.default = pobj.datatype.default
pobj.value = pobj.default
else:
# value given explicitly, either by cfg or as Parameter argument
pobj.given = True # for PersistentMixin
if hasattr(self, 'write_' + pname):
self.writeDict[pname] = pobj.value
if pobj.default is None:
pobj.default = pobj.value
# this checks again for datatype and sets the timestamp
setattr(self, pname, pobj.value)
# 5) ensure consistency
for aobj in self.accessibles.values(): for aobj in self.accessibles.values():
aobj.finish(self) aobj.finish(self)
@ -482,18 +418,18 @@ class Module(HasAccessibles):
self.applyMainUnit(mainunit) self.applyMainUnit(mainunit)
# 6) check complete configuration of * properties # 6) check complete configuration of * properties
if not errors: if not self.errors:
try: try:
self.checkProperties() self.checkProperties()
except ConfigError as e: except ConfigError as e:
errors.append(str(e)) self.errors.append(str(e))
for aname, aobj in self.accessibles.items(): for aname, aobj in self.accessibles.items():
try: try:
aobj.checkProperties() aobj.checkProperties()
except (ConfigError, ProgrammingError) as e: except (ConfigError, ProgrammingError) as e:
errors.append(f'{aname}: {e}') self.errors.append(f'{aname}: {e}')
if errors: if self.errors:
raise ConfigError(errors) raise ConfigError(self.errors)
# helper cfg-editor # helper cfg-editor
def __iter__(self): def __iter__(self):
@ -507,6 +443,69 @@ class Module(HasAccessibles):
for pobj in self.parameters.values(): for pobj in self.parameters.values():
pobj.datatype.set_main_unit(mainunit) pobj.datatype.set_main_unit(mainunit)
def _add_accessible(self, name, accessible, cfg=None):
if self.startModuleDone:
raise ProgrammingError('Accessibles can only be added before startModule()!')
if not self.export: # do not export parameters of a module not exported
accessible.export = False
self.accessibles[name] = accessible
if accessible.export:
self.accessiblename2attr[accessible.export] = name
if isinstance(accessible, Parameter):
self.parameters[name] = accessible
if isinstance(accessible, Command):
self.commands[name] = accessible
if cfg:
try:
for propname, propvalue in cfg.items():
accessible.setProperty(propname, propvalue)
except KeyError:
self.errors.append(f"'{name}' has no property '{propname}'")
except BadValueError as e:
self.errors.append(f'{name}.{propname}: {str(e)}')
if isinstance(accessible, Parameter):
self._handle_writes(name, accessible)
def _handle_writes(self, pname, pobj):
""" register value for writing, if given
apply default when no value is given (in cfg or as Parameter argument)
or complain, when cfg is needed
"""
self.valueCallbacks[pname] = []
self.errorCallbacks[pname] = []
if isinstance(pobj, Limit):
basepname = pname.rpartition('_')[0]
baseparam = self.parameters.get(basepname)
if not baseparam:
self.errors.append(f'limit {pname!r} is given, but not {basepname!r}')
return
if baseparam.datatype is None:
return # an error will be reported on baseparam
pobj.set_datatype(baseparam.datatype)
if not pobj.hasDatatype():
self.errors.append(f'{pname} needs a datatype')
return
if pobj.value is None:
if pobj.needscfg:
self.errors.append(f'{pname!r} has no default value and was not given in config!')
if pobj.default is None:
# we do not want to call the setter for this parameter for now,
# this should happen on the first read
pobj.readerror = ConfigError(f'parameter {pname!r} not initialized')
# above error will be triggered on activate after startup,
# when not all hardware parameters are read because of startup timeout
pobj.default = pobj.datatype.default
pobj.value = pobj.default
else:
# value given explicitly, either by cfg or as Parameter argument
pobj.given = True # for PersistentMixin
if hasattr(self, 'write_' + pname):
self.writeDict[pname] = pobj.value
if pobj.default is None:
pobj.default = pobj.value
# this checks again for datatype and sets the timestamp
setattr(self, pname, pobj.value)
def announceUpdate(self, pname, value=None, err=None, timestamp=None, validate=True): def announceUpdate(self, pname, value=None, err=None, timestamp=None, validate=True):
"""announce a changed value or readerror """announce a changed value or readerror
@ -630,6 +629,8 @@ class Module(HasAccessibles):
registers it in the server for waiting registers it in the server for waiting
<timeout> defaults to 30 seconds <timeout> defaults to 30 seconds
""" """
# we do not need self.errors any longer. should we delete it?
# del self.errors
if self.polledModules: if self.polledModules:
mkthread(self.__pollThread, self.polledModules, start_events.get_trigger()) mkthread(self.__pollThread, self.polledModules, start_events.get_trigger())
self.startModuleDone = True self.startModuleDone = True