fix inheritance problem with mixin

- a mixin should not inherit from module then it has Parameters
- Parameters in mixins must be complete, not just overrides
- check precedence of read_<param> or handler

Change-Id: I72d9355a1982770d1a99d9552a20330103c97edb
This commit is contained in:
zolliker 2021-03-18 13:32:54 +01:00
parent 6538500881
commit 48230334af
9 changed files with 55 additions and 24 deletions

View File

@ -495,9 +495,12 @@ class SecopClient(ProxyClient):
def _set_state(self, online, state=None):
# treat reconnecting as online!
state = state or self.state
try:
self.callback(None, 'nodeStateChange', online, state)
for mname in self.modules:
self.callback(mname, 'nodeStateChange', online, state)
except Exception as e:
self.log.error('ERROR in nodeStateCallback %s', e)
# set online attribute after callbacks -> callback may check for old state
self.online = online
self.state = state

View File

@ -98,14 +98,17 @@ def clamp(_min, value, _max):
def get_class(spec):
"""loads a class given by string in dotted notaion (as python would do)"""
"""loads a class given by string in dotted notation (as python would do)"""
modname, classname = spec.rsplit('.', 1)
if modname.startswith('secop'):
module = importlib.import_module(modname)
else:
# rarely needed by now....
module = importlib.import_module('secop.' + modname)
try:
return getattr(module, classname)
except AttributeError:
raise AttributeError('no such class') from None
def mkthread(func, *args, **kwds):

View File

@ -93,9 +93,12 @@ class HasAccessibles(HasProperties):
rfunc_handler = pobj.handler.get_read_func(cls, pname) if pobj.handler else None
wrapped = hasattr(rfunc, '__wrapped__')
if rfunc_handler:
if rfunc and not wrapped:
if 'read_' + pname in cls.__dict__:
if pname in cls.__dict__:
raise ProgrammingError("parameter '%s' can not have a handler "
"and read_%s" % (pname, pname))
# read_<pname> overwrites inherited handler
else:
rfunc = rfunc_handler
wrapped = False

View File

@ -101,10 +101,10 @@ class Parameter(Accessible):
description = Property(
'mandatory description of the parameter', TextType(),
extname='description', mandatory=True)
extname='description', mandatory=True, export='always')
datatype = Property(
'datatype of the Parameter (SECoP datainfo)', DataTypeType(),
extname='datainfo', mandatory=True)
extname='datainfo', mandatory=True, export='always')
readonly = Property(
'not changeable via SECoP (default True)', BoolType(),
extname='readonly', default=True, export='always')
@ -283,7 +283,7 @@ class Command(Accessible):
description = Property(
'description of the Command', TextType(),
extname='description', export=True, mandatory=True)
extname='description', export='always', mandatory=True)
group = Property(
'optional command group of the command.', StringType(),
extname='group', export=True, default='')

View File

@ -228,11 +228,15 @@ class Server:
errors.append(self.unknown_options(cls, opts))
self.modules = OrderedDict()
badclass = None
failed = set() # python modules failed to load
self.lastError = None
for modname, options in self.module_cfg.items():
opts = dict(options)
try:
classname = opts.pop('class')
pymodule = classname.rpartition('.')[0]
if pymodule in failed:
continue
cls = get_class(classname)
modobj = cls(modname, self.log.getChild(modname), opts, self)
# all used args should be popped from opts!
@ -242,8 +246,12 @@ class Server:
except ConfigError as e:
errors.append(str(e))
except Exception as e:
if str(e) == 'no such class':
errors.append('%s not found' % classname)
else:
failed.add(pymodule)
badclass = classname
errors.append('error while loading %s' % badclass)
errors.append('error importing %s' % pymodule)
poll_table = dict()
# all objs created, now start them up and interconnect

View File

@ -43,9 +43,17 @@ def make_cvt_list(dt, tail=''):
else:
return [] # ArrayType, BlobType and TextType are ignored: too much data, probably not used
result = []
print('START', dt)
for subkey, elmtype in items:
print('MAKE_CVT_LIST', subkey, elmtype)
for fun, tail_, opts in make_cvt_list(elmtype, '%s.%s' % (tail, subkey)):
result.append((lambda v, k=subkey, f=fun: f(v[k]), tail_, opts))
def conv(value, key=subkey, func=fun):
try:
return value[key]
except KeyError: # can not use value.get() because value might be a list
return None
result.append((conv, tail_, opts))
print('END', result)
return result

