merge 'parameters' and 'commands' to 'accessibles'

- for now, the definition also accepts the old syntax
  (to be changed later)
- Commands have datatype CommandType
- do not need keyword for the decription parameter of Override
- issue a Warning when a Parameter is overwritten without Overrride
  (this should be turned into an error message)
-

Change-Id: Ib2c0f520abb5b4d7e6aed4d77a0d2b8bc470a85a
Reviewed-on: https://forge.frm2.tum.de/review/18251
Tested-by: JenkinsCodeReview <bjoern_pedersen@frm2.tum.de>
Reviewed-by: Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
This commit is contained in:
2018-06-25 13:45:15 +02:00
parent 807f821968
commit fb1939d5c8
10 changed files with 122 additions and 104 deletions

View File

@ -41,7 +41,7 @@ except ImportError:
import mlzlog
from secop.datatypes import get_datatype, EnumType
from secop.datatypes import get_datatype, EnumType, CommandType
from secop.lib import mkthread, formatException, formatExtendedStack
from secop.lib.parsing import parse_time, format_time
#from secop.protocol.encoding import ENCODERS
@ -342,7 +342,7 @@ class Client(object):
return self.describingModulesData[module]
def _getDescribingParameterData(self, module, parameter):
return self._getDescribingModuleData(module)['parameters'][parameter]
return self._getDescribingModuleData(module)['accessibles'][parameter]
def _decode_list_to_ordereddict(self, data):
# takes a list of 2*N <key>, <value> entries and
@ -371,7 +371,7 @@ class Client(object):
['modules'], describing_data)
for modname, module in list(describing_data['modules'].items()):
describing_data['modules'][modname] = self._decode_substruct(
['parameters', 'commands'], module)
['accessibles'], module)
self.describing_data = describing_data
# import pprint
@ -382,18 +382,15 @@ class Client(object):
# pprint.pprint(r(describing_data))
for module, moduleData in self.describing_data['modules'].items():
for parameter, parameterData in moduleData['parameters'].items():
datatype = get_datatype(parameterData['datatype'])
for aname, adata in moduleData['accessibles'].items():
datatype = get_datatype(adata['datatype'])
# *sigh* special handling for 'some' parameters....
if isinstance(datatype, EnumType):
datatype._enum.name = parameter
if parameter == 'status':
datatype.subtypes[0]._enum.name = 'status'
self.describing_data['modules'][module]['parameters'] \
[parameter]['datatype'] = datatype
for _cmdname, cmdData in moduleData['commands'].items():
cmdData['arguments'] = list(map(get_datatype, cmdData['arguments']))
cmdData['resulttype'] = get_datatype(cmdData['resulttype'])
datatype._enum.name = aname
if aname == 'status':
datatype.subtypes[0]._enum.name = 'Status'
self.describing_data['modules'][module]['accessibles'] \
[aname]['datatype'] = datatype
except Exception as _exc:
print(formatException(verbose=True))
raise
@ -551,7 +548,9 @@ class Client(object):
return list(self.describing_data['modules'].keys())
def getParameters(self, module):
return list(self.describing_data['modules'][module]['parameters'].keys())
params = filter(lambda item: not isinstance(item[1]['datatype'], CommandType),
self.describing_data['modules'][module]['accessibles'].items())
return list(param[0] for param in params)
def getModuleProperties(self, module):
return self.describing_data['modules'][module]['properties']
@ -560,14 +559,16 @@ class Client(object):
return self.getModuleProperties(module)['interface_class']
def getCommands(self, module):
return self.describing_data['modules'][module]['commands']
cmds = filter(lambda item: isinstance(item[1]['datatype'], CommandType),
self.describing_data['modules'][module]['accessibles'].items())
return OrderedDict(cmds)
def execCommand(self, module, command, args):
# ignore reply message + reply specifier, only return data
return self._communicate('do', '%s:%s' % (module, command), list(args) if args else None)[2]
def getProperties(self, module, parameter):
return self.describing_data['modules'][module]['parameters'][parameter]
return self.describing_data['modules'][module]['accessibles'][parameter]
def syncCommunicate(self, *msg):
res = self._communicate(*msg) # pylint: disable=E1120

