add features

+ split out metaclass and params

Change-Id: I4d9092827cd74da6757ef1f30d2460471e5e5ef3
Reviewed-on: https://forge.frm2.tum.de/review/18190
Tested-by: JenkinsCodeReview <bjoern_pedersen@frm2.tum.de>
Reviewed-by: Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
This commit is contained in:
Enrico Faulhaber
2018-06-18 18:03:54 +02:00
parent 33e6ded5b8
commit 48382852b8
4 changed files with 511 additions and 289 deletions

View File

@ -30,297 +30,12 @@ from __future__ import print_function
import time
try:
from six import add_metaclass # for py2/3 compat
except ImportError:
# copied from six v1.10.0
def add_metaclass(metaclass):
"""Class decorator for creating a class with a metaclass."""
def wrapper(cls):
orig_vars = cls.__dict__.copy()
slots = orig_vars.get('__slots__')
if slots is not None:
if isinstance(slots, str):
slots = [slots]
for slots_var in slots:
orig_vars.pop(slots_var)
orig_vars.pop('__dict__', None)
orig_vars.pop('__weakref__', None)
return metaclass(cls.__name__, cls.__bases__, orig_vars)
return wrapper
from secop.lib import formatExtendedStack, mkthread, unset_value
from secop.lib.enum import Enum
from secop.errors import ConfigError, ProgrammingError
from secop.datatypes import DataType, EnumType, TupleOf, StringType, FloatRange, get_datatype
EVENT_ONLY_ON_CHANGED_VALUES = False
class CountedObj(object):
ctr = [0]
def __init__(self):
cl = self.__class__.ctr
cl[0] += 1
self.ctr = cl[0]
class Parameter(CountedObj):
"""storage for Parameter settings + value + qualifiers
if readonly is False, the value can be changed (by code, or remote)
if no default is given, the parameter MUST be specified in the configfile
during startup, value is initialized with the default value or
from the config file if specified there
poll can be:
- False (never poll this parameter)
- True (poll this ever pollinterval)
- positive int (poll every N(th) pollinterval)
- negative int (normally poll every N(th) pollinterval, if module is busy, poll every pollinterval)
note: Drivable (and derived classes) poll with 10 fold frequency if module is busy....
"""
def __init__(self,
description,
datatype=None,
default=unset_value,
unit='',
readonly=True,
export=True,
group='',
poll=False,
value=unset_value,
timestamp=0,
optional=False,
ctr=None):
super(Parameter, self).__init__()
if not isinstance(datatype, DataType):
if issubclass(datatype, DataType):
# goodie: make an instance from a class (forgotten ()???)
datatype = datatype()
else:
raise ValueError(
'datatype MUST be derived from class DataType!')
self.description = description
self.datatype = datatype
self.default = default
self.unit = unit
self.readonly = readonly
self.export = export
self.group = group
self.optional = optional
# note: auto-converts True/False to 1/0 which yield the expected
# behaviour...
self.poll = int(poll)
# internal caching: value and timestamp of last change...
self.value = default
self.timestamp = 0
def __repr__(self):
return '%s_%d(%s)' % (self.__class__.__name__, self.ctr, ', '.join(
['%s=%r' % (k, v) for k, v in sorted(self.__dict__.items())]))
def copy(self):
# return a copy of ourselfs
return Parameter(**self.__dict__)
def for_export(self):
# used for serialisation only
res = dict(
description=self.description,
readonly=self.readonly,
datatype=self.datatype.export_datatype(),
)
if self.unit:
res['unit'] = self.unit
if self.group:
res['group'] = self.group
return res
def export_value(self):
return self.datatype.export_value(self.value)
class Override(CountedObj):
"""Stores the overrides to ba applied to a Parameter
note: overrides are applied by the metaclass during class creating
"""
def __init__(self, **kwds):
super(Override, self).__init__()
self.kwds = kwds
self.kwds['ctr'] = self.ctr
def apply(self, paramobj):
if isinstance(paramobj, Parameter):
for k, v in self.kwds.items():
if hasattr(paramobj, k):
setattr(paramobj, k, v)
return paramobj
else:
raise ProgrammingError(
"Can not apply Override(%s=%r) to %r: non-existing property!" %
(k, v, paramobj))
else:
raise ProgrammingError(
"Overrides can only be applied to Parameter's, %r is none!" %
paramobj)
class Command(CountedObj):
"""storage for Commands settings (description + call signature...)
"""
def __init__(self, description, arguments=None, result=None, 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
# whether implementation is optional
self.optional = optional
def __repr__(self):
return '%s_%d(%s)' % (self.__class__.__name__, self.ctr, ', '.join(
['%s=%r' % (k, v) for k, v in sorted(self.__dict__.items())]))
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,
)
# Meta class
# warning: MAGIC!
class ModuleMeta(type):
"""Metaclass
joining the class's properties, parameters and commands dicts with
those of base classes.
also creates getters/setter for parameter access
and wraps read_*/write_* methods
(so the dispatcher will get notfied of changed values)
"""
def __new__(mcs, name, bases, attrs):
newtype = type.__new__(mcs, name, bases, attrs)
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')
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())
# Check naming of EnumType
for k, v in newparams.items():
if isinstance(v.datatype, EnumType) and not v.datatype._enum.name:
v.datatype._enum.name = k
# 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
rfunc = attrs.get('read_' + pname, None)
for base in bases:
if rfunc is not None:
break
rfunc = getattr(base, 'read_' + pname, None)
def wrapped_rfunc(self, maxage=0, pname=pname, rfunc=rfunc):
if rfunc:
self.log.debug("rfunc(%s): call %r" % (pname, rfunc))
value = rfunc(self, maxage)
else:
# return cached value
self.log.debug("rfunc(%s): return cached value" % pname)
value = self.parameters[pname].value
setattr(self, pname, value) # important! trigger the setter
return value
if rfunc:
wrapped_rfunc.__doc__ = rfunc.__doc__
if getattr(rfunc, '__wrapped__', False) is False:
setattr(newtype, 'read_' + pname, wrapped_rfunc)
wrapped_rfunc.__wrapped__ = True
if not pobj.readonly:
wfunc = attrs.get('write_' + pname, None)
for base in bases:
if wfunc is not None:
break
wfunc = getattr(base, 'write_' + pname, None)
def wrapped_wfunc(self, value, pname=pname, wfunc=wfunc):
self.log.debug("wfunc(%s): set %r" % (pname, value))
pobj = self.parameters[pname]
value = pobj.datatype.validate(value)
if wfunc:
self.log.debug('calling %r(%r)' % (wfunc, value))
value = wfunc(self, value) or value
# XXX: use setattr or direct manipulation
# of self.parameters[pname]?
setattr(self, pname, value)
return value
if wfunc:
wrapped_wfunc.__doc__ = wfunc.__doc__
if getattr(wfunc, '__wrapped__', False) is False:
setattr(newtype, 'write_' + pname, wrapped_wfunc)
wrapped_wfunc.__wrapped__ = True
def getter(self, pname=pname):
return self.parameters[pname].value
def setter(self, value, pname=pname):
pobj = self.parameters[pname]
value = pobj.datatype.validate(value)
pobj.timestamp = time.time()
if (not EVENT_ONLY_ON_CHANGED_VALUES) or (value != pobj.value):
pobj.value = value
# also send notification
if self.parameters[pname].export:
self.log.debug('%s is now %r' % (pname, value))
self.DISPATCHER.announce_update(self, pname, pobj)
setattr(newtype, pname, property(getter, setter))
# also collect/update information about Command's
setattr(newtype, 'commands', getattr(newtype, 'commands', {}))
for attrname in attrs:
if attrname.startswith('do_'):
if attrname[3:] not in newtype.commands:
raise ProgrammingError('%r: command %r has to be specified '
'explicitly!' % (name, attrname[3:]))
attrs['__constructed__'] = True
return newtype
from secop.errors import ConfigError
from secop.datatypes import EnumType, TupleOf, StringType, FloatRange, get_datatype
from secop.metaclass import add_metaclass, ModuleMeta
from secop.params import Command, Parameter, Override
@add_metaclass(ModuleMeta)