additional parameter properties

- custom properties: to be declared at startup calling the
  classmethods Parameter.add_property or Command.add_property
  Probably the call should be placed in secop_<facility>/__init__.py

- valid properties are listed in Parameter.valid_properties /
  Command.valid_properties. New properties without relation to code
  in the SECnode like fmtstr, group, visibility can be added at one
  place in code only

Change-Id: I8c550eba955473665d246ddef3815b23c68be4f7
Reviewed-on: https://forge.frm2.tum.de/review/19611
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:
zolliker 2018-12-13 16:54:06 +01:00
parent 92e421a939
commit ef27fd1b54
3 changed files with 93 additions and 52 deletions

View File

@ -46,3 +46,6 @@ d.group=pid
value.unit=K value.unit=K
# test custom properties
value.test=customized value

View File

@ -36,7 +36,44 @@ class CountedObj(object):
cl[0] += 1 cl[0] += 1
self.ctr = cl[0] self.ctr = cl[0]
class Parameter(CountedObj): class Accessible(CountedObj):
'''abstract base class for Parameter and Command'''
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
props = self.__dict__.copy()
props.pop('ctr')
return type(self)(**props)
def exported_properties(self):
res = dict(datatype=self.datatype.export_datatype())
for key, value in self.__dict__.items():
if key in self.valid_properties:
res[self.valid_properties[key]] = value
return res
@classmethod
def add_property(cls, *args, **kwds):
'''add custom properties
args: custom properties, exported with leading underscore
kwds: special cases, where exported name differs from internal
intention: to be called in secop_<facility>/__init__.py for
facility specific properties
'''
for name in args:
kwds[name] = '_' + name
for name, external in kwds.items():
if name in cls.valid_properties and name != cls.valid_properties[name]:
raise ProgrammingError('can not overrride property name %s' % name)
cls.valid_properties[name] = external
class Parameter(Accessible):
"""storage for Parameter settings + value + qualifiers """storage for Parameter settings + value + qualifiers
if readonly is False, the value can be changed (by code, or remote) if readonly is False, the value can be changed (by code, or remote)
@ -52,6 +89,12 @@ class Parameter(CountedObj):
note: Drivable (and derived classes) poll with 10 fold frequency if module is busy.... note: Drivable (and derived classes) poll with 10 fold frequency if module is busy....
""" """
# unit and datatype are not listed (handled separately)
valid_properties = dict()
for prop in ('description', 'readonly', 'group', 'visibility', 'fmtstr', 'precision'):
valid_properties[prop] = prop
def __init__(self, def __init__(self,
description, description,
datatype=None, datatype=None,
@ -59,12 +102,12 @@ class Parameter(CountedObj):
unit='', unit='',
readonly=True, readonly=True,
export=True, export=True,
group='',
poll=False, poll=False,
value=unset_value, value=None, # swallow
timestamp=0, timestamp=None, # swallow
optional=False, optional=False,
ctr=None): ctr=None,
**kwds):
super(Parameter, self).__init__() super(Parameter, self).__init__()
if not isinstance(datatype, DataType): if not isinstance(datatype, DataType):
if issubclass(datatype, DataType): if issubclass(datatype, DataType):
@ -79,39 +122,26 @@ class Parameter(CountedObj):
self.unit = unit self.unit = unit
self.readonly = readonly self.readonly = readonly
self.export = export self.export = export
self.group = group
self.optional = optional self.optional = optional
# note: auto-converts True/False to 1/0 which yield the expected # note: auto-converts True/False to 1/0 which yield the expected
# behaviour... # behaviour...
self.poll = int(poll) self.poll = int(poll)
for key in kwds:
if key not in self.valid_properties:
raise ProgrammingError('%s is not a valid parameter property' % key)
self.__dict__.update(kwds)
# internal caching: value and timestamp of last change... # internal caching: value and timestamp of last change...
self.value = default self.value = default
self.timestamp = 0 self.timestamp = 0
if ctr is not None: if ctr is not None:
self.ctr = ctr self.ctr = ctr
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
params = self.__dict__.copy()
params.pop('ctr')
return Parameter(**params)
def for_export(self): def for_export(self):
# used for serialisation only # used for serialisation only
res = dict( res = self.exported_properties()
description=self.description,
readonly=self.readonly,
datatype=self.datatype.export_datatype(),
)
if self.unit: if self.unit:
res['unit'] = self.unit res['unit'] = self.unit
if self.group:
res['group'] = self.group
return res return res
def export_value(self): def export_value(self):
@ -119,7 +149,7 @@ class Parameter(CountedObj):
class Override(CountedObj): class Override(CountedObj):
"""Stores the overrides to ba applied to a Parameter """Stores the overrides to be applied to a Parameter
note: overrides are applied by the metaclass during class creating note: overrides are applied by the metaclass during class creating
""" """
@ -137,27 +167,38 @@ class Override(CountedObj):
['%s=%r' % (k, v) for k, v in sorted(self.kwds.items())])) ['%s=%r' % (k, v) for k, v in sorted(self.kwds.items())]))
def apply(self, obj): def apply(self, obj):
if isinstance(obj, CountedObj): if isinstance(obj, Accessible):
props = obj.__dict__.copy() props = obj.__dict__.copy()
for k, v in self.kwds.items(): for key in self.kwds:
if k in props: if key not in props and key not in type(obj).valid_properties:
props[k] = v raise ProgrammingError( "%s is not a valid %s property" %
else: (key, type(obj).__name__))
raise ProgrammingError( props.update(self.kwds)
"Can not apply Override(%s=%r) to %r: non-existing property!" %
(k, v, props))
props['ctr'] = self.ctr props['ctr'] = self.ctr
return type(obj)(**props) return type(obj)(**props)
else: else:
raise ProgrammingError( raise ProgrammingError(
"Overrides can only be applied to Parameter's, %r is none!" % "Overrides can only be applied to Accessibles, %r is none!" %
obj) obj)
class Command(CountedObj): class Command(Accessible):
"""storage for Commands settings (description + call signature...) """storage for Commands settings (description + call signature...)
""" """
def __init__(self, description, argument=None, result=None, export=True, optional=False, datatype=None, ctr=None): # datatype is not listed (handled separately)
valid_properties = dict()
for prop in ('description', 'group', 'visibility'):
valid_properties[prop] = prop
def __init__(self,
description,
argument=None,
result=None,
export=True,
optional=False,
datatype=None, # swallow datatype argument on copy
ctr=None,
**kwds):
super(Command, self).__init__() super(Command, self).__init__()
# descriptive text for humans # descriptive text for humans
self.description = description self.description = description
@ -168,23 +209,13 @@ class Command(CountedObj):
# whether implementation is optional # whether implementation is optional
self.optional = optional self.optional = optional
self.export = export self.export = export
for key in kwds:
if key not in self.valid_properties:
raise ProgrammingError('%s is not a valid command property' % key)
self.__dict__.update(kwds)
if ctr is not None: if ctr is not None:
self.ctr = ctr self.ctr = ctr
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): def for_export(self):
# used for serialisation only # used for serialisation only
return self.exported_properties()
return dict(
description=self.description,
datatype = self.datatype.export_datatype(),
)
def copy(self):
# return a copy of ourselfs
params = self.__dict__.copy()
params.pop('ctr')
return Command(**params)