View File

@ -46,7 +46,7 @@ __all__ = [
u'BoolType', u'EnumType',
u'BLOBType', u'StringType',
u'TupleOf', u'ArrayOf', u'StructOf',
u'Command',
u'CommandType',
]
# base class for all DataTypes
@ -516,17 +516,16 @@ class StructOf(DataType):
return self.validate(dict(value))
# idea to mix commands and params, not yet used....
class Command(DataType):
class CommandType(DataType):
IS_COMMAND = True
def __init__(self, argtypes=tuple(), resulttype=None):
for arg in argtypes:
if not isinstance(arg, DataType):
raise ValueError(u'Command: Argument types must be DataTypes!')
raise ValueError(u'CommandType: Argument types must be DataTypes!')
if resulttype is not None:
if not isinstance(resulttype, DataType):
raise ValueError(u'Command: result type must be DataTypes!')
raise ValueError(u'CommandType: result type must be DataTypes!')
self.argtypes = argtypes
self.resulttype = resulttype
@ -542,8 +541,8 @@ class Command(DataType):
def __repr__(self):
argstr = u', '.join(repr(arg) for arg in self.argtypes)
if self.resulttype is None:
return u'Command(%s)' % argstr
return u'Command(%s)->%s' % (argstr, repr(self.resulttype))
return u'CommandType(%s)' % argstr
return u'CommandType(%s)->%s' % (argstr, repr(self.resulttype))
def validate(self, value):
"""return the validated arguments value or raise"""
@ -606,7 +605,7 @@ DATATYPES = dict(
enum=lambda kwds: EnumType('', **kwds),
struct=lambda named_subtypes: StructOf(
**dict((n, get_datatype(t)) for n, t in list(named_subtypes.items()))),
command=Command,
command=CommandType,
)

View File

@ -127,8 +127,8 @@ class CommandButton(QPushButton):
super(CommandButton, self).__init__(parent)
self._cmdname = cmdname
self._argintypes = cmdinfo['arguments'] # list of datatypes
self.resulttype = cmdinfo['resulttype']
self._argintypes = cmdinfo['datatype'].argtypes # list of datatypes
self.resulttype = cmdinfo['datatype'].resulttype
self._cb = cb # callback function for exection
self.setText(cmdname)

View File

@ -194,7 +194,7 @@ class ReadableWidget(QWidget):
# XXX: avoid a nasty race condition, mainly biting on M$
for i in range(15):
if 'status' in self._node.describing_data['modules'][module]['parameters']:
if 'status' in self._node.describing_data['modules'][module]['accessibles']:
break
sleep(0.01*i)
@ -246,7 +246,7 @@ class ReadableWidget(QWidget):
# XXX: also connect update_status signal to LineEdit ??
def update_status(self, status, qualifiers=None):
display_string = self._status_type.subtypes[0].entries.get(status[0])
display_string = self._status_type.subtypes[0]._enum[status[0]].name
if status[1]:
display_string += ':' + status[1]
self.statusLineEdit.setText(display_string)

View File

