rework property handling

+ DataType validators are shifted to __call__
+ as_json is moved to export_datatape()
+ new HasProperties Base Mixin for Modules/DataTypes
+ accessibles can be accessed via iterator of a module
+ properties are properly 'derived' and checked, are set with .setPropertyValue

remember: parameters only have properties, so use getPropertyValue()

Change-Id: Iae0273f971aacb00fe6bf05e6a4d24a6d1be881a
Reviewed-on: https://forge.frm2.tum.de/review/20635
Tested-by: JenkinsCodeReview <bjoern_pedersen@frm2.tum.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
This commit is contained in:
Enrico Faulhaber
2019-05-14 16:08:20 +02:00
committed by Markus Zolliker
parent 155dd8e4c6
commit f6d8f823d9
23 changed files with 956 additions and 465 deletions

View File

@ -27,14 +27,17 @@ import sys
import time
from collections import OrderedDict
from secop.datatypes import EnumType, FloatRange, \
StringType, TupleOf, get_datatype
from secop.datatypes import EnumType, FloatRange, BoolType, IntRange, \
StringType, TupleOf, get_datatype, ArrayOf
from secop.errors import ConfigError, ProgrammingError
from secop.lib import formatException, \
formatExtendedStack, mkthread, unset_value
from secop.lib.enum import Enum
from secop.metaclass import ModuleMeta, add_metaclass
from secop.params import PREDEFINED_ACCESSIBLES, Command, Override, Parameter
from secop.lib.metaclass import add_metaclass
from secop.metaclass import ModuleMeta
from secop.params import PREDEFINED_ACCESSIBLES, Command, Override, Parameter, Parameters, Commands
from secop.properties import HasProperties, Property
# XXX: connect with 'protocol'-Modules.
# Idea: every Module defined herein is also a 'protocol'-Module,
@ -43,7 +46,7 @@ from secop.params import PREDEFINED_ACCESSIBLES, Command, Override, Parameter
@add_metaclass(ModuleMeta)
class Module(object):
class Module(HasProperties):
"""Basic Module
ALL secop Modules derive from this
@ -61,16 +64,17 @@ class Module(object):
# static properties, definitions in derived classes should overwrite earlier ones.
# note: properties don't change after startup and are usually filled
# with data from a cfg file...
# note: so far all properties are STRINGS
# note: only the properties defined here are allowed to be set in the cfg file
# note: only the properties predefined here are allowed to be set in the cfg file
# note: the names map to a [datatype, value] list, value comes from the cfg file,
# datatype is fixed!
properties = {
'export': True, # should be exported remotely?
'group': None, # some Modules may be grouped together
'description': "Short description of this Module class and its functionality.",
'meaning': None, # XXX: ???
'priority': None, # XXX: ???
'visibility': None, # XXX: ????
'export': Property(BoolType(), default=True, export=False),
'group': Property(StringType(), default='', extname='group'),
'description': Property(StringType(), extname='description', mandatory=True),
'meaning': Property(TupleOf(StringType(),IntRange(0,50)), default=('',0), extname='meaning'),
'visibility': Property(EnumType('visibility', user=1, advanced=2, expert=3), default=1, extname='visibility'),
'implementation': Property(StringType(), extname='implementation'),
'interface_class': Property(ArrayOf(StringType()), extname='interface_class'),
# what else?
}
@ -89,32 +93,26 @@ class Module(object):
# handle module properties
# 1) make local copies of properties
# XXX: self.properties = self.properties.copy() ???
props = {}
for k, v in list(self.properties.items()):
props[k] = v
self.properties = props
super(Module, self).__init__()
# 2) check and apply properties specified in cfgdict
# specified as '.<propertyname> = <propertyvalue>'
for k, v in list(cfgdict.items()): # keep list() as dict may change during iter
if k[0] == '.':
if k[1:] in self.properties:
self.properties[k[1:]] = cfgdict.pop(k)
elif k[1] == '_':
self.properties[k[1:]] = cfgdict.pop(k)
if k[1:] in self.__class__.properties:
self.setProperty(k[1:], cfgdict.pop(k))
else:
raise ConfigError('Module %r has no property %r' %
(self.name, k[1:]))
# 3) remove unset (default) module properties
for k, v in list(self.properties.items()): # keep list() as dict may change during iter
if v is None:
del self.properties[k]
# 4) set automatic properties
mycls = self.__class__
myclassname = '%s.%s' % (mycls.__module__, mycls.__name__)
self.properties['_implementation'] = myclassname
self.properties['implementation'] = myclassname
# list of all 'secop' modules
self.properties['interface_class'] = [
b.__name__ for b in mycls.__mro__ if b.__module__.startswith('secop.modules')]
# list of only the 'highest' secop module class
self.properties['interface_class'] = [[
b.__name__ for b in mycls.__mro__ if b.__module__.startswith('secop.modules')][0]]
@ -136,20 +134,20 @@ class Module(object):
predefined_obj = PREDEFINED_ACCESSIBLES.get(aname, None)
if predefined_obj:
if isinstance(aobj, predefined_obj):
aobj.export = aname
aobj.setProperty('export', aname)
else:
raise ProgrammingError("can not use '%s' as name of a %s" %
(aname, aobj.__class__.__name__))
else: # create custom parameter
aobj.export = '_' + aname
aobj.setProperty('export', '_' + aname)
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 = dict((k,v) for k,v in accessibles.items() if isinstance(v, Parameter))
self.commands = dict((k,v) for k,v in accessibles.items() if isinstance(v, Command))
self.parameters = Parameters((k,v) for k,v in accessibles.items() if isinstance(v, Parameter))
self.commands = Commands((k,v) for k,v in accessibles.items() if isinstance(v, Command))
# 2) check and apply parameter_properties
# specified as '<paramname>.<propertyname> = <propertyvalue>'
@ -160,9 +158,9 @@ class Module(object):
# paramobj might also be a command (not sure if this is needed)
if paramobj:
if propname == 'datatype':
paramobj.datatype = get_datatype(cfgdict.pop(k))
elif hasattr(paramobj, propname):
setattr(paramobj, propname, cfgdict.pop(k))
paramobj.setProperty('datatype', get_datatype(cfgdict.pop(k)))
elif propname in paramobj.__class__.properties:
paramobj.setProperty(propname, cfgdict.pop(k))
else:
raise ConfigError('Module %s: Parameter %r has no property %r!' %
(self.name, paramname, propname))
@ -194,8 +192,8 @@ class Module(object):
# apply datatype, complain if type does not fit
datatype = self.parameters[k].datatype
try:
v = datatype.validate(v)
self.parameters[k].default = v
v = datatype(v)
self.parameters[k].value = v
except (ValueError, TypeError):
self.log.exception(formatExtendedStack())
raise
@ -212,6 +210,18 @@ class Module(object):
if '$' in v.unit:
v.unit = v.unit.replace('$', self.parameters['value'].unit)
# 6) check complete configuration of * properties
self.checkProperties()
for p in self.parameters.values():
p.checkProperties()
# helper cfg-editor
def __iter__(self):
return self.accessibles.__iter__()
def __getitem__(self, item):
return self.accessibles.__getitem__(item)
def isBusy(self):
'''helper function for treating substates of BUSY correctly'''
# defined even for non drivable (used for dynamic polling)
@ -247,10 +257,9 @@ class Readable(Module):
Status = Enum('Status',
IDLE = 100,
WARN = 200,
UNSTABLE = 250,
UNSTABLE = 270,
ERROR = 400,
DISABLED = 500,
UNKNOWN = 0,
DISABLED = 0,
)
parameters = {
'value': Parameter('current value of the Module', readonly=True,