View File

@ -28,8 +28,12 @@ from math import atan
from secop.datatypes import EnumType, FloatRange, TupleOf from secop.datatypes import EnumType, FloatRange, TupleOf
from secop.lib import clamp, mkthread from secop.lib import clamp, mkthread
from secop.modules import Drivable, Parameter, Override from secop.modules import Drivable, Parameter, Command, Override
# test custom property (value.test can be changed in config file)
Parameter.add_property('test')
# in the rare case of namespace conflicts, the external name could be completely different
Command.add_property(special='_peculiar')
class CryoBase(Drivable): class CryoBase(Drivable):
pass pass
@ -42,6 +46,7 @@ class Cryostat(CryoBase):
- cooling power - cooling power
- thermal transfer between regulation and samplen - thermal transfer between regulation and samplen
""" """
parameters = dict( parameters = dict(
jitter=Parameter("amount of random noise on readout values", jitter=Parameter("amount of random noise on readout values",
datatype=FloatRange(0, 1), unit="K", datatype=FloatRange(0, 1), unit="K",
@ -81,6 +86,7 @@ class Cryostat(CryoBase):
), ),
value=Override("regulation temperature", value=Override("regulation temperature",
datatype=FloatRange(0), default=0, unit="K", datatype=FloatRange(0), default=0, unit="K",
test='TEST',
), ),
pid=Parameter("regulation coefficients", pid=Parameter("regulation coefficients",
datatype=TupleOf(FloatRange(0), FloatRange(0, 100), datatype=TupleOf(FloatRange(0), FloatRange(0, 100),
@ -126,7 +132,8 @@ class Cryostat(CryoBase):
) )
commands = dict( commands = dict(
stop=Override( stop=Override(
"Stop ramping the setpoint\n\nby setting the current setpoint as new target"), "Stop ramping the setpoint\n\nby setting the current setpoint as new target",
special='content of special property'),
) )
def init_module(self): def init_module(self):