@ -22,6 +22,7 @@
"""Define Metaclass for Modules/Features"""
from __future__ import print_function
from collections import OrderedDict
try:
# pylint: disable=unused-import
@ -47,7 +48,7 @@ import time
from secop.errors import ProgrammingError
from secop.datatypes import EnumType
from secop.params import Parameter
from secop.params import Parameter, Override, Command
EVENT_ONLY_ON_CHANGED_VALUES = True
@ -68,36 +69,61 @@ class ModuleMeta(type):
if '__constructed__' in attrs:
return newtype
# merge properties, Parameter and commands from all sub-classes
for entry in ['properties', 'parameters', 'commands']:
newentry = {}
for base in reversed(bases):
if hasattr(base, entry):
newentry.update(getattr(base, entry))
newentry.update(attrs.get(entry, {}))
setattr(newtype, entry, newentry)
# apply Overrides from all sub-classes
newparams = getattr(newtype, 'parameters')
# merge properties from all sub-classes
newentry = {}
for base in reversed(bases):
overrides = getattr(base, 'overrides', {})
for n, o in overrides.items():
newparams[n] = o.apply(newparams[n].copy())
for n, o in attrs.get('overrides', {}).items():
newparams[n] = o.apply(newparams[n].copy())
newentry.update(getattr(base, "properties", {}))
newentry.update(attrs.get("properties", {}))
newtype.properties = newentry
# merge accessibles from all sub-classes, treat overrides
# for now, allow to use also the old syntax (parameters/commands dict)
accessibles_list = []
for base in reversed(bases):
if hasattr(base, "accessibles"):
accessibles_list.append(base.accessibles)
for entry in ['accessibles', 'parameters', 'commands', 'overrides']:
accessibles_list.append(attrs.get(entry, {}))
accessibles = {} # unordered dict of accessibles
newtype.parameters = {}
for accessibles_dict in accessibles_list:
for key, obj in accessibles_dict.items():
if isinstance(obj, Override):
try:
obj = obj.apply(accessibles[key])
accessibles[key] = obj
newtype.parameters[key] = obj
except KeyError:
raise ProgrammingError("module %s: %s does not exist"
% (name, key))
else:
if key in accessibles:
# for now, accept redefinitions:
print("WARNING: module %s: %s should not be redefined"
% (name, key))
# raise ProgrammingError("module %s: %s must not be redefined"
# % (name, key))
if isinstance(obj, Parameter):
newtype.parameters[key] = obj
accessibles[key] = obj
elif isinstance(obj, Command):
accessibles[key] = obj
else:
raise ProgrammingError('%r: accessibles entry %r should be a '
'Parameter or Command object!' % (name, key))
# Correct naming of EnumTypes
for k, v in newparams.items():
for k, v in newtype.parameters.items():
if isinstance(v.datatype, EnumType) and not v.datatype._enum.name:
v.datatype._enum.name = k
# newtype.accessibles will be used in 2 places only:
# 1) for inheritance (see above)
# 2) for the describing message
newtype.accessibles = OrderedDict(sorted(accessibles.items(), key=lambda item: item[1].ctr))
# check validity of Parameter entries
for pname, pobj in newtype.parameters.items():
# XXX: allow dicts for overriding certain aspects only.
if not isinstance(pobj, Parameter):
raise ProgrammingError('%r: Parameters entry %r should be a '
'Parameter object!' % (name, pname))
# XXX: create getters for the units of params ??
# wrap of reading/writing funcs
@ -165,8 +191,7 @@ class ModuleMeta(type):
setattr(newtype, pname, property(getter, setter))
# also collect/update information about Command's
setattr(newtype, 'commands', getattr(newtype, 'commands', {}))
# check information about Command's
for attrname in attrs:
if attrname.startswith('do_'):
if attrname[3:] not in newtype.commands:

View File