View File

@ -128,8 +128,9 @@ class Main(Communicator):
return data # return data as string
class PpmsBase(HasIodev, Readable):
class PpmsMixin:
"""common base for all ppms modules"""
iodev = Attached()
pollerClass = Poller
@ -139,7 +140,7 @@ class PpmsBase(HasIodev, Readable):
# as this pollinterval affects only the polling of settings
# it would be confusing to export it.
pollinterval = Parameter(export=False)
pollinterval = Parameter('', FloatRange(), needscfg=False, export=False)
def initModule(self):
self._iodev.register(self)
@ -172,7 +173,7 @@ class PpmsBase(HasIodev, Readable):
self.status = (self.Status.IDLE, '')
class Channel(PpmsBase):
class Channel(PpmsMixin, HasIodev, Readable):
"""channel base class"""
value = Parameter('main value of channels', poll=True)
@ -270,7 +271,7 @@ class BridgeChannel(Channel):
return self.no, 0, 0, change.dcflag, change.readingmode, 0
class Level(PpmsBase):
class Level(PpmsMixin, HasIodev, Readable):
"""helium level"""
level = IOHandler('level', 'LEVEL?', '%g,%d')
@ -293,7 +294,7 @@ class Level(PpmsBase):
return dict(value=level, status=(self.Status.IDLE, ''))
class Chamber(PpmsBase, Drivable):
class Chamber(PpmsMixin, HasIodev, Drivable):
"""sample chamber handling
value is an Enum, which is redundant with the status text
@ -368,7 +369,7 @@ class Chamber(PpmsBase, Drivable):
return (change.target,)
class Temp(PpmsBase, Drivable):
class Temp(PpmsMixin, HasIodev, Drivable):
"""temperature"""
temp = IOHandler('temp', 'TEMP?', '%g,%g,%d')
@ -553,7 +554,7 @@ class Temp(PpmsBase, Drivable):
self._stopped = True
class Field(PpmsBase, Drivable):
class Field(PpmsMixin, HasIodev, Drivable):
"""magnetic field"""
field = IOHandler('field', 'FIELD?', '%g,%g,%d,%d')
@ -562,6 +563,7 @@ class Field(PpmsBase, Drivable):
PREPARED=150,
PREPARING=340,
RAMPING=370,
STABILIZING=380,
FINALIZING=390,
)
# pylint: disable=invalid-name
@ -584,7 +586,7 @@ class Field(PpmsBase, Drivable):
2: (Status.PREPARING, 'switch warming'),
3: (Status.FINALIZING, 'switch cooling'),
4: (Status.IDLE, 'driven stable'),
5: (Status.FINALIZING, 'driven final'),
5: (Status.STABILIZING, 'driven final'),
6: (Status.RAMPING, 'charging'),
7: (Status.RAMPING, 'discharging'),
8: (Status.ERROR, 'current error'),
@ -690,7 +692,7 @@ class Field(PpmsBase, Drivable):
self._stopped = True
class Position(PpmsBase, Drivable):
class Position(PpmsMixin, HasIodev, Drivable):
"""rotator position"""
move = IOHandler('move', 'MOVE?', '%g,%g,%g')

View File

@ -359,6 +359,8 @@ class SeaModule(Module):
else:
pobj = Parameter(**kwds)
datatype = pobj.datatype
if name == 'cc' and key == 'value':
print('cc.value: %r %r' % (kwds, pobj))
attributes[key] = pobj
if not hasattr(cls, 'read_' + key):
def rfunc(self, cmd='hval /sics/%s/%s' % (sea_object, path)):

View File

@ -38,12 +38,14 @@ class TestCmd(Module):
result=StringType())
def arg(self, *arg):
"""5 args"""
self.tuple = arg
return repr(arg)
@Command(argument=StructOf(a=StringType(), b=FloatRange(), c=BoolType(), optional=['b']),
result=StringType())
def keyed(self, **arg):
"""keyworded arg"""
self.struct = arg
return repr(arg)
@Command(argument=FloatRange(), result=StringType())