@ -23,11 +23,10 @@
from secop.lib import unset_value
from secop.errors import ProgrammingError
from secop.datatypes import DataType
from secop.datatypes import DataType, CommandType
EVENT_ONLY_ON_CHANGED_VALUES = False
class CountedObj(object):
ctr = [0]
def __init__(self):
@ -88,6 +87,8 @@ class Parameter(CountedObj):
# internal caching: value and timestamp of last change...
self.value = default
self.timestamp = 0
if ctr is not None:
self.ctr = ctr
def __repr__(self):
return '%s_%d(%s)' % (self.__class__.__name__, self.ctr, ', '.join(
@ -119,21 +120,30 @@ class Override(CountedObj):
note: overrides are applied by the metaclass during class creating
"""
def __init__(self, **kwds):
def __init__(self, description="", **kwds):
super(Override, self).__init__()
self.kwds = kwds
self.kwds['ctr'] = self.ctr
# allow to override description without keyword
if description:
self.kwds['description'] = description
# for now, do not use the Override ctr
# self.kwds['ctr'] = self.ctr
def __repr__(self):
return '%s_%d(%s)' % (self.__class__.__name__, self.ctr, ', '.join(
['%s=%r' % (k, v) for k, v in sorted(self.kwds.items())]))
def apply(self, paramobj):
if isinstance(paramobj, Parameter):
props = paramobj.__dict__.copy()
for k, v in self.kwds.items():
if hasattr(paramobj, k):
setattr(paramobj, k, v)
return paramobj
if k in props:
props[k] = v
else:
raise ProgrammingError(
"Can not apply Override(%s=%r) to %r: non-existing property!" %
(k, v, paramobj))
(k, v, props))
return Parameter(**props)
else:
raise ProgrammingError(
"Overrides can only be applied to Parameter's, %r is none!" %
@ -143,16 +153,16 @@ class Override(CountedObj):
class Command(CountedObj):
"""storage for Commands settings (description + call signature...)
"""
def __init__(self, description, arguments=None, result=None, optional=False):
def __init__(self, description, arguments=None, result=None, export=True, optional=False):
super(Command, self).__init__()
# descriptive text for humans
self.description = description
# list of datatypes for arguments
self.arguments = arguments or []
# datatype for result
self.resulttype = result
self.datatype = CommandType(arguments, result)
# whether implementation is optional
self.optional = optional
self.export = export
def __repr__(self):
return '%s_%d(%s)' % (self.__class__.__name__, self.ctr, ', '.join(
@ -160,8 +170,8 @@ class Command(CountedObj):
def for_export(self):
# used for serialisation only
return dict(
description=self.description,
arguments=[arg.export_datatype() for arg in self.arguments],
resulttype=self.resulttype.export_datatype() if self.resulttype else None,
datatype = self.datatype.export_datatype(),
)

View File

@ -150,32 +150,20 @@ class Dispatcher(object):
# return a copy of our list
return self._export[:]
def list_module_params(self, modulename):
self.log.debug(u'list_module_params(%r)' % modulename)
def export_accessibles(self, modulename):
self.log.debug(u'export_accessibles(%r)' % modulename)
if modulename in self._export:
# omit export=False params!
res = {}
for paramname, param in list(self.get_module(modulename).parameters.items()):
if param.export:
res[paramname] = param.for_export()
self.log.debug(u'list params for module %s -> %r' %
res = []
for aname, aobj in self.get_module(modulename).accessibles.items():
if aobj.export:
res.extend([aname, aobj.for_export()])
self.log.debug(u'list accessibles for module %s -> %r' %
(modulename, res))
return res
self.log.debug(u'-> module is not to be exported!')
return {}
def list_module_cmds(self, modulename):
self.log.debug(u'list_module_cmds(%r)' % modulename)
if modulename in self._export:
# omit export=False params!
res = {}
for cmdname, cmdobj in list(self.get_module(modulename).commands.items()):
res[cmdname] = cmdobj.for_export()
self.log.debug(u'list cmds for module %s -> %r' % (modulename, res))
return res
self.log.debug(u'-> module is not to be exported!')
return {}
def get_descriptive_data(self):
"""returns a python object which upon serialisation results in the descriptive data"""
# XXX: be lazy and cache this?
@ -184,12 +172,7 @@ class Dispatcher(object):
for modulename in self._export:
module = self.get_module(modulename)
# some of these need rework !
mod_desc = {u'parameters': [], u'commands': []}
for pname, param in list(self.list_module_params(
modulename).items()):
mod_desc[u'parameters'].extend([pname, param])
for cname, cmd in list(self.list_module_cmds(modulename).items()):
mod_desc[u'commands'].extend([cname, cmd])
mod_desc = {u'accessibles': self.export_accessibles(modulename)}
for propname, prop in list(module.properties.items()):
mod_desc[propname] = prop
result[u'modules'].extend([modulename, mod_desc])
@ -211,7 +194,7 @@ class Dispatcher(object):
cmdspec = moduleobj.commands.get(command, None)
if cmdspec is None:
raise NoSuchCommandError(module=modulename, command=command)
if len(cmdspec.arguments) != len(arguments):
if len(cmdspec.datatype.argtypes) != len(arguments):
raise BadValueError(
module=modulename,
command=command,