diff --git a/.pylintrc b/.pylintrc index a40281e..daa9688 100644 --- a/.pylintrc +++ b/.pylintrc @@ -212,13 +212,13 @@ max-locals=50 max-returns=10 # Maximum number of branch for function / method body -max-branches=40 +max-branches=50 # Maximum number of statements in function / method body max-statements=150 # Maximum number of parents for a class (see R0901). -max-parents=10 +max-parents=15 # Maximum number of attributes for a class (see R0902). max-attributes=50 diff --git a/doc/source/reference.rst b/doc/source/reference.rst index 0a1eac9..a8f67a4 100644 --- a/doc/source/reference.rst +++ b/doc/source/reference.rst @@ -20,13 +20,12 @@ Parameters, Commands and Properties ................................... .. autoclass:: secop.params.Parameter -.. autoclass:: secop.params.usercommand +.. autoclass:: secop.params.Command .. autoclass:: secop.properties.Property .. autoclass:: secop.modules.Attached :show-inheritance: - Datatypes ......... diff --git a/secop/core.py b/secop/core.py index c7a0793..9a62b46 100644 --- a/secop/core.py +++ b/secop/core.py @@ -29,11 +29,10 @@ from secop.datatypes import FloatRange, IntRange, ScaledInteger, \ BoolType, EnumType, BLOBType, StringType, TupleOf, ArrayOf, StructOf from secop.lib.enum import Enum -from secop.modules import Module, Readable, Writable, Drivable, Communicator, Attached +from secop.modules import Module, Readable, Writable, Drivable, Communicator, Attached, Done from secop.properties import Property -from secop.params import Parameter, Command, Override, usercommand +from secop.params import Parameter, Command from secop.poller import AUTO, REGULAR, SLOW, DYNAMIC -from secop.metaclass import Done from secop.iohandler import IOHandler, IOHandlerBase from secop.stringio import StringIO, HasIodev from secop.proxy import SecNode, Proxy, proxy_class diff --git a/secop/datatypes.py b/secop/datatypes.py index 0fc66c2..a291408 100644 --- a/secop/datatypes.py +++ b/secop/datatypes.py @@ -98,7 +98,7 @@ class DataType(HasProperties): def set_properties(self, **kwds): """init datatype properties""" try: - for k,v in kwds.items(): + for k, v in kwds.items(): self.setProperty(k, v) self.checkProperties() except Exception as e: @@ -151,11 +151,12 @@ class Stub(DataType): """ for dtcls in globals().values(): if isinstance(dtcls, type) and issubclass(dtcls, DataType): - for prop in dtcls.properties.values(): + for prop in dtcls.propertyDict.values(): stub = prop.datatype if isinstance(stub, cls): prop.datatype = globals()[stub.name](*stub.args) + # SECoP types: class FloatRange(DataType): @@ -165,16 +166,14 @@ class FloatRange(DataType): :param maxval: (property **max**) :param kwds: any of the properties below """ - properties = { - 'min': Property('low limit', Stub('FloatRange'), extname='min', default=-sys.float_info.max), - 'max': Property('high limit', Stub('FloatRange'), extname='max', default=sys.float_info.max), - 'unit': Property('physical unit', Stub('StringType'), extname='unit', default=''), - 'fmtstr': Property('format string', Stub('StringType'), extname='fmtstr', default='%g'), - 'absolute_resolution': Property('absolute resolution', Stub('FloatRange', 0), - extname='absolute_resolution', default=0.0), - 'relative_resolution': Property('relative resolution', Stub('FloatRange', 0), - extname='relative_resolution', default=1.2e-7), - } + min = Property('low limit', Stub('FloatRange'), extname='min', default=-sys.float_info.max) + max = Property('high limit', Stub('FloatRange'), extname='max', default=sys.float_info.max) + unit = Property('physical unit', Stub('StringType'), extname='unit', default='') + fmtstr = Property('format string', Stub('StringType'), extname='fmtstr', default='%g') + absolute_resolution = Property('absolute resolution', Stub('FloatRange', 0), + extname='absolute_resolution', default=0.0) + relative_resolution = Property('relative resolution', Stub('FloatRange', 0), + extname='relative_resolution', default=1.2e-7) def __init__(self, minval=None, maxval=None, **kwds): super().__init__() @@ -204,7 +203,7 @@ class FloatRange(DataType): if self.min - prec <= value <= self.max + prec: return min(max(value, self.min), self.max) raise BadValueError('%.14g should be a float between %.14g and %.14g' % - (value, self.min, self.max)) + (value, self.min, self.max)) def __repr__(self): hints = self.get_info() @@ -212,7 +211,7 @@ class FloatRange(DataType): hints['minval'] = hints.pop('min') if 'max' in hints: hints['maxval'] = hints.pop('max') - return 'FloatRange(%s)' % (', '.join('%s=%r' % (k,v) for k,v in hints.items())) + return 'FloatRange(%s)' % (', '.join('%s=%r' % (k, v) for k, v in hints.items())) def export_value(self, value): """returns a python object fit for serialisation""" @@ -247,12 +246,10 @@ class IntRange(DataType): :param minval: (property **min**) :param maxval: (property **max**) """ - properties = { - 'min': Property('minimum value', Stub('IntRange', -UNLIMITED, UNLIMITED), extname='min', mandatory=True), - 'max': Property('maximum value', Stub('IntRange', -UNLIMITED, UNLIMITED), extname='max', mandatory=True), - # a unit on an int is now allowed in SECoP, but do we need them in Frappy? - # 'unit': Property('physical unit', StringType(), extname='unit', default=''), - } + min = Property('minimum value', Stub('IntRange', -UNLIMITED, UNLIMITED), extname='min', mandatory=True) + max = Property('maximum value', Stub('IntRange', -UNLIMITED, UNLIMITED), extname='max', mandatory=True) + # a unit on an int is now allowed in SECoP, but do we need them in Frappy? + # unit = Property('physical unit', StringType(), extname='unit', default='') def __init__(self, minval=None, maxval=None): super().__init__() @@ -278,7 +275,12 @@ class IntRange(DataType): raise BadValueError('Can not convert %r to int' % value) def __repr__(self): - return 'IntRange(%d, %d)' % (self.min, self.max) + args = (self.min, self.max) + if args[1] == DEFAULT_MAX_INT: + args = args[:1] + if args[0] == DEFAULT_MIN_INT: + args = () + return 'IntRange%s' % repr(args) def export_value(self, value): """returns a python object fit for serialisation""" @@ -315,24 +317,23 @@ class ScaledInteger(DataType): note: limits are for the scaled float value the scale is only used for calculating to/from transport serialisation """ - properties = { - 'scale': Property('scale factor', FloatRange(sys.float_info.min), extname='scale', mandatory=True), - 'min': Property('low limit', FloatRange(), extname='min', mandatory=True), - 'max': Property('high limit', FloatRange(), extname='max', mandatory=True), - 'unit': Property('physical unit', Stub('StringType'), extname='unit', default=''), - 'fmtstr': Property('format string', Stub('StringType'), extname='fmtstr', default='%g'), - 'absolute_resolution': Property('absolute resolution', FloatRange(0), - extname='absolute_resolution', default=0.0), - 'relative_resolution': Property('relative resolution', FloatRange(0), - extname='relative_resolution', default=1.2e-7), - } + scale = Property('scale factor', FloatRange(sys.float_info.min), extname='scale', mandatory=True) + min = Property('low limit', FloatRange(), extname='min', mandatory=True) + max = Property('high limit', FloatRange(), extname='max', mandatory=True) + unit = Property('physical unit', Stub('StringType'), extname='unit', default='') + fmtstr = Property('format string', Stub('StringType'), extname='fmtstr', default='%g') + absolute_resolution = Property('absolute resolution', FloatRange(0), + extname='absolute_resolution', default=0.0) + relative_resolution = Property('relative resolution', FloatRange(0), + extname='relative_resolution', default=1.2e-7) def __init__(self, scale, minval=None, maxval=None, absolute_resolution=None, **kwds): super().__init__() scale = float(scale) if absolute_resolution is None: absolute_resolution = scale - self.set_properties(scale=scale, + self.set_properties( + scale=scale, min=DEFAULT_MIN_INT * scale if minval is None else float(minval), max=DEFAULT_MAX_INT * scale if maxval is None else float(maxval), absolute_resolution=absolute_resolution, @@ -363,8 +364,8 @@ class ScaledInteger(DataType): def export_datatype(self): return self.get_info(type='scaled', - min = int((self.min + self.scale * 0.5) // self.scale), - max = int((self.max + self.scale * 0.5) // self.scale)) + min=int((self.min + self.scale * 0.5) // self.scale), + max=int((self.max + self.scale * 0.5) // self.scale)) def __call__(self, value): try: @@ -377,15 +378,15 @@ class ScaledInteger(DataType): value = min(max(value, self.min), self.max) else: raise BadValueError('%g should be a float between %g and %g' % - (value, self.min, self.max)) + (value, self.min, self.max)) intval = int((value + self.scale * 0.5) // self.scale) value = float(intval * self.scale) return value # return 'actual' value (which is more discrete than a float) def __repr__(self): hints = self.get_info(scale=float('%g' % self.scale), - min = int((self.min + self.scale * 0.5) // self.scale), - max = int((self.max + self.scale * 0.5) // self.scale)) + min=int((self.min + self.scale * 0.5) // self.scale), + max=int((self.max + self.scale * 0.5) // self.scale)) return 'ScaledInteger(%s)' % (', '.join('%s=%r' % kv for kv in hints.items())) def export_value(self, value): @@ -434,10 +435,11 @@ class EnumType(DataType): return EnumType(self._enum) def export_datatype(self): - return {'type': 'enum', 'members':dict((m.name, m.value) for m in self._enum.members)} + return {'type': 'enum', 'members': dict((m.name, m.value) for m in self._enum.members)} def __repr__(self): - return "EnumType(%r, %s)" % (self._enum.name, ', '.join('%s=%d' %(m.name, m.value) for m in self._enum.members)) + return "EnumType(%r, %s)" % (self._enum.name, + ', '.join('%s=%d' % (m.name, m.value) for m in self._enum.members)) def export_value(self, value): """returns a python object fit for serialisation""" @@ -451,7 +453,7 @@ class EnumType(DataType): """return the validated (internal) value or raise""" try: return self._enum[value] - except (KeyError, TypeError): # TypeError will be raised when value is not hashable + except (KeyError, TypeError): # TypeError will be raised when value is not hashable raise BadValueError('%r is not a member of enum %r' % (value, self._enum)) def from_string(self, text): @@ -460,6 +462,9 @@ class EnumType(DataType): def format_value(self, value, unit=None): return '%s<%s>' % (self._enum[value].name, self._enum[value].value) + def set_name(self, name): + self._enum.name = name + def compatible(self, other): for m in self._enum.members: other(m) @@ -471,12 +476,10 @@ class BLOBType(DataType): internally treated as bytes """ - properties = { - 'minbytes': Property('minimum number of bytes', IntRange(0), extname='minbytes', - default=0), - 'maxbytes': Property('maximum number of bytes', IntRange(0), extname='maxbytes', - mandatory=True), - } + minbytes = Property('minimum number of bytes', IntRange(0), extname='minbytes', + default=0) + maxbytes = Property('maximum number of bytes', IntRange(0), extname='maxbytes', + mandatory=True) def __init__(self, minbytes=0, maxbytes=None): super().__init__() @@ -538,14 +541,12 @@ class StringType(DataType): for parameters see properties below """ - properties = { - 'minchars': Property('minimum number of character points', IntRange(0, UNLIMITED), - extname='minchars', default=0), - 'maxchars': Property('maximum number of character points', IntRange(0, UNLIMITED), - extname='maxchars', default=UNLIMITED), - 'isUTF8': Property('flag telling whether encoding is UTF-8 instead of ASCII', - Stub('BoolType'), extname='isUTF8', default=False), - } + minchars = Property('minimum number of character points', IntRange(0, UNLIMITED), + extname='minchars', default=0) + maxchars = Property('maximum number of character points', IntRange(0, UNLIMITED), + extname='maxchars', default=UNLIMITED) + isUTF8 = Property('flag telling whether encoding is UTF-8 instead of ASCII', + Stub('BoolType'), extname='isUTF8', default=False) def __init__(self, minchars=0, maxchars=None, **kwds): super().__init__() @@ -611,7 +612,8 @@ class StringType(DataType): # TextType is a special StringType intended for longer texts (i.e. embedding \n), # whereas StringType is supposed to not contain '\n' # unfortunately, SECoP makes no distinction here.... -# note: content is supposed to follow the format of a git commit message, i.e. a line of text, 2 '\n' + a longer explanation +# note: content is supposed to follow the format of a git commit message, +# i.e. a line of text, 2 '\n' + a longer explanation class TextType(StringType): def __init__(self, maxchars=None): if maxchars is None: @@ -621,7 +623,7 @@ class TextType(StringType): def __repr__(self): if self.maxchars == UNLIMITED: return 'TextType()' - return 'TextType(%d)' % (self.maxchars) + return 'TextType(%d)' % self.maxchars def copy(self): # DataType.copy will not work, because it is exported as 'string' @@ -678,12 +680,10 @@ class ArrayOf(DataType): :param members: the datatype of the elements """ - properties = { - 'minlen': Property('minimum number of elements', IntRange(0), extname='minlen', - default=0), - 'maxlen': Property('maximum number of elements', IntRange(0), extname='maxlen', - mandatory=True), - } + minlen = Property('minimum number of elements', IntRange(0), extname='minlen', + default=0) + maxlen = Property('maximum number of elements', IntRange(0), extname='maxlen', + mandatory=True) def __init__(self, members, minlen=0, maxlen=None): super().__init__() @@ -714,14 +714,14 @@ class ArrayOf(DataType): def setProperty(self, key, value): """set also properties of members""" - if key in self.__class__.properties: + if key in self.propertyDict: super().setProperty(key, value) else: self.members.setProperty(key, value) def export_datatype(self): return dict(type='array', minlen=self.minlen, maxlen=self.maxlen, - members=self.members.export_datatype()) + members=self.members.export_datatype()) def __repr__(self): return 'ArrayOf(%s, %s, %s)' % ( @@ -806,11 +806,10 @@ class TupleOf(DataType): try: if len(value) != len(self.members): raise BadValueError( - 'Illegal number of Arguments! Need %d arguments.' % - (len(self.members))) + 'Illegal number of Arguments! Need %d arguments.' % len(self.members)) # validate elements and return as list return tuple(sub(elem) - for sub, elem in zip(self.members, value)) + for sub, elem in zip(self.members, value)) except Exception as exc: raise BadValueError('Can not validate:', str(exc)) @@ -830,12 +829,12 @@ class TupleOf(DataType): def format_value(self, value, unit=None): return '(%s)' % (', '.join([sub.format_value(elem) - for sub, elem in zip(self.members, value)])) + for sub, elem in zip(self.members, value)])) def compatible(self, other): if not isinstance(other, TupleOf): raise BadValueError('incompatible datatypes') - if len(self.members) != len(other.members) : + if len(self.members) != len(other.members): raise BadValueError('incompatible datatypes') for a, b in zip(self.members, other.members): a.compatible(b) @@ -867,15 +866,15 @@ class StructOf(DataType): if name not in members: raise ProgrammingError( 'Only members of StructOf may be declared as optional!') - self.default = dict((k,el.default) for k, el in members.items()) + self.default = dict((k, el.default) for k, el in members.items()) def copy(self): """DataType.copy does not work when members contain enums""" - return StructOf(self.optional, **{k: v.copy() for k,v in self.members.items()}) + return StructOf(self.optional, **{k: v.copy() for k, v in self.members.items()}) def export_datatype(self): res = dict(type='struct', members=dict((n, s.export_datatype()) - for n, s in list(self.members.items()))) + for n, s in list(self.members.items()))) if self.optional: res['optional'] = self.optional return res @@ -991,8 +990,8 @@ class CommandType(DataType): raise BadValueError('incompatible datatypes') - # internally used datatypes (i.e. only for programming the SEC-node) + class DataTypeType(DataType): def __call__(self, value): """check if given value (a python obj) is a valid datatype @@ -1091,16 +1090,13 @@ class LimitsType(TupleOf): class StatusType(TupleOf): - # shorten initialisation and allow acces to status enumMembers from status values + # shorten initialisation and allow access to status enumMembers from status values def __init__(self, enum): TupleOf.__init__(self, EnumType(enum), StringType()) - self.enum = enum + self._enum = enum def __getattr__(self, key): - enum = TupleOf.__getattr__(self, 'enum') - if hasattr(enum, key): - return getattr(enum, key) - return TupleOf.__getattr__(self, key) + return getattr(self._enum, key) def floatargs(kwds): diff --git a/secop/features.py b/secop/features.py index 8eb9d11..5268f7e 100644 --- a/secop/features.py +++ b/secop/features.py @@ -24,11 +24,10 @@ from secop.datatypes import ArrayOf, BoolType, EnumType, \ FloatRange, StringType, StructOf, TupleOf -from secop.metaclass import ModuleMeta -from secop.modules import Command, Parameter +from secop.modules import Command, Parameter, HasAccessibles -class Feature(metaclass=ModuleMeta): +class Feature(HasAccessibles): """all things belonging to a small, predefined functionality influencing the working of a module""" @@ -39,33 +38,37 @@ class HAS_PID(Feature): # note: (i would still but them in the same group, though) # note: if extra elements are implemented in the pid struct they MUST BE # properly described in the description of the pid Parameter - parameters = { - 'use_pid' : Parameter('use the pid mode', datatype=EnumType(openloop=0, pid_control=1), ), - 'p' : Parameter('proportional part of the regulation', datatype=FloatRange(0), ), - 'i' : Parameter('(optional) integral part', datatype=FloatRange(0), optional=True), - 'd' : Parameter('(optional) derivative part', datatype=FloatRange(0), optional=True), - 'base_output' : Parameter('(optional) minimum output value', datatype=FloatRange(0), optional=True), - 'pid': Parameter('(optional) Struct of p,i,d, minimum output value', - datatype=StructOf(p=FloatRange(0), - i=FloatRange(0), - d=FloatRange(0), - base_output=FloatRange(0), - ), optional=True, - ), # note: struct may be extended with custom elements (names should be prefixed with '_') - 'output' : Parameter('(optional) output of pid-control', datatype=FloatRange(0), optional=True, readonly=False), - } + + # parameters + use_pid = Parameter('use the pid mode', datatype=EnumType(openloop=0, pid_control=1), ) + # pylint: disable=invalid-name + p = Parameter('proportional part of the regulation', datatype=FloatRange(0), ) + i = Parameter('(optional) integral part', datatype=FloatRange(0), optional=True) + d = Parameter('(optional) derivative part', datatype=FloatRange(0), optional=True) + base_output = Parameter('(optional) minimum output value', datatype=FloatRange(0), optional=True) + pid = Parameter('(optional) Struct of p,i,d, minimum output value', + datatype=StructOf(p=FloatRange(0), + i=FloatRange(0), + d=FloatRange(0), + base_output=FloatRange(0), + ), optional=True, + ) # note: struct may be extended with custom elements (names should be prefixed with '_') + output = Parameter('(optional) output of pid-control', datatype=FloatRange(0), optional=True, readonly=False) + class Has_PIDTable(HAS_PID): - parameters = { - 'use_pidtable' : Parameter('use the zoning mode', datatype=EnumType(fixed_pid=0, zone_mode=1)), - 'pidtable' : Parameter('Table of pid-values vs. target temperature', datatype=ArrayOf(TupleOf(FloatRange(0), - StructOf(p=FloatRange(0), - i=FloatRange(0), - d=FloatRange(0), - _heater_range=FloatRange(0), - _base_output=FloatRange(0),),),), optional=True), # struct may include 'heaterrange' - } + + # parameters + use_pidtable = Parameter('use the zoning mode', datatype=EnumType(fixed_pid=0, zone_mode=1)) + pidtable = Parameter('Table of pid-values vs. target temperature', datatype=ArrayOf(TupleOf(FloatRange(0), + StructOf(p=FloatRange(0), + i=FloatRange(0), + d=FloatRange(0), + _heater_range=FloatRange(0), + _base_output=FloatRange(0),),),), optional=True) # struct may include 'heaterrange' + + class HAS_Persistent(Feature): @@ -75,89 +78,98 @@ class HAS_Persistent(Feature): # 'coupled' : Status.BUSY+2, # to be discussed. # 'decoupling' : Status.BUSY+3, # to be discussed. #} - parameters = { - 'persistent_mode': Parameter('Use persistent mode', - datatype=EnumType(off=0,on=1), - default=0, readonly=False), - 'is_persistent': Parameter('current state of persistence', - datatype=BoolType(), optional=True), - 'stored_value': Parameter('current persistence value, often used as the modules value', - datatype='main', unit='$', optional=True), - 'driven_value': Parameter('driven value (outside value, syncs with stored_value if non-persistent)', - datatype='main', unit='$' ), - } + + # parameters + persistent_mode = Parameter('Use persistent mode', + datatype=EnumType(off=0,on=1), + default=0, readonly=False) + is_persistent = Parameter('current state of persistence', + datatype=BoolType(), optional=True) + stored_value = Parameter('current persistence value, often used as the modules value', + datatype='main', unit='$', optional=True) + driven_value = Parameter('driven value (outside value, syncs with stored_value if non-persistent)', + datatype='main', unit='$' ) + class HAS_Tolerance(Feature): # detects IDLE status by checking if the value lies in a given window: # tolerance is the maximum allowed deviation from target, value must lie in this interval # for at least ´timewindow´ seconds. - parameters = { - 'tolerance': Parameter('Half height of the Window', - datatype=FloatRange(0), default=1, unit='$'), - 'timewindow': Parameter('Length of the timewindow to check', - datatype=FloatRange(0), default=30, unit='s', - optional=True), - } + + # parameters + tolerance = Parameter('Half height of the Window', + datatype=FloatRange(0), default=1, unit='$') + timewindow = Parameter('Length of the timewindow to check', + datatype=FloatRange(0), default=30, unit='s', + optional=True) + class HAS_Timeout(Feature): - parameters = { - 'timeout': Parameter('timeout for movement', - datatype=FloatRange(0), default=0, unit='s'), - } + + # parameters + timeout = Parameter('timeout for movement', + datatype=FloatRange(0), default=0, unit='s') + class HAS_Pause(Feature): # just a proposal, can't agree on it.... - parameters = { - 'pause': Command('pauses movement', argument=None, result=None), - 'go': Command('continues movement or start a new one if target was change since the last pause', - argument=None, result=None), - } + + @Command(argument=None, result=None) + def pause(self): + """pauses movement""" + + @Command(argument=None, result=None) + def go(self): + """continues movement or start a new one if target was change since the last pause""" class HAS_Ramp(Feature): - parameters = { - 'ramp': Parameter('speed of movement', unit='$/min', - datatype=FloatRange(0)), - 'use_ramp': Parameter('use the ramping of the setpoint, or jump', - datatype=EnumType(disable_ramp=0, use_ramp=1), - optional=True), - 'setpoint': Parameter('currently active setpoint', - datatype=FloatRange(0), unit='$', - readonly=True, ), - } + + # parameters + ramp =Parameter('speed of movement', unit='$/min', + datatype=FloatRange(0)) + use_ramp = Parameter('use the ramping of the setpoint, or jump', + datatype=EnumType(disable_ramp=0, use_ramp=1), + optional=True) + setpoint = Parameter('currently active setpoint', + datatype=FloatRange(0), unit='$', + readonly=True, ) + class HAS_Speed(Feature): - parameters = { - 'speed' : Parameter('(maximum) speed of movement (of the main value)', - unit='$/s', datatype=FloatRange(0)), - } + + # parameters + speed = Parameter('(maximum) speed of movement (of the main value)', + unit='$/s', datatype=FloatRange(0)) + class HAS_Accel(HAS_Speed): - parameters = { - 'accel' : Parameter('acceleration of movement', unit='$/s^2', - datatype=FloatRange(0)), - 'decel' : Parameter('deceleration of movement', unit='$/s^2', - datatype=FloatRange(0), optional=True), - } + + # parameters + accel = Parameter('acceleration of movement', unit='$/s^2', + datatype=FloatRange(0)) + decel = Parameter('deceleration of movement', unit='$/s^2', + datatype=FloatRange(0), optional=True) + class HAS_MotorCurrents(Feature): - parameters = { - 'movecurrent' : Parameter('Current while moving', - datatype=FloatRange(0)), - 'idlecurrent' : Parameter('Current while idle', - datatype=FloatRange(0), optional=True), - } + + # parameters + movecurrent = Parameter('Current while moving', + datatype=FloatRange(0)) + idlecurrent = Parameter('Current while idle', + datatype=FloatRange(0), optional=True) + class HAS_Curve(Feature): # proposed, not yet agreed upon! - parameters = { - 'curve' : Parameter('Calibration curve', datatype=StringType(80), default=''), - # XXX: tbd. (how to upload/download/select a curve?) - } + + # parameters + curve = Parameter('Calibration curve', datatype=StringType(80), default='') diff --git a/secop/iohandler.py b/secop/iohandler.py index 471a08f..cc9f3fe 100644 --- a/secop/iohandler.py +++ b/secop/iohandler.py @@ -54,7 +54,7 @@ method has to be called explicitly int the write_ method, if needed. """ import re -from secop.metaclass import Done +from secop.modules import Done from secop.errors import ProgrammingError diff --git a/secop/lib/classdoc.py b/secop/lib/classdoc.py index b6a22c5..d00d8fe 100644 --- a/secop/lib/classdoc.py +++ b/secop/lib/classdoc.py @@ -67,6 +67,7 @@ SIMPLETYPES = { 'IntRange': 'int', 'BlobType': 'bytes', 'StringType': 'str', + 'TextType': 'str', 'BoolType': 'bool', 'StructOf': 'dict', } @@ -179,7 +180,7 @@ def append_to_doc(cls, lines, itemcls, name, attrname, fmtfunc): def class_doc_handler(app, what, name, cls, options, lines): if what == 'class': if issubclass(cls, HasProperties): - append_to_doc(cls, lines, Property, 'properties', 'properties', fmt_property) + append_to_doc(cls, lines, Property, 'properties', 'propertyDict', fmt_property) if issubclass(cls, Module): append_to_doc(cls, lines, Parameter, 'parameters', 'accessibles', fmt_param) append_to_doc(cls, lines, Command, 'commands', 'accessibles', fmt_command) diff --git a/secop/lib/sequence.py b/secop/lib/sequence.py index f03244b..08e047a 100644 --- a/secop/lib/sequence.py +++ b/secop/lib/sequence.py @@ -141,7 +141,7 @@ class SequencerMixin: return self.read_hw_status() return self.Status.IDLE, '' - def do_stop(self): + def stop(self): if self.seq_is_alive(): self._seq_stopflag = True diff --git a/secop/metaclass.py b/secop/metaclass.py deleted file mode 100644 index bc827db..0000000 --- a/secop/metaclass.py +++ /dev/null @@ -1,248 +0,0 @@ -# -*- coding: utf-8 -*- -# ***************************************************************************** -# -# This program is free software; you can redistribute it and/or modify it under -# the terms of the GNU General Public License as published by the Free Software -# Foundation; either version 2 of the License, or (at your option) any later -# version. -# -# This program is distributed in the hope that it will be useful, but WITHOUT -# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more -# details. -# -# You should have received a copy of the GNU General Public License along with -# this program; if not, write to the Free Software Foundation, Inc., -# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA -# -# Module authors: -# Enrico Faulhaber -# Markus Zolliker -# -# ***************************************************************************** -"""Define Metaclass for Modules/Features""" - - -from collections import OrderedDict - -from secop.errors import ProgrammingError, BadValueError -from secop.params import Command, Override, Parameter, Accessible, usercommand -from secop.datatypes import EnumType -from secop.properties import PropertyMeta, flatten_dict, Property - - -class Done: - """a special return value for a read/write function - - indicating that the setter is triggered already""" - - -# warning: MAGIC! - -class ModuleMeta(PropertyMeta): - """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__(cls, name, bases, attrs): # pylint: disable=too-many-branches - # allow to declare accessibles directly as class attribute - # all these attributes are removed - flatten_dict('parameters', Parameter, attrs) - # do not remove commands from attrs, they are kept as descriptors - flatten_dict('commands', usercommand, attrs, remove=False) - flatten_dict('properties', Property, attrs) - - commands = attrs.pop('commands', {}) - parameters = attrs.pop('parameters', {}) - overrides = attrs.pop('overrides', {}) - - newtype = type.__new__(cls, name, bases, attrs) - if '__constructed__' in attrs: - return newtype - - newtype = PropertyMeta.__join_properties__(newtype, name, bases, attrs) - - # 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 accessibles in [parameters, commands, overrides]: - accessibles_list.append(accessibles) - accessibles = {} # unordered dict of accessibles, will be sorted later - for accessibles_dict in accessibles_list: - for key, obj in accessibles_dict.items(): - if isinstance(obj, Override): - if key not in accessibles: - raise ProgrammingError("module %s: can not apply Override on %s: no such accessible!" - % (name, key)) - obj = obj.apply(accessibles[key]) - accessibles[key] = obj - else: - aobj = accessibles.get(key) - if aobj: - if obj.kwds is not None: # obj may be used for override - if isinstance(obj, Command) != isinstance(obj, Command): - raise ProgrammingError("module %s.%s: can not override a %s with a %s!" - % (name, key, aobj.__class_.name, obj.__class_.name, )) - obj = aobj.override(obj) - accessibles[key] = obj - setattr(newtype, key, obj) - if not isinstance(obj, (Parameter, Command)): - raise ProgrammingError('%r: accessibles entry %r should be a ' - 'Parameter or Command object!' % (name, key)) - accessibles[key] = obj - - # Correct naming of EnumTypes - for k, v in accessibles.items(): - if isinstance(v, Parameter) and isinstance(v.datatype, EnumType): - 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 for attributes overriding parameter values - for pname, pobj in newtype.accessibles.items(): - if pname in attrs: - value = attrs[pname] - if isinstance(value, (Accessible, Override)): - continue - if isinstance(pobj, Parameter): - try: - value = pobj.datatype(attrs[pname]) - except BadValueError: - raise ProgrammingError('parameter %r can not be set to %r' - % (pname, attrs[pname])) - newtype.accessibles[pname] = pobj.override(default=value) - elif isinstance(pobj, usercommand): - if not callable(attrs[pname]): - raise ProgrammingError('%s.%s overwrites a command' - % (newtype.__name__, pname)) - pobj = pobj.override(func=attrs[name]) - newtype.accessibles[pname] = pobj - - # check validity of Parameter entries - for pname, pobj in newtype.accessibles.items(): - # XXX: create getters for the units of params ?? - - # wrap of reading/writing funcs - if isinstance(pobj, Command): - if isinstance(pobj, usercommand): - do_name = 'do_' + pname - # create additional method do_ for backwards compatibility - if do_name not in attrs: - setattr(newtype, do_name, pobj) - continue - rfunc = attrs.get('read_' + pname, None) - rfunc_handler = pobj.handler.get_read_func(newtype, pname) if pobj.handler else None - if rfunc_handler: - if rfunc: - raise ProgrammingError("parameter '%s' can not have a handler " - "and read_%s" % (pname, pname)) - rfunc = rfunc_handler - else: - for base in bases: - if rfunc is not None: - break - rfunc = getattr(base, 'read_' + pname, None) - - # create wrapper except when read function is already wrapped - if rfunc is None or getattr(rfunc, '__wrapped__', False) is False: - - def wrapped_rfunc(self, pname=pname, rfunc=rfunc): - if rfunc: - self.log.debug("calling %r" % rfunc) - try: - value = rfunc(self) - self.log.debug("rfunc(%s) returned %r" % (pname, value)) - if value is Done: # the setter is already triggered - return getattr(self, pname) - except Exception as e: - self.log.debug("rfunc(%s) failed %r" % (pname, e)) - self.announceUpdate(pname, None, e) - raise - else: - # return cached value - self.log.debug("rfunc(%s): return cached value" % pname) - value = self.accessibles[pname].value - setattr(self, pname, value) # important! trigger the setter - return value - - if rfunc: - wrapped_rfunc.__doc__ = rfunc.__doc__ - setattr(newtype, 'read_' + pname, wrapped_rfunc) - wrapped_rfunc.__wrapped__ = True - - if not pobj.readonly: - wfunc = attrs.get('write_' + pname, None) - if wfunc is None: # ignore the handler, if a write function is present - wfunc = pobj.handler.get_write_func(pname) if pobj.handler else None - for base in bases: - if wfunc is not None: - break - wfunc = getattr(base, 'write_' + pname, None) - - # create wrapper except when write function is already wrapped - if wfunc is None or getattr(wfunc, '__wrapped__', False) is False: - - def wrapped_wfunc(self, value, pname=pname, wfunc=wfunc): - self.log.debug("check validity of %s = %r" % (pname, value)) - pobj = self.accessibles[pname] - value = pobj.datatype(value) - if wfunc: - self.log.debug('calling %s %r(%r)' % (wfunc.__name__, wfunc, value)) - returned_value = wfunc(self, value) - if returned_value is Done: # the setter is already triggered - return getattr(self, pname) - if returned_value is not None: # goodie: accept missing return value - value = returned_value - setattr(self, pname, value) - return value - - if wfunc: - wrapped_wfunc.__doc__ = wfunc.__doc__ - setattr(newtype, 'write_' + pname, wrapped_wfunc) - wrapped_wfunc.__wrapped__ = True - - def getter(self, pname=pname): - return self.accessibles[pname].value - - def setter(self, value, pname=pname): - self.announceUpdate(pname, value) - - setattr(newtype, pname, property(getter, setter)) - - # check information about Command's - for attrname in attrs: - if attrname.startswith('do_'): - if attrname[3:] not in newtype.accessibles: - raise ProgrammingError('%r: command %r has to be specified ' - 'explicitly!' % (name, attrname[3:])) - - attrs['__constructed__'] = True - return newtype - - @property - def configurables(cls): - # note: this ends up as an property of the Module class (not on the instance)! - - # dict of properties with Property and Parameter with dict of properties - res = {} - # collect info about properties - for pn, pv in cls.properties.items(): - if pv.settable: - res[pn] = pv - # collect info about parameters and their properties - for param, pobj in cls.accessibles.items(): - res[param] = {} - for pn, pv in pobj.getProperties().items(): - if pv.settable: - res[param][pn] = pv - return res diff --git a/secop/modules.py b/secop/modules.py index 4b22fdc..f4b493e 100644 --- a/secop/modules.py +++ b/secop/modules.py @@ -20,12 +20,11 @@ # Markus Zolliker # # ***************************************************************************** -"""Define Baseclasses for real Modules implemented in the server""" +"""Define base classes for real Modules implemented in the server""" import sys import time -from collections import OrderedDict from secop.datatypes import EnumType, FloatRange, BoolType, IntRange, \ StringType, TupleOf, get_datatype, ArrayOf, TextType, StatusType @@ -33,19 +32,147 @@ from secop.errors import ConfigError, ProgrammingError, SECoPError, BadValueErro SilentError, InternalError, secop_error from secop.lib import formatException, formatExtendedStack, mkthread from secop.lib.enum import Enum -from secop.metaclass import ModuleMeta -from secop.params import PREDEFINED_ACCESSIBLES, Command, Override, Parameter, Parameters, Commands +from secop.params import PREDEFINED_ACCESSIBLES, Command, Parameter, Accessible from secop.properties import HasProperties, Property from secop.poller import Poller, BasicPoller -# XXX: connect with 'protocol'-Modules. -# Idea: every Module defined herein is also a 'protocol'-Module, -# all others MUST derive from those, the 'interface'-class is still derived -# from these base classes (how to do this?) +Done = object() #: a special return value for a read/write function indicating that the setter is triggered already -class Module(HasProperties, metaclass=ModuleMeta): +class HasAccessibles(HasProperties): + """base class of Module + + joining the class's properties, parameters and commands dicts with + those of base classes. + wrap read_*/write_* methods + (so the dispatcher will get notified of changed values) + """ + @classmethod + def __init_subclass__(cls): # pylint: disable=too-many-branches + super().__init_subclass__() + # merge accessibles from all sub-classes, treat overrides + # for now, allow to use also the old syntax (parameters/commands dict) + accessibles = {} + for base in cls.__bases__: + accessibles.update(getattr(base, 'accessibles', {})) + newaccessibles = {k: v for k, v in cls.__dict__.items() if isinstance(v, Accessible)} + for aname, aobj in accessibles.items(): + value = getattr(cls, aname, None) + if not isinstance(value, Accessible): # else override is already done in __set_name__ + anew = aobj.override(value) + newaccessibles[aname] = anew + setattr(cls, aname, anew) + anew.__set_name__(cls, aname) + ordered = {} + for aname in cls.__dict__.get('paramOrder', ()): + if aname in accessibles: + ordered[aname] = accessibles.pop(aname) + elif aname in newaccessibles: + ordered[aname] = newaccessibles.pop(aname) + # ignore unknown names + # starting from old accessibles not mentioned, append items from 'order' + accessibles.update(ordered) + # then new accessibles not mentioned + accessibles.update(newaccessibles) + cls.accessibles = accessibles + + # Correct naming of EnumTypes + for k, v in accessibles.items(): + if isinstance(v, Parameter) and isinstance(v.datatype, EnumType): + v.datatype.set_name(k) + + # check validity of Parameter entries + for pname, pobj in accessibles.items(): + # XXX: create getters for the units of params ?? + + # wrap of reading/writing funcs + if isinstance(pobj, Command): + # nothing to do for now + continue + rfunc = cls.__dict__.get('read_' + pname, None) + rfunc_handler = pobj.handler.get_read_func(cls, pname) if pobj.handler else None + if rfunc_handler: + if rfunc: + raise ProgrammingError("parameter '%s' can not have a handler " + "and read_%s" % (pname, pname)) + rfunc = rfunc_handler + + # create wrapper except when read function is already wrapped + if rfunc is None or getattr(rfunc, '__wrapped__', False) is False: + + def wrapped_rfunc(self, pname=pname, rfunc=rfunc): + if rfunc: + self.log.debug("calling %r" % rfunc) + try: + value = rfunc(self) + self.log.debug("rfunc(%s) returned %r" % (pname, value)) + if value is Done: # the setter is already triggered + return getattr(self, pname) + except Exception as e: + self.log.debug("rfunc(%s) failed %r" % (pname, e)) + self.announceUpdate(pname, None, e) + raise + else: + # return cached value + self.log.debug("rfunc(%s): return cached value" % pname) + value = self.accessibles[pname].value + setattr(self, pname, value) # important! trigger the setter + return value + + if rfunc: + wrapped_rfunc.__doc__ = rfunc.__doc__ + setattr(cls, 'read_' + pname, wrapped_rfunc) + wrapped_rfunc.__wrapped__ = True + + if not pobj.readonly: + wfunc = getattr(cls, 'write_' + pname, None) + if wfunc is None: # ignore the handler, if a write function is present + wfunc = pobj.handler.get_write_func(pname) if pobj.handler else None + + # create wrapper except when write function is already wrapped + if wfunc is None or getattr(wfunc, '__wrapped__', False) is False: + + def wrapped_wfunc(self, value, pname=pname, wfunc=wfunc): + self.log.debug("check validity of %s = %r" % (pname, value)) + pobj = self.accessibles[pname] + value = pobj.datatype(value) + if wfunc: + self.log.debug('calling %s %r(%r)' % (wfunc.__name__, wfunc, value)) + returned_value = wfunc(self, value) + if returned_value is Done: # the setter is already triggered + return getattr(self, pname) + if returned_value is not None: # goodie: accept missing return value + value = returned_value + setattr(self, pname, value) + return value + + if wfunc: + wrapped_wfunc.__doc__ = wfunc.__doc__ + setattr(cls, 'write_' + pname, wrapped_wfunc) + wrapped_wfunc.__wrapped__ = True + + # check information about Command's + for attrname in cls.__dict__: + if attrname.startswith('do_'): + raise ProgrammingError('%r: old style command %r not supported anymore' + % (cls.__name__, attrname)) + + res = {} + # collect info about properties + for pn, pv in cls.propertyDict.items(): + if pv.settable: + res[pn] = pv + # collect info about parameters and their properties + for param, pobj in cls.accessibles.items(): + res[param] = {} + for pn, pv in pobj.getProperties().items(): + if pv.settable: + res[param][pn] = pv + cls.configurables = res + + +class Module(HasAccessibles): """basic module all SECoP modules derive from this. @@ -58,7 +185,8 @@ class Module(HasProperties, metaclass=ModuleMeta): Notes: - the programmer normally should not need to reimplement :meth:`__init__` - - within modules, parameters should only be addressed as ``self.``, i.e. ``self.value``, ``self.target`` etc... + - within modules, parameters should only be addressed as ``self.``, + i.e. ``self.value``, ``self.target`` etc... - these are accessing the cached version. - they can also be written to, generating an async update @@ -77,22 +205,17 @@ class Module(HasProperties, metaclass=ModuleMeta): # note: properties don't change after startup and are usually filled # with data from a 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': Property('Flag if this Module is to be exported', BoolType(), default=True, export=False), - 'group': Property('Optional group the Module belongs to', StringType(), default='', extname='group'), - 'description': Property('Description of the module', TextType(), extname='description', mandatory=True), - 'meaning': Property('Optional Meaning indicator', TupleOf(StringType(),IntRange(0,50)), - default=('',0), extname='meaning'), - 'visibility': Property('Optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3), - default='user', extname='visibility'), - 'implementation': Property('Internal name of the implementation class of the module', StringType(), - extname='implementation'), - 'interface_classes': Property('Offical highest Interface-class of the module', ArrayOf(StringType()), - extname='interface_classes'), - # what else? - } + export = Property('flag if this module is to be exported', BoolType(), default=True, export=False) + group = Property('optional group the module belongs to', StringType(), default='', extname='group') + description = Property('description of the module', TextType(), extname='description', mandatory=True) + meaning = Property('optional meaning indicator', TupleOf(StringType(), IntRange(0, 50)), + default=('', 0), extname='meaning') + visibility = Property('optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3), + default='user', extname='visibility') + implementation = Property('internal name of the implementation class of the module', StringType(), + extname='implementation') + interface_classes = Property('offical highest Interface-class of the module', ArrayOf(StringType()), + extname='interface_classes') # properties, parameters and commands are auto-merged upon subclassing parameters = {} @@ -113,14 +236,14 @@ class Module(HasProperties, metaclass=ModuleMeta): # handle module properties # 1) make local copies of properties - super(Module, self).__init__() + super().__init__() # 2) check and apply properties specified in cfgdict # specified as '. = ' # (this is for legacy config files only) for k, v in list(cfgdict.items()): # keep list() as dict may change during iter if k[0] == '.': - if k[1:] in self.__class__.properties: + if k[1:] in self.propertyDict: self.setProperty(k[1:], cfgdict.pop(k)) else: raise ConfigError('Module %r has no property %r' % @@ -128,20 +251,20 @@ class Module(HasProperties, metaclass=ModuleMeta): # 3) check and apply properties specified in cfgdict as # ' = ' (without '.' prefix) - for k in self.__class__.properties: + for k in self.propertyDict: if k in cfgdict: self.setProperty(k, cfgdict.pop(k)) # 4) set automatic properties mycls = self.__class__ myclassname = '%s.%s' % (mycls.__module__, mycls.__name__) - self.properties['implementation'] = myclassname + self.implementation = myclassname # list of all 'secop' modules - self.properties['interface_classes'] = [ - b.__name__ for b in mycls.__mro__ if b.__module__.startswith('secop.modules')] + # self.interface_classes = [ + # b.__name__ for b in mycls.__mro__ if b.__module__.startswith('secop.modules')] # list of only the 'highest' secop module class - self.properties['interface_classes'] = [[ - b.__name__ for b in mycls.__mro__ if b.__module__.startswith('secop.modules')][0]] + self.interface_classes = [ + b.__name__ for b in mycls.__mro__ if b.__module__.startswith('secop.modules')][0:1] # handle Features # XXX: todo @@ -150,7 +273,7 @@ class Module(HasProperties, metaclass=ModuleMeta): # 1) make local copies of parameter objects # they need to be individual per instance since we use them also # to cache the current value + qualifiers... - accessibles = OrderedDict() + accessibles = {} # conversion from exported names to internal attribute names accessiblename2attr = {} for aname, aobj in self.accessibles.items(): @@ -159,31 +282,31 @@ class Module(HasProperties, metaclass=ModuleMeta): if isinstance(aobj, Parameter): # fix default properties poll and needscfg if aobj.poll is None: - aobj.properties['poll'] = bool(aobj.handler) + aobj.poll = bool(aobj.handler) if aobj.needscfg is None: - aobj.properties['needscfg'] = not aobj.poll + aobj.needscfg = not aobj.poll if not self.export: # do not export parameters of a module not exported - aobj.properties['export'] = False + aobj.export = False if aobj.export: if aobj.export is True: predefined_obj = PREDEFINED_ACCESSIBLES.get(aname, None) if predefined_obj: if isinstance(aobj, predefined_obj): - aobj.setProperty('export', aname) + aobj.export = aname else: raise ProgrammingError("can not use '%s' as name of a %s" % - (aname, aobj.__class__.__name__)) - else: # create custom parameter - aobj.setProperty('export', '_' + aname) + (aname, aobj.__class__.__name__)) + else: # create custom parameter + aobj.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 = 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)) + self.parameters = {k: v for k, v in accessibles.items() if isinstance(v, Parameter)} + self.commands = {k: v for k, v in accessibles.items() if isinstance(v, Command)} # 2) check and apply parameter_properties # specified as '. = ' @@ -200,6 +323,9 @@ class Module(HasProperties, metaclass=ModuleMeta): else: raise ConfigError('Module %s: Parameter %r has no property %r!' % (self.name, paramname, propname)) + else: + raise ConfigError('Module %s has no Parameter %r!' % + (self.name, paramname)) # 3) check config for problems: # only accept remaining config items specified in parameters @@ -209,7 +335,7 @@ class Module(HasProperties, metaclass=ModuleMeta): 'Module %s:config Parameter %r ' 'not understood! (use one of %s)' % (self.name, k, ', '.join(list(self.parameters) + - list(self.__class__.properties)))) + list(self.propertyDict)))) # 4) complain if a Parameter entry has no default value and # is not specified in cfgdict and deal with parameters to be written. @@ -348,7 +474,6 @@ class Module(HasProperties, metaclass=ModuleMeta): modobj.announceUpdate(p, value) self.valueCallbacks[pname].append(cb) - def isBusy(self, status=None): """helper function for treating substates of BUSY correctly""" # defined even for non drivable (used for dynamic polling) @@ -403,31 +528,22 @@ class Module(HasProperties, metaclass=ModuleMeta): class Readable(Module): - """basic readable Module""" + """basic readable module""" # pylint: disable=invalid-name Status = Enum('Status', - IDLE = 100, - WARN = 200, - UNSTABLE = 270, - ERROR = 400, - DISABLED = 0, - UNKNOWN = 401, - ) #: status codes - parameters = { - 'value': Parameter('current value of the Module', readonly=True, - datatype=FloatRange(), - poll=True, - ), - 'pollinterval': Parameter('sleeptime between polls', default=5, - readonly=False, - datatype=FloatRange(0.1, 120), - ), - 'status': Parameter('current status of the Module', - default=(Status.IDLE, ''), - datatype=TupleOf(EnumType(Status), StringType()), - readonly=True, poll=True, - ), - } + IDLE=100, + WARN=200, + UNSTABLE=270, + ERROR=400, + DISABLED=0, + UNKNOWN=401, + ) #: status codes + + value = Parameter('current value of the module', FloatRange(), poll=True) + status = Parameter('current status of the module', TupleOf(EnumType(Status), StringType()), + default=(Status.IDLE, ''), poll=True) + pollinterval = Parameter('sleeptime between polls', FloatRange(0.1, 120), + default=5, readonly=False) def startModule(self, started_callback): """start basic polling thread""" @@ -476,11 +592,9 @@ class Readable(Module): class Writable(Readable): """basic writable module""" - parameters = { - 'target': Parameter('target value of the Module', - default=0, readonly=False, datatype=FloatRange(), - ), - } + + target = Parameter('target value of the module', + default=0, readonly=False, datatype=FloatRange()) class Drivable(Writable): @@ -488,17 +602,7 @@ class Drivable(Writable): Status = Enum(Readable.Status, BUSY=300) #: status codes - commands = { - 'stop': Command( - 'cease driving, go to IDLE state', - argument=None, - result=None - ), - } - - overrides = { - 'status': Override(datatype=StatusType(Status)), - } + status = Parameter(datatype=StatusType(Status)) # override Readable.status def isBusy(self, status=None): """check for busy, treating substates correctly @@ -532,23 +636,16 @@ class Drivable(Writable): self.pollOneParam(pname) return fastpoll - def do_stop(self): - """default implementation of the stop command - - by default does nothing.""" + @Command(None, result=None) + def stop(self): + """cease driving, go to IDLE state""" class Communicator(Module): """basic abstract communication module""" - commands = { - "communicate": Command("provides the simplest mean to communication", - argument=StringType(), - result=StringType() - ), - } - - def do_communicate(self, command): + @Command(StringType(), result=StringType()) + def communicate(self, command): """communicate command :param command: the command to be sent @@ -569,7 +666,8 @@ class Attached(Property): # we can not put this to properties.py, as it needs datatypes def __init__(self, attrname=None): self.attrname = attrname - super().__init__('attached module', StringType()) + # we can not make it mandatory, as the check in Module.__init__ will be before auto-assign in HasIodev + super().__init__('attached module', StringType(), mandatory=False) def __repr__(self): return 'Attached(%s)' % (repr(self.attrname) if self.attrname else '') diff --git a/secop/params.py b/secop/params.py index 9c94d84..c6e21a8 100644 --- a/secop/params.py +++ b/secop/params.py @@ -24,62 +24,69 @@ import inspect -import itertools -from collections import OrderedDict from secop.datatypes import CommandType, DataType, StringType, BoolType, EnumType, DataTypeType, ValueType, OrType, \ - NoneOr, TextType, IntRange, TupleOf + NoneOr, TextType, IntRange, TupleOf, StructOf from secop.errors import ProgrammingError, BadValueError from secop.properties import HasProperties, Property -object_counter = itertools.count(1) +UNSET = object() # an argument not given, not even None class Accessible(HasProperties): """base class for Parameter and Command""" - properties = {} kwds = None # is a dict if it might be used as Override - def __init__(self, ctr, **kwds): - self.ctr = ctr or next(object_counter) - super(Accessible, self).__init__() - # do not use self.properties.update here, as no invalid values should be + def __init__(self, **kwds): + super().__init__() + self.init(kwds) + + def init(self, kwds): + # do not use self.propertyValues.update here, as no invalid values should be # assigned to properties, even not before checkProperties - for k,v in kwds.items(): + for k, v in kwds.items(): self.setProperty(k, v) - def __repr__(self): - props = [] - for k, prop in sorted(self.__class__.properties.items()): - v = self.properties.get(k, prop.default) - if v != prop.default: - props.append('%s=%r' % (k, v)) - return '%s(%s, ctr=%d)' % (self.__class__.__name__, ', '.join(props), self.ctr) + def inherit(self, cls, owner): + for base in owner.__bases__: + if hasattr(base, self.name): + aobj = getattr(base, 'accessibles', {}).get(self.name) + if aobj: + if not isinstance(aobj, cls): + raise ProgrammingError('%s %s.%s can not inherit from a %s' % + (cls.__name__, owner.__name__, self.name, aobj.__class__.__name__)) + # inherit from aobj + for pname, value in aobj.propertyValues.items(): + if pname not in self.propertyValues: + self.propertyValues[pname] = value + break def as_dict(self): - return self.properties + return self.propertyValues - def override(self, from_object=None, **kwds): - """return a copy of ourselfs, modified by """ - props = dict(self.properties, ctr=self.ctr) - if from_object: - props.update(from_object.kwds) - props.update(kwds) - props['datatype'] = props['datatype'].copy() - return type(self)(inherit=False, internally_called=True, **props) + def override(self, value=UNSET, **kwds): + """return a copy, overridden by a bare attribute + + and/or some properties""" + raise NotImplementedError def copy(self): - """return a copy of ourselfs""" - props = dict(self.properties, ctr=self.ctr) - # deep copy, as datatype might be altered from config - props['datatype'] = props['datatype'].copy() - return type(self)(inherit=False, internally_called=True, **props) + """return a (deep) copy of ourselfs""" + raise NotImplementedError def for_export(self): """prepare for serialisation""" - return self.exportProperties() + raise NotImplementedError + + def __repr__(self): + props = [] + for k, prop in sorted(self.propertyDict.items()): + v = self.propertyValues.get(k, prop.default) + if v != prop.default: + props.append('%s=%r' % (k, v)) + return '%s(%s)' % (self.__class__.__name__, ', '.join(props)) class Parameter(Accessible): @@ -87,65 +94,78 @@ class Parameter(Accessible): :param description: description :param datatype: the datatype - :param inherit: whether properties not given should be inherited. - defaults to True when datatype or description is missing, else to False - :param reorder: when True, put this parameter after all inherited items in the accessible list + :param inherit: whether properties not given should be inherited :param kwds: optional properties - :param ctr: (for internal use only) - :param internally_used: (for internal use only) """ # storage for Parameter settings + value + qualifiers - properties = { - 'description': Property('mandatory description of the parameter', TextType(), - extname='description', mandatory=True), - 'datatype': Property('datatype of the Parameter (SECoP datainfo)', DataTypeType(), - extname='datainfo', mandatory=True), - 'readonly': Property('not changeable via SECoP (default True)', BoolType(), - extname='readonly', default=True), - 'group': Property('optional parameter group this parameter belongs to', StringType(), - extname='group', default=''), - 'visibility': Property('optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3), - extname='visibility', default=1), - 'constant': Property('optional constant value for constant parameters', ValueType(), - extname='constant', default=None, mandatory=False), - 'default': Property('[internal] default (startup) value of this parameter ' - 'if it can not be read from the hardware', - ValueType(), export=False, default=None, mandatory=False), - 'export': Property(''' - [internal] export settings - - * False: not accessible via SECoP. - * True: exported, name automatic. - * a string: exported with custom name''', - OrType(BoolType(), StringType()), export=False, default=True), - 'poll': Property(''' - [internal] polling indicator - - may be: - - * None (omitted): will be converted to True/False if handler is/is not None - * False or 0 (never poll this parameter) - * True or 1 (AUTO), converted to SLOW (readonly=False) - DYNAMIC (*status* and *value*) or REGULAR (else) - * 2 (SLOW), polled with lower priority and a multiple of pollinterval - * 3 (REGULAR), polled with pollperiod - * 4 (DYNAMIC), if BUSY, with a fraction of pollinterval, - else polled with pollperiod - ''', - NoneOr(IntRange()), export=False, default=None), - 'needscfg': Property('[internal] needs value in config', NoneOr(BoolType()), export=False, default=None), - 'optional': Property('[internal] is this parameter optional?', BoolType(), export=False, - settable=False, default=False), - 'handler': Property('[internal] overload the standard read and write functions', - ValueType(), export=False, default=None, mandatory=False, settable=False), - 'initwrite': Property('[internal] write this parameter on initialization' - ' (default None: write if given in config)', - NoneOr(BoolType()), export=False, default=None, mandatory=False, settable=False), - } + description = Property( + 'mandatory description of the parameter', TextType(), + extname='description', mandatory=True) + datatype = Property( + 'datatype of the Parameter (SECoP datainfo)', DataTypeType(), + extname='datainfo', mandatory=True) + readonly = Property( + 'not changeable via SECoP (default True)', BoolType(), + extname='readonly', default=True, export='always') + group = Property( + 'optional parameter group this parameter belongs to', StringType(), + extname='group', default='') + visibility = Property( + 'optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3), + extname='visibility', default=1) + constant = Property( + 'optional constant value for constant parameters', ValueType(), + extname='constant', default=None) + default = Property( + '''[internal] default (startup) value of this parameter - def __init__(self, description=None, datatype=None, inherit=True, *, - reorder=False, ctr=None, internally_called=False, **kwds): + if it can not be read from the hardware''', ValueType(), + export=False, default=None) + export = Property( + '''[internal] export settings + + * False: not accessible via SECoP. + * True: exported, name automatic. + * a string: exported with custom name''', OrType(BoolType(), StringType()), + export=False, default=True) + poll = Property( + '''[internal] polling indicator + + may be: + + * None (omitted): will be converted to True/False if handler is/is not None + * False or 0 (never poll this parameter) + * True or 1 (AUTO), converted to SLOW (readonly=False) + DYNAMIC (*status* and *value*) or REGULAR (else) + * 2 (SLOW), polled with lower priority and a multiple of pollinterval + * 3 (REGULAR), polled with pollperiod + * 4 (DYNAMIC), if BUSY, with a fraction of pollinterval, + else polled with pollperiod + ''', NoneOr(IntRange()), + export=False, default=None) + needscfg = Property( + '[internal] needs value in config', NoneOr(BoolType()), + export=False, default=None) + optional = Property( + '[internal] is this parameter optional?', BoolType(), + export=False, settable=False, default=False) + handler = Property( + '[internal] overload the standard read and write functions', ValueType(), + export=False, default=None, settable=False) + initwrite = Property( + '''[internal] write this parameter on initialization + + default None: write if given in config''', NoneOr(BoolType()), + export=False, default=None, settable=False) + + # used on the instance copy only + value = None + timestamp = 0 + readerror = None + + def __init__(self, description=None, datatype=None, inherit=True, *, unit=None, constant=None, **kwds): + super().__init__(**kwds) if datatype is not None: if not isinstance(datatype, DataType): if isinstance(datatype, type) and issubclass(datatype, DataType): @@ -154,57 +174,92 @@ class Parameter(Accessible): else: raise ProgrammingError( 'datatype MUST be derived from class DataType!') - kwds['datatype'] = datatype + self.datatype = datatype + if 'default' in kwds: + self.default = datatype(kwds['default']) if description is not None: - if not internally_called: - description = inspect.cleandoc(description) - kwds['description'] = description + self.description = inspect.cleandoc(description) - unit = kwds.pop('unit', None) - if unit is not None and datatype: # for legacy code only - datatype.setProperty('unit', unit) + # save for __set_name__ + self._inherit = inherit + self._unit = unit # for legacy code only + self._constant = constant - constant = kwds.get('constant') - if constant is not None: - constant = datatype(constant) + def __get__(self, instance, owner): + # not used yet + if instance is None: + return self + return instance.parameters[self.name].value + + def __set__(self, obj, value): + obj.announceUpdate(self.name, value) + + def __set_name__(self, owner, name): + self.name = name + + if self._inherit: + self.inherit(Parameter, owner) + + # check for completeness + missing_properties = [pname for pname in ('description', 'datatype') if pname not in self.propertyValues] + if missing_properties: + raise ProgrammingError('Parameter %s.%s needs a %s' % + (owner.__name__, name, ' and a '.join(missing_properties))) + if self._unit is not None: + self.datatype.setProperty('unit', self._unit) + + if self._constant is not None: + constant = self.datatype(self._constant) # The value of the `constant` property should be the # serialised version of the constant, or unset - kwds['constant'] = datatype.export_value(constant) - kwds['readonly'] = True - if internally_called: # fixes in case datatype has changed - default = kwds.get('default') - if default is not None: - try: - datatype(default) - except BadValueError: - # clear default, if it does not match datatype - kwds['default'] = None - super().__init__(ctr, **kwds) - if inherit: - if reorder: - kwds['ctr'] = next(object_counter) - if unit is not None: - kwds['unit'] = unit - self.kwds = kwds # contains only the items which must be overwritten + self.constant = self.datatype.export_value(constant) + self.readonly = True - # internal caching: value and timestamp of last change... - self.value = self.default - self.timestamp = 0 - self.readerror = None # if not None, indicates that last read was not successful + if 'default' in self.propertyValues: + # fixes in case datatype has changed + try: + self.datatype(self.default) + except BadValueError: + # clear default, if it does not match datatype + self.propertyValues.pop('default') + + if self.export is True: + if isinstance(self, PREDEFINED_ACCESSIBLES.get(name, type(None))): + self.export = name + else: + self.export = '_' + name + + def copy(self): + # deep copy, as datatype might be altered from config + res = Parameter() + res.name = self.name + res.init(self.propertyValues) + res.datatype = res.datatype.copy() + return res + + def override(self, value=UNSET, **kwds): + res = self.copy() + res.init(kwds) + if value is not UNSET: + res.value = res.datatype(value) + return res def export_value(self): return self.datatype.export_value(self.value) + def for_export(self): + return dict(self.exportProperties(), readonly=self.readonly) + def getProperties(self): """get also properties of datatype""" - superProp = super().getProperties().copy() - superProp.update(self.datatype.getProperties()) - return superProp + super_prop = super().getProperties().copy() + super_prop.update(self.datatype.getProperties()) + return super_prop def setProperty(self, key, value): """set also properties of datatype""" - if key in self.__class__.properties: + if key in self.propertyDict: super().setProperty(key, value) else: self.datatype.setProperty(key, value) @@ -213,208 +268,168 @@ class Parameter(Accessible): super().checkProperties() self.datatype.checkProperties() - def for_export(self): - """prepare for serialisation - - readonly is mandatory for serialisation, but not for declaration in classes - """ - r = super().for_export() - if 'readonly' not in r: - r['readonly'] = self.__class__.properties['readonly'].default - return r - - -class UnusedClass: - # do not derive anything from this! - pass - - -class Parameters(OrderedDict): - """class storage for Parameters""" - def __init__(self, *args, **kwds): - self.exported = {} # only for lookups! - super(Parameters, self).__init__(*args, **kwds) - - def __setitem__(self, key, value): - if value.export: - if isinstance(value, PREDEFINED_ACCESSIBLES.get(key, UnusedClass)): - value.properties['export'] = key - else: - value.properties['export'] = '_' + key - self.exported[value.export] = key - super(Parameters, self).__setitem__(key, value) - - def __getitem__(self, item): - return super(Parameters, self).__getitem__(self.exported.get(item, item)) - - -class Commands(Parameters): - """class storage for Commands""" - - -class Override: - """Stores the overrides to be applied to a Parameter - - note: overrides are applied by the metaclass during class creating - reorder=True: use position of Override instead of inherited for the order - """ - def __init__(self, description="", datatype=None, *, reorder=False, **kwds): - self.kwds = kwds - # allow to override description and datatype without keyword - if description: - self.kwds['description'] = description - if datatype is not None: - self.kwds['datatype'] = datatype - if reorder: # result from apply must use new ctr from Override - self.kwds['ctr'] = next(object_counter) - - def __repr__(self): - return '%s(%s)' % (self.__class__.__name__, ', '.join( - ['%s=%r' % (k, v) for k, v in sorted(self.kwds.items())])) - - def apply(self, obj): - return obj.override(self) - class Command(Accessible): - # to be merged with usercommand - properties = { - 'description': Property('description of the Command', TextType(), - extname='description', export=True, mandatory=True), - 'group': Property('optional command group of the command.', StringType(), - extname='group', export=True, default=''), - 'visibility': Property('optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3), - extname='visibility', export=True, default=1), - 'export': Property(''' - [internal] export settings - - * False: not accessible via SECoP. - * True: exported, name automatic. - * a string: exported with custom name''', - OrType(BoolType(), StringType()), export=False, default=True), - 'optional': Property('[internal] is the command optional to implement? (vs. mandatory)', - BoolType(), export=False, default=False, settable=False), - 'datatype': Property('[internal] datatype of the command, auto generated from \'argument\' and \'result\'', - DataTypeType(), extname='datainfo', mandatory=True), - 'argument': Property('datatype of the argument to the command, or None', - NoneOr(DataTypeType()), export=False, mandatory=True), - 'result': Property('datatype of the result from the command, or None', - NoneOr(DataTypeType()), export=False, mandatory=True), - } - - def __init__(self, description=None, *, reorder=False, inherit=True, - internally_called=False, ctr=None, **kwds): - if internally_called: - inherit = False - # make sure either all or no datatype info is in kwds - if 'argument' in kwds or 'result' in kwds: - datatype = CommandType(kwds.get('argument'), kwds.get('result')) - else: - datatype = kwds.get('datatype') - datainfo = {} - datainfo['datatype'] = datatype or CommandType() - datainfo['argument'] = datainfo['datatype'].argument - datainfo['result'] = datainfo['datatype'].result - if datatype: - kwds.update(datainfo) - if description is not None: - kwds['description'] = description - if datatype: - datainfo = {} - super(Command, self).__init__(ctr, **datainfo, **kwds) - if inherit: - if reorder: - kwds['ctr'] = next(object_counter) - self.kwds = kwds - - @property - def argument(self): - return self.datatype.argument - - @property - def result(self): - return self.datatype.result - - -class usercommand(Command): """decorator to turn a method into a command :param argument: the datatype of the argument or None :param result: the datatype of the result or None - :param inherit: whether properties not given should be inherited. - defaults to True when datatype or description is missing, else to False - :param reorder: when True, put this command after all inherited items in the accessible list + :param inherit: whether properties not given should be inherited :param kwds: optional properties - - {all properties} """ + description = Property( + 'description of the Command', TextType(), + extname='description', export=True, mandatory=True) + group = Property( + 'optional command group of the command.', StringType(), + extname='group', export=True, default='') + visibility = Property( + 'optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3), + extname='visibility', export=True, default=1) + export = Property( + '''[internal] export settings + + * False: not accessible via SECoP. + * True: exported, name automatic. + * a string: exported with custom name''', OrType(BoolType(), StringType()), + export=False, default=True) + optional = Property( + '[internal] is the command optional to implement? (vs. mandatory)', BoolType(), + export=False, default=False, settable=False) + datatype = Property( + "datatype of the command, auto generated from 'argument' and 'result'", + DataTypeType(), extname='datainfo', export='always') + argument = Property( + 'datatype of the argument to the command, or None', NoneOr(DataTypeType()), + export=False, mandatory=True) + result = Property( + 'datatype of the result from the command, or None', NoneOr(DataTypeType()), + export=False, mandatory=True) + func = None - def __init__(self, argument=False, result=None, inherit=True, **kwds): + def __init__(self, argument=False, *, result=None, inherit=True, **kwds): + super().__init__(**kwds) if result or kwds or isinstance(argument, DataType) or not callable(argument): # normal case - self.func = None if argument is False and result: argument = None if argument is not False: if isinstance(argument, (tuple, list)): - # goodie: allow declaring multiple arguments as a tuple - # TODO: check that calling works properly + # goodie: treat as TupleOf argument = TupleOf(*argument) - kwds['argument'] = argument - kwds['result'] = result - self.kwds = kwds + self.argument = argument + self.result = result else: - # goodie: allow @usercommand instead of @usercommand() + # goodie: allow @Command instead of @Command() self.func = argument # this is the wrapped method! - if argument.__doc__ is not None: - kwds['description'] = argument.__doc__ + if argument.__doc__: + self.description = inspect.cleandoc(argument.__doc__) self.name = self.func.__name__ - super().__init__(kwds.pop('description', ''), inherit=inherit, **kwds) - - def override(self, from_object=None, **kwds): - result = super().override(from_object, **kwds) - func = kwds.pop('func', from_object.func if from_object else None) - if func: - result(func) # pylint: disable=not-callable - return result + self._inherit = inherit # save for __set_name__ def __set_name__(self, owner, name): self.name = name + if self.func is None: + raise ProgrammingError('Command %s.%s must be used as a method decorator' % + (owner.__name__, name)) + if self._inherit: + self.inherit(Command, owner) + + self.datatype = CommandType(self.argument, self.result) + if self.export is True: + if isinstance(self, PREDEFINED_ACCESSIBLES.get(name, type(None))): + self.export = name + else: + self.export = '_' + name def __get__(self, obj, owner=None): if obj is None: return self if not self.func: - raise ProgrammingError('usercommand %s not properly configured' % self.name) + raise ProgrammingError('Command %s not properly configured' % self.name) return self.func.__get__(obj, owner) - def __call__(self, fun): - description = self.kwds.get('description') or fun.__doc__ - self.properties['description'] = self.kwds['description'] = description - self.name = fun.__name__ - self.func = fun + def __call__(self, func): + if 'description' not in self.propertyValues and func.__doc__: + self.description = inspect.cleandoc(func.__doc__) + self.func = func return self + def copy(self): + res = Command() + res.name = self.name + res.func = self.func + res.init(self.propertyValues) + if res.argument: + res.argument = res.argument.copy() + if res.result: + res.result = res.result.copy() + res.datatype = CommandType(res.argument, res.result) + return res + + def override(self, value=UNSET, **kwds): + res = self.copy() + res.init(kwds) + if value is not UNSET: + res.func = value + return res + + def do(self, module_obj, argument): + """perform function call + + :param module_obj: the module on which the command is to be executed + :param argument: the argument from the do command + :returns: the return value converted to the result type + + - when the argument type is TupleOf, the function is called with multiple arguments + - when the argument type is StructOf, the function is called with keyworded arguments + - the validity of the argument/s is/are checked + """ + func = self.__get__(module_obj) + if self.argument: + # validate + argument = self.argument(argument) + if isinstance(self.argument, TupleOf): + res = func(*argument) + elif isinstance(self.argument, StructOf): + res = func(**argument) + else: + res = func(argument) + else: + if argument is not None: + raise BadValueError('%s.%s takes no arguments' % (module_obj.__class__.__name__, self.name)) + res = func() + if self.result: + return self.result(res) + return None # silently ignore the result from the method + + def for_export(self): + return self.exportProperties() + + def __repr__(self): + result = super().__repr__() + return result[:-1] + ', %r)' % self.func if self.func else result + # list of predefined accessibles with their type PREDEFINED_ACCESSIBLES = dict( - value = Parameter, - status = Parameter, - target = Parameter, - pollinterval = Parameter, - ramp = Parameter, - user_ramp = Parameter, - setpoint = Parameter, - time_to_target = Parameter, - unit = Parameter, # reserved name - loglevel = Parameter, # reserved name - mode = Parameter, # reserved name - stop = Command, - reset = Command, - go = Command, - abort = Command, - shutdown = Command, - communicate = Command, + value=Parameter, + status=Parameter, + target=Parameter, + pollinterval=Parameter, + ramp=Parameter, + user_ramp=Parameter, + setpoint=Parameter, + time_to_target=Parameter, + unit=Parameter, # reserved name + loglevel=Parameter, # reserved name + mode=Parameter, # reserved name + stop=Command, + reset=Command, + go=Command, + abort=Command, + shutdown=Command, + communicate=Command, ) diff --git a/secop/properties.py b/secop/properties.py index 8d0d24d..c413d63 100644 --- a/secop/properties.py +++ b/secop/properties.py @@ -23,27 +23,44 @@ """Define validated data types.""" +import sys import inspect -from collections import OrderedDict -from secop.errors import ProgrammingError, ConfigError, BadValueError +from secop.errors import ConfigError, ProgrammingError, BadValueError -def flatten_dict(dictname, itemcls, attrs, remove=True): - properties = {} - # allow to declare properties directly as class attribute - # all these attributes are removed - for k, v in attrs.items(): - if isinstance(v, tuple) and v and isinstance(v[0], itemcls): - # this might happen when migrating from old to new style - raise ProgrammingError('declared %r with trailing comma' % k) - if isinstance(v, itemcls): - properties[k] = v - if remove: - for k in properties: - attrs.pop(k) - properties.update(attrs.get(dictname, {})) - attrs[dictname] = properties +class HasDescriptorMeta(type): + def __new__(cls, name, bases, attrs): + newtype = type.__new__(cls, name, bases, attrs) + if sys.version_info < (3, 6): + # support older python versions + for key, attr in attrs.items(): + if hasattr(attr, '__set_name__'): + attr.__set_name__(newtype, key) + newtype.__init_subclass__() + return newtype + + +class HasDescriptors(metaclass=HasDescriptorMeta): + @classmethod + def __init_subclass__(cls): + # when migrating old style declarations, sometimes the trailing comma is not removed + bad = [k for k, v in cls.__dict__.items() + if isinstance(v, tuple) and len(v) == 1 and hasattr(v[0], '__set_name__')] + if bad: + raise ProgrammingError('misplaced trailing comma after %s.%s' % (cls.__name__, '/'.join(bad))) + + @classmethod + def filterDescriptors(cls, filter_type): + res = {} + for name in dir(cls): + desc = getattr(cls, name, None) + if isinstance(desc, filter_type): + res[name] = desc + return res + + +UNSET = object() # an unset value, not even None # storage for 'properties of a property' @@ -56,7 +73,8 @@ class Property: :param default: a default value. SECoP properties are normally not sent to the ECS, when they match the default :param extname: external name - :param export: sent to the ECS when True. defaults to True, when ``extname`` is given + :param export: sent to the ECS when True. defaults to True, when ``extname`` is given. + special value 'always': export also when matching the default :param mandatory: defaults to True, when ``default`` is not given. indicates that it must have a value assigned from the cfg file (or, in case of a module property, it may be assigned as a class attribute) :param settable: settable from the cfg file @@ -64,148 +82,134 @@ class Property: # note: this is intended to be used on base classes. # the VALUES of the properties are on the instances! - def __init__(self, description, datatype, default=None, extname='', export=False, mandatory=None, settable=True): + def __init__(self, description, datatype, default=UNSET, extname='', export=False, mandatory=None, + settable=True, value=UNSET, name=''): if not callable(datatype): raise ValueError('datatype MUST be a valid DataType or a basic_validator') self.description = inspect.cleandoc(description) - self.default = datatype.default if default is None else datatype(default) + self.default = datatype.default if default is UNSET else datatype(default) self.datatype = datatype self.extname = extname self.export = export or bool(extname) if mandatory is None: - mandatory = default is None + mandatory = default is UNSET self.mandatory = mandatory self.settable = settable or mandatory # settable means settable from the cfg file + self.value = UNSET if value is UNSET else datatype(value) + self.name = name + + def __get__(self, instance, owner): + if instance is None: + return self + return instance.propertyValues.get(self.name, self.default) + + def __set__(self, instance, value): + instance.propertyValues[self.name] = self.datatype(value) + + def __set_name__(self, owner, name): + self.name = name + if self.export and not self.extname: + self.extname = '_' + name + if self.description == '_': + # the programmer indicates, that the name is already speaking for itself + self.description = name.replace('_', ' ') def __repr__(self): - return 'Property(%r, %s, default=%r, extname=%r, export=%r, mandatory=%r, settable=%r)' % ( - self.description, self.datatype, self.default, self.extname, self.export, - self.mandatory, self.settable) + extras = ['default=%s' % repr(self.default)] + if self.export: + extras.append('extname=%r' % self.extname) + extras.append('export=%r' % self.export) + if self.mandatory: + extras.append('mandatory=True') + if not self.settable: + extras.append('settable=False') + if self.value is not UNSET: + extras.append('value=%s' % repr(self.value)) + if not self.name: + extras.append('name=%r' % self.name) + return 'Property(%r, %s, %s)' % (self.description, self.datatype, ', '.join(extras)) -class Properties(OrderedDict): - """a collection of `Property` objects - - checks values upon assignment. - You can either assign a Property object, or a value - (which must pass the validator of the already existing Property) - """ - def __setitem__(self, key, value): - if not isinstance(value, Property): - raise ProgrammingError('setting property %r on classes is not supported!' % key) - # make sure, extname is valid if export is True - if not value.extname and value.export: - value.extname = '_%s' % key # generate custom key - elif value.extname and not value.export: - value.export = True - OrderedDict.__setitem__(self, key, value) - - def __delitem__(self, key): - raise ProgrammingError('deleting Properties is not supported!') - - -class PropertyMeta(type): - """Metaclass for HasProperties - - joining the class's properties with those of base classes. - """ - - def __new__(cls, name, bases, attrs): - newtype = type.__new__(cls, name, bases, attrs) - if '__constructed__' in attrs: - return newtype - - flatten_dict('properties', Property, attrs) - newtype = cls.__join_properties__(newtype, name, bases, attrs) - - attrs['__constructed__'] = True - return newtype - - @classmethod - def __join_properties__(cls, newtype, name, bases, attrs): - # merge properties from all sub-classes - properties = Properties() - for base in reversed(bases): - properties.update(getattr(base, "properties", {})) - # update with properties from new class - properties.update(attrs.get('properties', {})) - newtype.properties = properties - - # generate getters - for k, po in properties.items(): - - def getter(self, pname=k): - val = self.__class__.properties[pname].default - return self.properties.get(pname, val) - - if k in attrs and not isinstance(attrs[k], (property, Property)): - if callable(attrs[k]): - raise ProgrammingError('%r: property %r collides with method' - % (newtype, k)) - # store the attribute value for putting on the instance later - try: - # for inheritance reasons, it seems best to store it as a renamed attribute - setattr(newtype, '_initProp_' + k, po.datatype(attrs[k])) - except BadValueError: - raise ProgrammingError('%r: property %r can not be set to %r' - % (newtype, k, attrs[k])) - setattr(newtype, k, property(getter)) - return newtype - - -class HasProperties(metaclass=PropertyMeta): - properties = {} +class HasProperties(HasDescriptors): + propertyValues = None def __init__(self): super(HasProperties, self).__init__() - self.initProperties() - - def initProperties(self): # store property values in the instance, keep descriptors on the class - self.properties = {} - # pre-init with properties default value (if any) - for pn, po in self.__class__.properties.items(): - value = getattr(self, '_initProp_' + pn, self) - if value is not self: # property value was given as attribute - self.properties[pn] = value - elif not po.mandatory: - self.properties[pn] = po.default + self.propertyValues = {} + # pre-init + for pn, po in self.propertyDict.items(): + if po.value is not UNSET: + self.setProperty(pn, po.value) + + @classmethod + def __init_subclass__(cls): + super().__init_subclass__() + # raise an error when an attribute is a tuple with one single descriptor as element + # when migrating old style declarations, sometimes the trailing comma is not removed + bad = [k for k, v in cls.__dict__.items() + if isinstance(v, tuple) and len(v) == 1 and hasattr(v[0], '__set_name__')] + if bad: + raise ProgrammingError('misplaced trailing comma after %s.%s' % (cls.__name__, '/'.join(bad))) + properties = {} + for base in cls.__bases__: + properties.update(getattr(base, 'propertyDict', {})) + properties.update(cls.filterDescriptors(Property)) + cls.propertyDict = properties + # treat overriding properties with bare values + for pn, po in properties.items(): + value = cls.__dict__.get(pn, po) + if not isinstance(value, Property): # attribute is a bare value + po = Property(**po.__dict__) + try: + po.value = po.datatype(value) + except BadValueError: + for base in cls.__bases__: + if pn in getattr(base, 'propertyDict', {}): + if callable(value): + raise ProgrammingError('method %s.%s collides with property of %s' % + (cls.__name__, pn, base.__name__)) + raise ProgrammingError('can not set property %s.%s to %r' % + (cls.__name__, pn, value)) + cls.propertyDict[pn] = po def checkProperties(self): """validates properties and checks for min... <= max...""" - for pn, po in self.__class__.properties.items(): - if po.export and po.mandatory: - if pn not in self.properties: + for pn, po in self.propertyDict.items(): + if po.mandatory: + if pn not in self.propertyDict: name = getattr(self, 'name', self.__class__.__name__) raise ConfigError('Property %r of %s needs a value of type %r!' % (pn, name, po.datatype)) # apply validator (which may complain further) - self.properties[pn] = po.datatype(self.properties[pn]) - for pn, po in self.__class__.properties.items(): + self.propertyValues[pn] = po.datatype(self.propertyValues[pn]) + for pn, po in self.propertyDict.items(): if pn.startswith('min'): maxname = 'max' + pn[3:] - minval = self.properties[pn] - maxval = self.properties.get(maxname, minval) + minval = self.propertyValues.get(pn, po.default) + maxval = self.propertyValues.get(maxname, minval) if minval > maxval: raise ConfigError('%s=%r must be <= %s=%r for %r' % (pn, minval, maxname, maxval, self)) - def getProperties(self): - return self.__class__.properties + return self.propertyDict def exportProperties(self): # export properties which have # export=True and # mandatory=True or non_default=True res = {} - for pn, po in self.__class__.properties.items(): - val = self.properties.get(pn, None) - if po.export and (po.mandatory or val != po.default): + for pn, po in self.propertyDict.items(): + val = self.propertyValues.get(pn, po.default) + if po.export and (po.export == 'always' or val != po.default): try: val = po.datatype.export_value(val) except AttributeError: - pass # for properties, accept simple datatypes without export_value + pass # for properties, accept simple datatypes without export_value res[po.extname] = val return res def setProperty(self, key, value): - self.properties[key] = self.__class__.properties[key].datatype(value) + # this is overwritten by Param.setProperty and DataType.setProperty + # in oder to extend setting to inner properties + # otherwise direct setting of self. = value is preferred + self.propertyValues[key] = self.propertyDict[key].datatype(value) diff --git a/secop/protocol/dispatcher.py b/secop/protocol/dispatcher.py index 3c6e903..e9296e2 100644 --- a/secop/protocol/dispatcher.py +++ b/secop/protocol/dispatcher.py @@ -42,7 +42,7 @@ import threading from collections import OrderedDict from time import time as currenttime -from secop.errors import BadValueError, NoSuchCommandError, NoSuchModuleError, \ +from secop.errors import NoSuchCommandError, NoSuchModuleError, \ NoSuchParameterError, ProtocolError, ReadOnlyError, SECoPServerError from secop.params import Parameter from secop.protocol.messages import COMMANDREPLY, DESCRIPTIONREPLY, \ @@ -53,10 +53,10 @@ from secop.protocol.messages import COMMANDREPLY, DESCRIPTIONREPLY, \ def make_update(modulename, pobj): if pobj.readerror: return (ERRORPREFIX + EVENTREPLY, '%s:%s' % (modulename, pobj.export), - # error-report ! - [pobj.readerror.name, repr(pobj.readerror), dict(t=pobj.timestamp)]) + # error-report ! + [pobj.readerror.name, repr(pobj.readerror), dict(t=pobj.timestamp)]) return (EVENTREPLY, '%s:%s' % (modulename, pobj.export), - [pobj.export_value(), dict(t=pobj.timestamp)]) + [pobj.export_value(), dict(t=pobj.timestamp)]) class Dispatcher: @@ -109,7 +109,7 @@ class Dispatcher: self._subscriptions.setdefault(eventname, set()).add(conn) def unsubscribe(self, conn, eventname): - if not ':' in eventname: + if ':' not in eventname: # also remove 'more specific' subscriptions for k, v in self._subscriptions.items(): if k.startswith('%s:' % eventname): @@ -177,7 +177,7 @@ class Dispatcher: result = {'modules': OrderedDict()} for modulename in self._export: module = self.get_module(modulename) - if not module.properties.get('export', False): + if not module.export: continue # some of these need rework ! mod_desc = {'accessibles': self.export_accessibles(modulename)} @@ -186,7 +186,7 @@ class Dispatcher: result['modules'][modulename] = mod_desc result['equipment_id'] = self.equipment_id result['firmware'] = 'FRAPPY - The Python Framework for SECoP' - result['version'] = '2019.08' + result['version'] = '2021.02' result.update(self.nodeprops) return result @@ -195,40 +195,24 @@ class Dispatcher: if moduleobj is None: raise NoSuchModuleError('Module %r does not exist' % modulename) - cmdname = moduleobj.commands.exported.get(exportedname, None) - if cmdname is None: - raise NoSuchCommandError('Module %r has no command %r' % (modulename, exportedname)) - cmdspec = moduleobj.commands[cmdname] - if argument is None and cmdspec.datatype.argument is not None: - raise BadValueError("Command '%s:%s' needs an argument" % (modulename, cmdname)) - - if argument is not None and cmdspec.datatype.argument is None: - raise BadValueError("Command '%s:%s' takes no argument" % (modulename, cmdname)) - - if cmdspec.datatype.argument: - # validate! - argument = cmdspec.datatype(argument) + cname = moduleobj.accessiblename2attr.get(exportedname) + cobj = moduleobj.commands.get(cname) + if cobj is None: + raise NoSuchCommandError('Module %r has no command %r' % (modulename, cname or exportedname)) # now call func # note: exceptions are handled in handle_request, not here! - func = getattr(moduleobj, 'do_' + cmdname) - res = func() if argument is None else func(argument) - - # pipe through cmdspec.datatype.result - if cmdspec.datatype.result: - res = cmdspec.datatype.result(res) - - return res, dict(t=currenttime()) + return cobj.do(moduleobj, argument), dict(t=currenttime()) def _setParameterValue(self, modulename, exportedname, value): moduleobj = self.get_module(modulename) if moduleobj is None: raise NoSuchModuleError('Module %r does not exist' % modulename) - pname = moduleobj.parameters.exported.get(exportedname, None) - if pname is None: - raise NoSuchParameterError('Module %r has no parameter %r' % (modulename, exportedname)) - pobj = moduleobj.parameters[pname] + pname = moduleobj.accessiblename2attr.get(exportedname) + pobj = moduleobj.parameters.get(pname) + if pobj is None: + raise NoSuchParameterError('Module %r has no parameter %r' % (modulename, pname or exportedname)) if pobj.constant is not None: raise ReadOnlyError("Parameter %s:%s is constant and can not be changed remotely" % (modulename, pname)) @@ -252,10 +236,10 @@ class Dispatcher: if moduleobj is None: raise NoSuchModuleError('Module %r does not exist' % modulename) - pname = moduleobj.parameters.exported.get(exportedname, None) - if pname is None: - raise NoSuchParameterError('Module %r has no parameter %r' % (modulename, exportedname)) - pobj = moduleobj.parameters[pname] + pname = moduleobj.accessiblename2attr.get(exportedname) + pobj = moduleobj.parameters.get(pname) + if pobj is None: + raise NoSuchParameterError('Module %r has no parameter %r' % (modulename, pname or exportedname)) if pobj.constant is not None: # really needed? we could just construct a readreply instead.... # raise ReadOnlyError('This parameter is constant and can not be accessed remotely.') @@ -321,15 +305,13 @@ class Dispatcher: return (WRITEREPLY, specifier, list(self._setParameterValue(modulename, pname, data))) def handle_do(self, conn, specifier, data): - # XXX: should this be done asyncron? we could just return the reply in - # that case modulename, cmd = specifier.split(':', 1) return (COMMANDREPLY, specifier, list(self._execute_command(modulename, cmd, data))) def handle_ping(self, conn, specifier, data): if data: raise ProtocolError('ping requests don\'t take data!') - return (HEARTBEATREPLY, specifier, [None, {'t':currenttime()}]) + return (HEARTBEATREPLY, specifier, [None, {'t': currenttime()}]) def handle_activate(self, conn, specifier, data): if data: diff --git a/secop/proxy.py b/secop/proxy.py index b494839..dc07545 100644 --- a/secop/proxy.py +++ b/secop/proxy.py @@ -28,14 +28,11 @@ from secop.properties import Property from secop.stringio import HasIodev from secop.lib import get_class from secop.client import SecopClient, decode_msg, encode_msg_frame -from secop.errors import ConfigError, make_secop_error, CommunicationFailedError +from secop.errors import ConfigError, make_secop_error, CommunicationFailedError, BadValueError class ProxyModule(HasIodev, Module): - properties = { - 'module': - Property('remote module name', datatype=StringType(), default=''), - } + module = Property('remote module name', datatype=StringType(), default='') pollerClass = None _consistency_check_done = False @@ -55,7 +52,7 @@ class ProxyModule(HasIodev, Module): def initModule(self): if not self.module: - self.properties['module'] = self.name + self.module = self.name self._secnode = self._iodev.secnode self._secnode.register_callback(self.module, self.updateEvent, self.descriptiveDataChange, self.nodeStateChange) @@ -103,9 +100,9 @@ class ProxyModule(HasIodev, Module): dt = props['datatype'] try: cobj.datatype.compatible(dt) - except Exception: + except BadValueError: self.log.warning('remote command %s:%s is not compatible: %r != %r' - % (self.module, pname, pobj.datatype, dt)) + % (self.module, cname, cobj.datatype, dt)) # what to do if descriptive data does not match? # we might raise an exception, but this would lead to a reconnection, # which might not help. @@ -141,14 +138,7 @@ PROXY_CLASSES = [ProxyDrivable, ProxyWritable, ProxyReadable, ProxyModule] class SecNode(Module): - properties = { - 'uri': - Property('uri of a SEC node', datatype=StringType()), - } - commands = { - 'request': - Command('send a request', argument=StringType(), result=StringType()) - } + uri = Property('uri of a SEC node', datatype=StringType()) def earlyInit(self): self.secnode = SecopClient(self.uri, self.log) @@ -156,8 +146,9 @@ class SecNode(Module): def startModule(self, started_callback): self.secnode.spawn_connect(started_callback) - def do_request(self, msg): - """for test purposes""" + @Command(StringType(), result=StringType()) + def request(self, msg): + """send a request, for debugging purposes""" reply = self.secnode.request(*decode_msg(msg.encode('utf-8'))) return encode_msg_frame(*reply).decode('utf-8') @@ -184,17 +175,12 @@ def proxy_class(remote_class, name=None): else: raise ConfigError('%r is no SECoP module class' % remote_class) - parameters = {} - commands = {} - attrs = dict(parameters=parameters, commands=commands, properties=rcls.properties) + attrs = rcls.propertyDict.copy() for aname, aobj in rcls.accessibles.items(): if isinstance(aobj, Parameter): - pobj = aobj.copy() - parameters[aname] = pobj - pobj.properties['poll'] = False - pobj.properties['handler'] = None - pobj.properties['needscfg'] = False + pobj = aobj.override(poll=False, handler=None, needscfg=False) + attrs[aname] = pobj def rfunc(self, pname=aname): value, _, readerror = self._secnode.getParameter(self.name, pname) @@ -216,12 +202,11 @@ def proxy_class(remote_class, name=None): elif isinstance(aobj, Command): cobj = aobj.copy() - commands[aname] = cobj def cfunc(self, arg=None, cname=aname): return self._secnode.execCommand(self.name, cname, arg) - attrs['do_' + aname] = cfunc + attrs[aname] = cobj(cfunc) else: raise ConfigError('do not now about %r in %s.accessibles' % (aobj, remote_class)) diff --git a/secop/server.py b/secop/server.py index 77800d2..03228d8 100644 --- a/secop/server.py +++ b/secop/server.py @@ -227,7 +227,7 @@ class Server: # all objs created, now start them up and interconnect for modname, modobj in self.modules.items(): self.log.info('registering module %r' % modname) - self.dispatcher.register_module(modobj, modname, modobj.properties['export']) + self.dispatcher.register_module(modobj, modname, modobj.export) if modobj.pollerClass is not None: # a module might be explicitly excluded from polling by setting pollerClass to None modobj.pollerClass.add_to_table(poll_table, modobj) @@ -236,10 +236,10 @@ class Server: # handle attached modules for modname, modobj in self.modules.items(): - for propname, propobj in modobj.__class__.properties.items(): + for propname, propobj in modobj.propertyDict.items(): if isinstance(propobj, Attached): setattr(modobj, propobj.attrname or '_' + propname, - self.dispatcher.get_module(modobj.properties[propname])) + self.dispatcher.get_module(getattr(modobj, propname))) # call init on each module after registering all for modname, modobj in self.modules.items(): modobj.initModule() diff --git a/secop/simulation.py b/secop/simulation.py index 34a63b9..e51f015 100644 --- a/secop/simulation.py +++ b/secop/simulation.py @@ -22,6 +22,8 @@ """Define Simulation classes""" +# TODO: rework after syntax change! + import random from time import sleep diff --git a/secop/stringio.py b/secop/stringio.py index af3170e..777ee33 100644 --- a/secop/stringio.py +++ b/secop/stringio.py @@ -27,11 +27,10 @@ import time import threading import re from secop.lib.asynconn import AsynConn, ConnectionClosed -from secop.modules import Module, Communicator, Parameter, Command, Property, Attached +from secop.modules import Module, Communicator, Parameter, Command, Property, Attached, Done from secop.datatypes import StringType, FloatRange, ArrayOf, BoolType, TupleOf, ValueType -from secop.errors import CommunicationFailedError, CommunicationSilentError +from secop.errors import CommunicationFailedError, CommunicationSilentError, ConfigError from secop.poller import REGULAR -from secop.metaclass import Done class StringIO(Communicator): @@ -39,38 +38,22 @@ class StringIO(Communicator): self healing is assured by polling the parameter 'is_connected' """ - properties = { - 'uri': - Property('hostname:portnumber', datatype=StringType()), - 'end_of_line': - Property('end_of_line character', datatype=ValueType(), - default='\n', settable=True), - 'encoding': - Property('used encoding', datatype=StringType(), - default='ascii', settable=True), - 'identification': - Property(''' - identification - - a list of tuples with commands and expected responses as regexp, - to be sent on connect''', - datatype=ArrayOf(TupleOf(StringType(), StringType())), default=[], export=False), - } - parameters = { - 'timeout': - Parameter('timeout', datatype=FloatRange(0), default=2), - 'wait_before': - Parameter('wait time before sending', datatype=FloatRange(), default=0), - 'is_connected': - Parameter('connection state', datatype=BoolType(), readonly=False, poll=REGULAR), - 'pollinterval': - Parameter('reconnect interval', datatype=FloatRange(0), readonly=False, default=10), - } - commands = { - 'multicomm': - Command('execute multiple commands in one go', - argument=ArrayOf(StringType()), result=ArrayOf(StringType())) - } + uri = Property('hostname:portnumber', datatype=StringType()) + end_of_line = Property('end_of_line character', datatype=ValueType(), + default='\n', settable=True) + encoding = Property('used encoding', datatype=StringType(), + default='ascii', settable=True) + identification = Property(''' + identification + + a list of tuples with commands and expected responses as regexp, + to be sent on connect''', + datatype=ArrayOf(TupleOf(StringType(), StringType())), default=[], export=False) + + timeout = Parameter('timeout', datatype=FloatRange(0), default=2) + wait_before = Parameter('wait time before sending', datatype=FloatRange(), default=0) + is_connected = Parameter('connection state', datatype=BoolType(), readonly=False, poll=REGULAR) + pollinterval = Parameter('reconnect interval', datatype=FloatRange(0), readonly=False, default=10) _reconnectCallbacks = None @@ -105,11 +88,12 @@ class StringIO(Communicator): self._conn = AsynConn(uri, self._eol_read) self.is_connected = True for command, regexp in self.identification: - reply = self.do_communicate(command) + reply = self.communicate(command) if not re.match(regexp, reply): self.closeConnection() raise CommunicationFailedError('bad response: %s does not match %s' % (reply, regexp)) + def closeConnection(self): """close connection @@ -125,7 +109,7 @@ class StringIO(Communicator): self.is_connected is changed only by self.connectStart or self.closeConnection """ if self.is_connected: - return Done # no need for intermediate updates + return Done # no need for intermediate updates try: self.connectStart() if self._last_error: @@ -170,7 +154,7 @@ class StringIO(Communicator): if removeme: self._reconnectCallbacks.pop(key) - def do_communicate(self, command): + def communicate(self, command): """send a command and receive a reply using end_of_line, encoding and self._lock @@ -179,6 +163,8 @@ class StringIO(Communicator): """ if not self.is_connected: self.read_is_connected() # try to reconnect + if not self._conn: + raise CommunicationSilentError('can not connect to %r' % self.uri) try: with self._lock: # read garbage and wait before send @@ -210,11 +196,13 @@ class StringIO(Communicator): self.log.error(self._last_error) raise - def do_multicomm(self, commands): + @Command(ArrayOf(StringType()), result=ArrayOf(StringType())) + def multicomm(self, commands): + """communicate multiple request/replies in one row""" replies = [] with self._lock: for cmd in commands: - replies.append(self.do_communicate(cmd)) + replies.append(self.communicate(cmd)) return replies @@ -223,17 +211,15 @@ class HasIodev(Module): not only StringIO ! """ - properties = { - 'iodev': Attached(), - 'uri': Property('uri for automatic creation of the attached communication module', - StringType(), default=''), - } + iodev = Attached() + uri = Property('uri for automatic creation of the attached communication module', + StringType(), default='') iodevDict = {} def __init__(self, name, logger, opts, srv): iodev = opts.get('iodev') - super().__init__(name, logger, opts, srv) + Module.__init__(self, name, logger, opts, srv) if self.uri: opts = {'uri': self.uri, 'description': 'communication device for %s' % name, 'export': False} @@ -243,7 +229,9 @@ class HasIodev(Module): iodev = self.iodevClass(ioname, srv.log.getChild(ioname), opts, srv) srv.modules[ioname] = iodev self.iodevDict[self.uri] = ioname - self.setProperty('iodev', ioname) + self.iodev = ioname + elif not self.iodev: + raise ConfigError("Module %s needs a value for either 'uri' or 'iodev'" % name) def initModule(self): try: @@ -254,4 +242,4 @@ class HasIodev(Module): super().initModule() def sendRecv(self, command): - return self._iodev.do_communicate(command) + return self._iodev.communicate(command) diff --git a/secop_demo/cryo.py b/secop_demo/cryo.py index 7efc466..c3e189a 100644 --- a/secop_demo/cryo.py +++ b/secop_demo/cryo.py @@ -27,18 +27,16 @@ from math import atan from secop.datatypes import EnumType, FloatRange, TupleOf, StringType, BoolType from secop.lib import clamp, mkthread -from secop.modules import Drivable, Override, Parameter +from secop.modules import Drivable, Parameter, Command # test custom property (value.test can be changed in config file) from secop.properties import Property -Parameter.properties['test'] = Property('A Property for testing purposes', StringType(), default='', export=True) +Parameter.propertyDict['test'] = Property('A Property for testing purposes', StringType(), default='', export=True) class CryoBase(Drivable): - properties = { - 'is_cryo': Property('private Flag if this is a cryostat', BoolType(), default=True, export=True), - } + is_cryo = Property('private Flag if this is a cryostat', BoolType(), default=True, export=True) class Cryostat(CryoBase): @@ -49,93 +47,88 @@ class Cryostat(CryoBase): - thermal transfer between regulation and samplen """ - parameters = dict( - jitter=Parameter("amount of random noise on readout values", - datatype=FloatRange(0, 1), unit="K", - default=0.1, readonly=False, export=False, - ), - T_start=Parameter("starting temperature for simulation", - datatype=FloatRange(0), default=10, - export=False, - ), - looptime=Parameter("timestep for simulation", - datatype=FloatRange(0.01, 10), unit="s", default=1, - readonly=False, export=False, + jitter = Parameter("amount of random noise on readout values", + datatype=FloatRange(0, 1), unit="K", + default=0.1, readonly=False, export=False, ), - ramp=Parameter("ramping speed of the setpoint", - datatype=FloatRange(0, 1e3), unit="K/min", default=1, - readonly=False, - ), - setpoint=Parameter("current setpoint during ramping else target", - datatype=FloatRange(), default=1, unit='K', - ), - maxpower=Parameter("Maximum heater power", - datatype=FloatRange(0), default=1, unit="W", - readonly=False, - group='heater_settings', - ), - heater=Parameter("current heater setting", - datatype=FloatRange(0, 100), default=0, unit="%", - group='heater_settings', - ), - heaterpower=Parameter("current heater power", - datatype=FloatRange(0), default=0, unit="W", - group='heater_settings', - ), - target=Override("target temperature", - datatype=FloatRange(0), default=0, unit="K", + T_start = Parameter("starting temperature for simulation", + datatype=FloatRange(0), default=10, + export=False, + ), + looptime = Parameter("timestep for simulation", + datatype=FloatRange(0.01, 10), unit="s", default=1, + readonly=False, export=False, + ), + ramp = Parameter("ramping speed of the setpoint", + datatype=FloatRange(0, 1e3), unit="K/min", default=1, readonly=False, ), - value=Override("regulation temperature", - datatype=FloatRange(0), default=0, unit="K", - test='TEST', + setpoint = Parameter("current setpoint during ramping else target", + datatype=FloatRange(), default=1, unit='K', + ), + maxpower = Parameter("Maximum heater power", + datatype=FloatRange(0), default=1, unit="W", + readonly=False, + group='heater_settings', + ), + heater = Parameter("current heater setting", + datatype=FloatRange(0, 100), default=0, unit="%", + group='heater_settings', + ), + heaterpower = Parameter("current heater power", + datatype=FloatRange(0), default=0, unit="W", + group='heater_settings', + ), + target = Parameter("target temperature", + datatype=FloatRange(0), default=0, unit="K", + readonly=False, + ), + value = Parameter("regulation temperature", + datatype=FloatRange(0), default=0, unit="K", + test='TEST', + ), + pid = Parameter("regulation coefficients", + datatype=TupleOf(FloatRange(0), FloatRange(0, 100), + FloatRange(0, 100)), + default=(40, 10, 2), readonly=False, + group='pid', ), - pid=Parameter("regulation coefficients", - datatype=TupleOf(FloatRange(0), FloatRange(0, 100), - FloatRange(0, 100)), - default=(40, 10, 2), readonly=False, + # pylint: disable=invalid-name + p = Parameter("regulation coefficient 'p'", + datatype=FloatRange(0), default=40, unit="%/K", readonly=False, group='pid', ), - p=Parameter("regulation coefficient 'p'", - datatype=FloatRange(0), default=40, unit="%/K", readonly=False, - group='pid', - ), - i=Parameter("regulation coefficient 'i'", - datatype=FloatRange(0, 100), default=10, readonly=False, - group='pid', - ), - d=Parameter("regulation coefficient 'd'", - datatype=FloatRange(0, 100), default=2, readonly=False, - group='pid', - ), - mode=Parameter("mode of regulation", - datatype=EnumType('mode', ramp=None, pid=None, openloop=None), - default='ramp', - readonly=False, - ), - pollinterval=Override("polling interval", - datatype=FloatRange(0), default=5, - ), - tolerance=Parameter("temperature range for stability checking", - datatype=FloatRange(0, 100), default=0.1, unit='K', + i = Parameter("regulation coefficient 'i'", + datatype=FloatRange(0, 100), default=10, readonly=False, + group='pid', + ), + d = Parameter("regulation coefficient 'd'", + datatype=FloatRange(0, 100), default=2, readonly=False, + group='pid', + ), + mode = Parameter("mode of regulation", + datatype=EnumType('mode', ramp=None, pid=None, openloop=None), + default='ramp', + readonly=False, + ), + pollinterval = Parameter("polling interval", + datatype=FloatRange(0), default=5, + ), + tolerance = Parameter("temperature range for stability checking", + datatype=FloatRange(0, 100), default=0.1, unit='K', + readonly=False, + group='stability', + ), + window = Parameter("time window for stability checking", + datatype=FloatRange(1, 900), default=30, unit='s', + readonly=False, + group='stability', + ), + timeout = Parameter("max waiting time for stabilisation check", + datatype=FloatRange(1, 36000), default=900, unit='s', readonly=False, group='stability', ), - window=Parameter("time window for stability checking", - datatype=FloatRange(1, 900), default=30, unit='s', - readonly=False, - group='stability', - ), - timeout=Parameter("max waiting time for stabilisation check", - datatype=FloatRange(1, 36000), default=900, unit='s', - readonly=False, - group='stability', - ), - ) - commands = dict( - stop=Override( - "Stop ramping the setpoint\n\nby setting the current setpoint as new target"), - ) def initModule(self): self._stopflag = False @@ -180,8 +173,11 @@ class Cryostat(CryoBase): def read_pid(self): return (self.p, self.i, self.d) - def do_stop(self): - # stop the ramp by setting current setpoint as target + @Command() + def stop(self): + """Stop ramping the setpoint + + by setting the current setpoint as new target""" # XXX: discussion: take setpoint or current value ??? self.write_target(self.setpoint) diff --git a/secop_demo/modules.py b/secop_demo/modules.py index 61de9ef..29809cb 100644 --- a/secop_demo/modules.py +++ b/secop_demo/modules.py @@ -28,42 +28,39 @@ import time from secop.datatypes import ArrayOf, BoolType, EnumType, \ FloatRange, IntRange, StringType, StructOf, TupleOf from secop.lib.enum import Enum -from secop.modules import Drivable, Override, Parameter as SECoP_Parameter, Readable +from secop.modules import Drivable, Parameter as SECoP_Parameter, Readable from secop.properties import Property class Parameter(SECoP_Parameter): - properties = { - 'test' : Property('A property for testing purposes', StringType(), default='', mandatory=False, extname='test'), - } + test = Property('A property for testing purposes', StringType(), default='', mandatory=False, extname='test') + PERSIST = 101 + class Switch(Drivable): """switch it on or off.... """ - parameters = { - 'value': Override('current state (on or off)', + + value = Parameter('current state (on or off)', + datatype=EnumType(on=1, off=0), default=0, + ) + target = Parameter('wanted state (on or off)', datatype=EnumType(on=1, off=0), default=0, - ), - 'target': Override('wanted state (on or off)', - datatype=EnumType(on=1, off=0), default=0, - readonly=False, - ), - 'switch_on_time': Parameter('seconds to wait after activating the switch', + readonly=False, + ) + switch_on_time = Parameter('seconds to wait after activating the switch', + datatype=FloatRange(0, 60), unit='s', + default=10, export=False, + ) + switch_off_time = Parameter('cool-down time in seconds', datatype=FloatRange(0, 60), unit='s', default=10, export=False, - ), - 'switch_off_time': Parameter('cool-down time in seconds', - datatype=FloatRange(0, 60), unit='s', - default=10, export=False, - ), - } + ) - properties = { - 'description' : Property('The description of the Module', StringType(), - default='no description', mandatory=False, extname='description'), - } + description = Property('The description of the Module', StringType(), + default='no description', mandatory=False, extname='description') def read_value(self): # could ask HW @@ -109,30 +106,29 @@ class Switch(Drivable): class MagneticField(Drivable): """a liquid magnet """ - parameters = { - 'value': Override('current field in T', + + value = Parameter('current field in T', + unit='T', datatype=FloatRange(-15, 15), default=0, + ) + target = Parameter('target field in T', unit='T', datatype=FloatRange(-15, 15), default=0, - ), - 'target': Override('target field in T', - unit='T', datatype=FloatRange(-15, 15), default=0, - readonly=False, - ), - 'ramp': Parameter('ramping speed', - unit='T/min', datatype=FloatRange(0, 1), default=0.1, - readonly=False, - ), - 'mode': Parameter('what to do after changing field', - default=1, datatype=EnumType(persistent=1, hold=0), - readonly=False, - ), - 'heatswitch': Parameter('name of heat switch device', - datatype=StringType(), export=False, - ), - } + readonly=False, + ) + ramp = Parameter('ramping speed', + unit='T/min', datatype=FloatRange(0, 1), default=0.1, + readonly=False, + ) + mode = Parameter('what to do after changing field', + default=1, datatype=EnumType(persistent=1, hold=0), + readonly=False, + ) + heatswitch = Parameter('name of heat switch device', + datatype=StringType(), export=False, + ) + Status = Enum(Drivable.Status, PERSIST=PERSIST, PREPARE=301, RAMPING=302, FINISH=303) - overrides = { - 'status' : Override(datatype=TupleOf(EnumType(Status), StringType())), - } + + status = Parameter(datatype=TupleOf(EnumType(Status), StringType())) def initModule(self): self._state = Enum('state', idle=1, switch_on=2, switch_off=3, ramp=4).idle @@ -202,21 +198,20 @@ class MagneticField(Drivable): time.sleep(max(0.01, ts + loopdelay - time.time())) self.log.error(self, 'main thread exited unexpectedly!') - def do_stop(self): + def stop(self): self.write_target(self.read_value()) class CoilTemp(Readable): """a coil temperature """ - parameters = { - 'value': Override('Coil temperatur', - unit='K', datatype=FloatRange(), default=0, - ), - 'sensor': Parameter("Sensor number or calibration id", - datatype=StringType(), readonly=True, - ), - } + + value = Parameter('Coil temperatur', + unit='K', datatype=FloatRange(), default=0, + ) + sensor = Parameter("Sensor number or calibration id", + datatype=StringType(), readonly=True, + ) def read_value(self): return round(2.3 + random.random(), 3) @@ -225,18 +220,17 @@ class CoilTemp(Readable): class SampleTemp(Drivable): """a sample temperature """ - parameters = { - 'value': Override('Sample temperature', - unit='K', datatype=FloatRange(), default=10, - ), - 'sensor': Parameter("Sensor number or calibration id", - datatype=StringType(), readonly=True, - ), - 'ramp': Parameter('moving speed in K/min', - datatype=FloatRange(0, 100), unit='K/min', default=0.1, - readonly=False, - ), - } + + value = Parameter('Sample temperature', + unit='K', datatype=FloatRange(), default=10, + ) + sensor = Parameter("Sensor number or calibration id", + datatype=StringType(), readonly=True, + ) + ramp = Parameter('moving speed in K/min', + datatype=FloatRange(0, 100), unit='K/min', default=0.1, + readonly=False, + ) def initModule(self): _thread = threading.Thread(target=self._thread) @@ -272,20 +266,19 @@ class Label(Readable): of several subdevices. used for demoing connections between modules. """ - parameters = { - 'system': Parameter("Name of the magnet system", - datatype=StringType(), export=False, - ), - 'subdev_mf': Parameter("name of subdevice for magnet status", - datatype=StringType(), export=False, - ), - 'subdev_ts': Parameter("name of subdevice for sample temp", - datatype=StringType(), export=False, - ), - 'value': Override("final value of label string", default='', - datatype=StringType(), - ), - } + + system = Parameter("Name of the magnet system", + datatype=StringType(), export=False, + ) + subdev_mf = Parameter("name of subdevice for magnet status", + datatype=StringType(), export=False, + ) + subdev_ts = Parameter("name of subdevice for sample temp", + datatype=StringType(), export=False, + ) + value = Parameter("final value of label string", default='', + datatype=StringType(), + ) def read_value(self): strings = [self.system] @@ -317,29 +310,25 @@ class Label(Readable): class DatatypesTest(Readable): """for demoing all datatypes """ - parameters = { - 'enum': Parameter('enum', datatype=EnumType(boo=None, faar=None, z=9), - readonly=False, default=1), - 'tupleof': Parameter('tuple of int, float and str', - datatype=TupleOf(IntRange(), FloatRange(), - StringType()), - readonly=False, default=(1, 2.3, 'a')), - 'arrayof': Parameter('array: 2..3 times bool', - datatype=ArrayOf(BoolType(), 2, 3), - readonly=False, default=[1, 0, 1]), - 'intrange': Parameter('intrange', datatype=IntRange(2, 9), - readonly=False, default=4), - 'floatrange': Parameter('floatrange', datatype=FloatRange(-1, 1), - readonly=False, default=0, ), - 'struct': Parameter('struct(a=str, b=int, c=bool)', - datatype=StructOf(a=StringType(), b=IntRange(), - c=BoolType()), - ), - } + + enum = Parameter('enum', datatype=EnumType(boo=None, faar=None, z=9), + readonly=False, default=1) + tupleof = Parameter('tuple of int, float and str', + datatype=TupleOf(IntRange(), FloatRange(), + StringType()), + readonly=False, default=(1, 2.3, 'a')) + arrayof = Parameter('array: 2..3 times bool', + datatype=ArrayOf(BoolType(), 2, 3), + readonly=False, default=[1, 0, 1]) + intrange = Parameter('intrange', datatype=IntRange(2, 9), + readonly=False, default=4) + floatrange = Parameter('floatrange', datatype=FloatRange(-1, 1), + readonly=False, default=0) + struct = Parameter('struct(a=str, b=int, c=bool)', + datatype=StructOf(a=StringType(), b=IntRange(), + c=BoolType())) class ArrayTest(Readable): - parameters = { - "x": Parameter('value', datatype=ArrayOf(FloatRange(), 0, 100000), - default = 100000 * [0]), - } + x = Parameter('value', datatype=ArrayOf(FloatRange(), 0, 100000), + default=100000 * [0]) diff --git a/secop_demo/test.py b/secop_demo/test.py index 31686a5..7966c11 100644 --- a/secop_demo/test.py +++ b/secop_demo/test.py @@ -24,7 +24,7 @@ import random from secop.datatypes import FloatRange, StringType -from secop.modules import Communicator, Drivable, Parameter, Readable, Override +from secop.modules import Communicator, Drivable, Parameter, Readable from secop.params import Command @@ -45,11 +45,10 @@ class Heater(Drivable): class name indicates it to be some heating element, but the implementation may do anything """ - parameters = { - 'maxheaterpower': Parameter('maximum allowed heater power', - datatype=FloatRange(0, 100), unit='W', - ), - } + + maxheaterpower = Parameter('maximum allowed heater power', + datatype=FloatRange(0, 100), unit='W', + ) def read_value(self): return round(100 * random.random(), 1) @@ -64,22 +63,21 @@ class Temp(Drivable): class name indicates it to be some temperature controller, but the implementation may do anything """ - parameters = { - 'sensor': Parameter( - "Sensor number or calibration id", - datatype=StringType( - 8, - 16), - readonly=True, - ), - 'target': Override( - "Target temperature", - default=300.0, - datatype=FloatRange(0), - readonly=False, - unit='K', - ), - } + + sensor = Parameter( + "Sensor number or calibration id", + datatype=StringType( + 8, + 16), + readonly=True, + ) + target = Parameter( + "Target temperature", + default=300.0, + datatype=FloatRange(0), + readonly=False, + unit='K', + ) def read_value(self): return round(100 * random.random(), 1) @@ -90,8 +88,8 @@ class Temp(Drivable): class Lower(Communicator): """Communicator returning a lowercase version of the request""" - command = { - 'communicate': Command('lowercase a string', argument=StringType(), result=StringType(), export='communicate'), - } - def do_communicate(self, request): - return str(request).lower() + + @Command(argument=StringType(), result=StringType(), export='communicate') + def communicate(self, command): + """lowercase a string""" + return str(command).lower() diff --git a/secop_ess/epics.py b/secop_ess/epics.py index c536f0a..515fbdf 100644 --- a/secop_ess/epics.py +++ b/secop_ess/epics.py @@ -58,20 +58,20 @@ except ImportError: class EpicsReadable(Readable): """EpicsDrivable handles a Drivable interfacing to EPICS v4""" # Commmon parameter for all EPICS devices - parameters = { - 'value': Parameter('EPICS generic value', - datatype=FloatRange(), - default=300.0,), - 'epics_version': Parameter("EPICS version used, v3 or v4", - datatype=EnumType(v3=3, v4=4),), - # 'private' parameters: not remotely accessible - 'value_pv': Parameter('EPICS pv_name of value', - datatype=StringType(), - default="unset", export=False), - 'status_pv': Parameter('EPICS pv_name of status', - datatype=StringType(), - default="unset", export=False), - } + + # parameters + value = Parameter('EPICS generic value', + datatype=FloatRange(), + default=300.0,) + epics_version = Parameter("EPICS version used, v3 or v4", + datatype=EnumType(v3=3, v4=4),) + value_pv = Parameter('EPICS pv_name of value', + datatype=StringType(), + default="unset", export=False) + status_pv = Parameter('EPICS pv_name of status', + datatype=StringType(), + default="unset", export=False) + # Generic read and write functions def _read_pv(self, pv_name): @@ -118,21 +118,21 @@ class EpicsReadable(Readable): class EpicsDrivable(Drivable): """EpicsDrivable handles a Drivable interfacing to EPICS v4""" # Commmon parameter for all EPICS devices - parameters = { - 'target': Parameter('EPICS generic target', datatype=FloatRange(), - default=300.0, readonly=False), - 'value': Parameter('EPICS generic value', datatype=FloatRange(), - default=300.0,), - 'epics_version': Parameter("EPICS version used, v3 or v4", - datatype=StringType(),), - # 'private' parameters: not remotely accessible - 'target_pv': Parameter('EPICS pv_name of target', datatype=StringType(), - default="unset", export=False), - 'value_pv': Parameter('EPICS pv_name of value', datatype=StringType(), - default="unset", export=False), - 'status_pv': Parameter('EPICS pv_name of status', datatype=StringType(), - default="unset", export=False), - } + + # parameters + target = Parameter('EPICS generic target', datatype=FloatRange(), + default=300.0, readonly=False) + value = Parameter('EPICS generic value', datatype=FloatRange(), + default=300.0,) + epics_version = Parameter("EPICS version used, v3 or v4", + datatype=StringType(),) + target_pv = Parameter('EPICS pv_name of target', datatype=StringType(), + default="unset", export=False) + value_pv = Parameter('EPICS pv_name of value', datatype=StringType(), + default="unset", export=False) + status_pv = Parameter('EPICS pv_name of status', datatype=StringType(), + default="unset", export=False) + # Generic read and write functions def _read_pv(self, pv_name): @@ -191,17 +191,16 @@ class EpicsDrivable(Drivable): class EpicsTempCtrl(EpicsDrivable): - parameters = { - # TODO: restrict possible values with oneof datatype - 'heaterrange': Parameter('Heater range', datatype=StringType(), - default='Off', readonly=False,), - 'tolerance': Parameter('allowed deviation between value and target', - datatype=FloatRange(1e-6, 1e6), default=0.1, - readonly=False,), - # 'private' parameters: not remotely accessible - 'heaterrange_pv': Parameter('EPICS pv_name of heater range', - datatype=StringType(), default="unset", export=False,), - } + + # parameters + heaterrange = Parameter('Heater range', datatype=StringType(), + default='Off', readonly=False,) + tolerance = Parameter('allowed deviation between value and target', + datatype=FloatRange(1e-6, 1e6), default=0.1, + readonly=False,) + heaterrange_pv = Parameter('EPICS pv_name of heater range', + datatype=StringType(), default="unset", export=False,) + def read_target(self): return self._read_pv(self.target_pv) diff --git a/secop_mlz/amagnet.py b/secop_mlz/amagnet.py index 94b509f..17c3d29 100644 --- a/secop_mlz/amagnet.py +++ b/secop_mlz/amagnet.py @@ -49,36 +49,37 @@ class GarfieldMagnet(SequencerMixin, Drivable): pollerClass = BasicPoller - parameters = { - 'subdev_currentsource': Parameter('(bipolar) Powersupply', datatype=StringType(), readonly=True, export=False), - 'subdev_enable': Parameter('Switch to set for on/off', datatype=StringType(), readonly=True, export=False), - 'subdev_polswitch': Parameter('Switch to set for polarity', datatype=StringType(), readonly=True, export=False), - 'subdev_symmetry': Parameter('Switch to read for symmetry', datatype=StringType(), readonly=True, export=False), - 'userlimits': Parameter('User defined limits of device value', - datatype=TupleOf(FloatRange(unit='$'), FloatRange(unit='$')), - default=(float('-Inf'), float('+Inf')), readonly=False, poll=10), - 'abslimits': Parameter('Absolute limits of device value', - datatype=TupleOf(FloatRange(unit='$'), FloatRange(unit='$')), - default=(-0.5, 0.5), poll=True, - ), - 'precision': Parameter('Precision of the device value (allowed deviation ' - 'of stable values from target)', - datatype=FloatRange(0.001, unit='$'), default=0.001, readonly=False, - ), - 'ramp': Parameter('Target rate of field change per minute', readonly=False, - datatype=FloatRange(unit='$/min'), default=1.0), - 'calibration': Parameter('Coefficients for calibration ' - 'function: [c0, c1, c2, c3, c4] calculates ' - 'B(I) = c0*I + c1*erf(c2*I) + c3*atan(c4*I)' - ' in T', poll=1, - datatype=ArrayOf(FloatRange(), 5, 5), - default=(1.0, 0.0, 0.0, 0.0, 0.0)), - 'calibrationtable': Parameter('Map of Coefficients for calibration per symmetry setting', - datatype=StructOf(symmetric=ArrayOf(FloatRange(), 5, 5), - short=ArrayOf( - FloatRange(), 5, 5), - asymmetric=ArrayOf(FloatRange(), 5, 5)), export=False), - } + + # parameters + subdev_currentsource = Parameter('(bipolar) Powersupply', datatype=StringType(), readonly=True, export=False) + subdev_enable = Parameter('Switch to set for on/off', datatype=StringType(), readonly=True, export=False) + subdev_polswitch = Parameter('Switch to set for polarity', datatype=StringType(), readonly=True, export=False) + subdev_symmetry = Parameter('Switch to read for symmetry', datatype=StringType(), readonly=True, export=False) + userlimits = Parameter('User defined limits of device value', + datatype=TupleOf(FloatRange(unit='$'), FloatRange(unit='$')), + default=(float('-Inf'), float('+Inf')), readonly=False, poll=10) + abslimits = Parameter('Absolute limits of device value', + datatype=TupleOf(FloatRange(unit='$'), FloatRange(unit='$')), + default=(-0.5, 0.5), poll=True, + ) + precision = Parameter('Precision of the device value (allowed deviation ' + 'of stable values from target)', + datatype=FloatRange(0.001, unit='$'), default=0.001, readonly=False, + ) + ramp = Parameter('Target rate of field change per minute', readonly=False, + datatype=FloatRange(unit='$/min'), default=1.0) + calibration = Parameter('Coefficients for calibration ' + 'function: [c0, c1, c2, c3, c4] calculates ' + 'B(I) = c0*I + c1*erf(c2*I) + c3*atan(c4*I)' + ' in T', poll=1, + datatype=ArrayOf(FloatRange(), 5, 5), + default=(1.0, 0.0, 0.0, 0.0, 0.0)) + calibrationtable = Parameter('Map of Coefficients for calibration per symmetry setting', + datatype=StructOf(symmetric=ArrayOf(FloatRange(), 5, 5), + short=ArrayOf( + FloatRange(), 5, 5), + asymmetric=ArrayOf(FloatRange(), 5, 5)), export=False) + def _current2field(self, current, *coefficients): """Return field in T for given current in A. @@ -307,7 +308,7 @@ class GarfieldMagnet(SequencerMixin, Drivable): return self._currentsource.read_status()[0] == 'BUSY' if self._currentsource.status[0] != 'BUSY': if self._enable.status[0] == 'ERROR': - self._enable.do_reset() + self._enable.reset() self._enable.read_status() self._enable.write_target('On') self._enable._hw_wait() diff --git a/secop_mlz/entangle.py b/secop_mlz/entangle.py index 6ed1bb1..95f3c6f 100644 --- a/secop_mlz/entangle.py +++ b/secop_mlz/entangle.py @@ -41,7 +41,7 @@ from secop.errors import CommunicationFailedError, \ ConfigError, HardwareError, ProgrammingError from secop.lib import lazy_property from secop.modules import Command, Drivable, \ - Module, Override, Parameter, Readable, BasicPoller + Module, Parameter, Readable, BasicPoller ##### @@ -160,24 +160,18 @@ class PyTangoDevice(Module): pollerClass = BasicPoller - parameters = { - 'comtries': Parameter('Maximum retries for communication', - datatype=IntRange(1, 100), default=3, readonly=False, - group='communication'), - 'comdelay': Parameter('Delay between retries', datatype=FloatRange(0), - unit='s', default=0.1, readonly=False, - group='communication'), - - 'tangodevice': Parameter('Tango device name', - datatype=StringType(), readonly=True, - # export=True, # for testing only - export=False, - ), - } - - commands = { - 'reset': Command('Tango reset command', argument=None, result=None), - } + # parameters + comtries = Parameter('Maximum retries for communication', + datatype=IntRange(1, 100), default=3, readonly=False, + group='communication') + comdelay = Parameter('Delay between retries', datatype=FloatRange(0), + unit='s', default=0.1, readonly=False, + group='communication') + tangodevice = Parameter('Tango device name', + datatype=StringType(), readonly=True, + # export=True, # for testing only + export=False, + ) tango_status_mapping = { PyTango.DevState.ON: Drivable.Status.IDLE, @@ -372,7 +366,9 @@ class PyTangoDevice(Module): return (myState, tangoStatus) - def do_reset(self): + @Command(argument=None, result=None) + def reset(self): + """Tango reset command""" self._dev.Reset() @@ -405,13 +401,9 @@ class Sensor(AnalogInput): # note: we don't transport the formula to secop.... # we support the adjust method - commands = { - 'setposition': Command('Set the position to the given value.', - argument=FloatRange(), result=None, - ), - } - - def do_setposition(self, value): + @Command(argument=FloatRange(), result=None) + def setposition(self, value): + """Set the position to the given value.""" self._dev.Adjust(value) @@ -427,29 +419,29 @@ class AnalogOutput(PyTangoDevice, Drivable): controllers, ... """ - parameters = { - 'userlimits': Parameter('User defined limits of device value', - datatype=LimitsType(FloatRange(unit='$')), - default=(float('-Inf'), float('+Inf')), - readonly=False, poll=10, - ), - 'abslimits': Parameter('Absolute limits of device value', + # parameters + userlimits = Parameter('User defined limits of device value', datatype=LimitsType(FloatRange(unit='$')), - ), - 'precision': Parameter('Precision of the device value (allowed deviation ' - 'of stable values from target)', - datatype=FloatRange(1e-38, unit='$'), - readonly=False, group='stability', - ), - 'window': Parameter('Time window for checking stabilization if > 0', - default=60.0, readonly=False, - datatype=FloatRange(0, 900, unit='s'), group='stability', - ), - 'timeout': Parameter('Timeout for waiting for a stable value (if > 0)', - default=60.0, readonly=False, - datatype=FloatRange(0, 900, unit='s'), group='stability', - ), - } + default=(float('-Inf'), float('+Inf')), + readonly=False, poll=10, + ) + abslimits = Parameter('Absolute limits of device value', + datatype=LimitsType(FloatRange(unit='$')), + ) + precision = Parameter('Precision of the device value (allowed deviation ' + 'of stable values from target)', + datatype=FloatRange(1e-38, unit='$'), + readonly=False, group='stability', + ) + window = Parameter('Time window for checking stabilization if > 0', + default=60.0, readonly=False, + datatype=FloatRange(0, 900, unit='s'), group='stability', + ) + timeout = Parameter('Timeout for waiting for a stable value (if > 0)', + default=60.0, readonly=False, + datatype=FloatRange(0, 900, unit='s'), group='stability', + ) + _history = () _timeout = None _moving = False @@ -566,7 +558,7 @@ class AnalogOutput(PyTangoDevice, Drivable): if self.status[0] == self.Status.BUSY: # changing target value during movement is not allowed by the # Tango base class state machine. If we are moving, stop first. - self.do_stop() + self.stop() self._hw_wait() self._dev.value = value # set meaningful timeout @@ -587,7 +579,7 @@ class AnalogOutput(PyTangoDevice, Drivable): while super(AnalogOutput, self).read_status()[0] == self.Status.BUSY: sleep(0.3) - def do_stop(self): + def stop(self): self._dev.Stop() @@ -601,21 +593,14 @@ class Actuator(AnalogOutput): """ # for secop: support the speed and ramp parameters - parameters = { - 'speed': Parameter('The speed of changing the value', - readonly=False, datatype=FloatRange(0, unit='$/s'), - ), - 'ramp': Parameter('The speed of changing the value', - readonly=False, datatype=FloatRange(0, unit='$/s'), - poll=30, - ), - } - - commands = { - 'setposition': Command('Set the position to the given value.', - argument=FloatRange(), result=None, - ), - } + # parameters + speed = Parameter('The speed of changing the value', + readonly=False, datatype=FloatRange(0, unit='$/s'), + ) + ramp = Parameter('The speed of changing the value', + readonly=False, datatype=FloatRange(0, unit='$/s'), + poll=30, + ) def read_speed(self): return self._dev.speed @@ -630,7 +615,9 @@ class Actuator(AnalogOutput): self.write_speed(value / 60.) return self.read_speed() * 60 - def do_setposition(self, value=FloatRange()): + @Command(FloatRange(), result=None) + def setposition(self, value=FloatRange()): + """Set the position to the given value.""" self._dev.Adjust(value) @@ -641,21 +628,16 @@ class Motor(Actuator): It has the ability to move a real object from one place to another place. """ - parameters = { - 'refpos': Parameter('Reference position', - datatype=FloatRange(unit='$'), - ), - 'accel': Parameter('Acceleration', - datatype=FloatRange(unit='$/s^2'), readonly=False, - ), - 'decel': Parameter('Deceleration', - datatype=FloatRange(unit='$/s^2'), readonly=False, - ), - } - - commands = { - 'reference': Command('Do a reference run', argument=None, result=None), - } + # parameters + refpos = Parameter('Reference position', + datatype=FloatRange(unit='$'), + ) + accel = Parameter('Acceleration', + datatype=FloatRange(unit='$/s^2'), readonly=False, + ) + decel = Parameter('Deceleration', + datatype=FloatRange(unit='$/s^2'), readonly=False, + ) def read_refpos(self): return float(self._getProperty('refpos')) @@ -672,7 +654,9 @@ class Motor(Actuator): def write_decel(self, value): self._dev.decel = value - def do_reference(self): + @Command() + def reference(self): + """Do a reference run""" self._dev.Reference() return self.read_value() @@ -681,32 +665,29 @@ class TemperatureController(Actuator): """A temperature control loop device. """ - parameters = { - 'p': Parameter('Proportional control Parameter', datatype=FloatRange(), - readonly=False, group='pid', - ), - 'i': Parameter('Integral control Parameter', datatype=FloatRange(), - readonly=False, group='pid', - ), - 'd': Parameter('Derivative control Parameter', datatype=FloatRange(), - readonly=False, group='pid', - ), - 'pid': Parameter('pid control Parameters', - datatype=TupleOf(FloatRange(), FloatRange(), FloatRange()), - readonly=False, group='pid', poll=30, - ), - 'setpoint': Parameter('Current setpoint', datatype=FloatRange(unit='$'), poll=1, - ), - 'heateroutput': Parameter('Heater output', datatype=FloatRange(), poll=1, - ), - } + # parameters + # pylint: disable=invalid-name + p = Parameter('Proportional control Parameter', datatype=FloatRange(), + readonly=False, group='pid', + ) + i = Parameter('Integral control Parameter', datatype=FloatRange(), + readonly=False, group='pid', + ) + d = Parameter('Derivative control Parameter', datatype=FloatRange(), + readonly=False, group='pid', + ) + pid = Parameter('pid control Parameters', + datatype=TupleOf(FloatRange(), FloatRange(), FloatRange()), + readonly=False, group='pid', poll=30, + ) + setpoint = Parameter('Current setpoint', datatype=FloatRange(unit='$'), poll=1, + ) + heateroutput = Parameter('Heater output', datatype=FloatRange(), poll=1, + ) - overrides = { - # We want this to be freely user-settable, and not produce a warning - # on startup, so select a usually sensible default. - 'precision': Override(default=0.1), - 'ramp': Override(description='Temperature ramp'), - } + # overrides + precision = Parameter(default=0.1) + ramp = Parameter(description='Temperature ramp') def read_ramp(self): return self._dev.ramp @@ -755,15 +736,14 @@ class PowerSupply(Actuator): """A power supply (voltage and current) device. """ - parameters = { - 'voltage': Parameter('Actual voltage', - datatype=FloatRange(unit='V'), poll=-5), - 'current': Parameter('Actual current', - datatype=FloatRange(unit='A'), poll=-5), - } - overrides = { - 'ramp': Override(description='Current/voltage ramp'), - } + # parameters + voltage = Parameter('Actual voltage', + datatype=FloatRange(unit='V'), poll=-5) + current = Parameter('Actual current', + datatype=FloatRange(unit='A'), poll=-5) + + # overrides + ramp = Parameter(description='Current/voltage ramp') def read_ramp(self): return self._dev.ramp @@ -782,9 +762,8 @@ class DigitalInput(PyTangoDevice, Readable): """A device reading a bitfield. """ - overrides = { - 'value': Override(datatype=IntRange()), - } + # overrides + value = Parameter(datatype=IntRange()) def read_value(self): return self._dev.value @@ -794,10 +773,9 @@ class NamedDigitalInput(DigitalInput): """A DigitalInput with numeric values mapped to names. """ - parameters = { - 'mapping': Parameter('A dictionary mapping state names to integers', - datatype=StringType(), export=False), # XXX:!!! - } + # parameters + mapping = Parameter('A dictionary mapping state names to integers', + datatype=StringType(), export=False) # XXX:!!! def initModule(self): super(NamedDigitalInput, self).initModule() @@ -821,12 +799,11 @@ class PartialDigitalInput(NamedDigitalInput): bit width accessed. """ - parameters = { - 'startbit': Parameter('Number of the first bit', - datatype=IntRange(0), default=0), - 'bitwidth': Parameter('Number of bits', - datatype=IntRange(0), default=1), - } + # parameters + startbit = Parameter('Number of the first bit', + datatype=IntRange(0), default=0) + bitwidth = Parameter('Number of bits', + datatype=IntRange(0), default=1) def initModule(self): super(PartialDigitalInput, self).initModule() @@ -844,10 +821,9 @@ class DigitalOutput(PyTangoDevice, Drivable): bitfield. """ - overrides = { - 'value': Override(datatype=IntRange()), - 'target': Override(datatype=IntRange()), - } + # overrides + value = Parameter(datatype=IntRange()) + target = Parameter(datatype=IntRange()) def read_value(self): return self._dev.value # mapping is done by datatype upon export() @@ -865,10 +841,9 @@ class NamedDigitalOutput(DigitalOutput): """A DigitalOutput with numeric values mapped to names. """ - parameters = { - 'mapping': Parameter('A dictionary mapping state names to integers', - datatype=StringType(), export=False), - } + # parameters + mapping = Parameter('A dictionary mapping state names to integers', + datatype=StringType(), export=False) def initModule(self): super(NamedDigitalOutput, self).initModule() @@ -894,12 +869,11 @@ class PartialDigitalOutput(NamedDigitalOutput): bit width accessed. """ - parameters = { - 'startbit': Parameter('Number of the first bit', - datatype=IntRange(0), default=0), - 'bitwidth': Parameter('Number of bits', - datatype=IntRange(0), default=1), - } + # parameters + startbit = Parameter('Number of the first bit', + datatype=IntRange(0), default=0) + bitwidth = Parameter('Number of bits', + datatype=IntRange(0), default=1) def initModule(self): super(PartialDigitalOutput, self).initModule() @@ -925,17 +899,16 @@ class StringIO(PyTangoDevice, Module): receives strings. """ - parameters = { - 'bustimeout': Parameter('Communication timeout', - datatype=FloatRange(unit='s'), readonly=False, - group='communication'), - 'endofline': Parameter('End of line', - datatype=StringType(), readonly=False, - group='communication'), - 'startofline': Parameter('Start of line', - datatype=StringType(), readonly=False, - group='communication'), - } + # parameters + bustimeout = Parameter('Communication timeout', + datatype=FloatRange(unit='s'), readonly=False, + group='communication') + endofline = Parameter('End of line', + datatype=StringType(), readonly=False, + group='communication') + startofline = Parameter('Start of line', + datatype=StringType(), readonly=False, + group='communication') def read_bustimeout(self): return self._dev.communicationTimeout @@ -955,53 +928,48 @@ class StringIO(PyTangoDevice, Module): def write_startofline(self, value): self._dev.startOfLine = value - commands = { - 'communicate': Command('Send a string and return the reply', - argument=StringType(), - result=StringType()), - 'flush': Command('Flush output buffer', - argument=None, result=None), - 'read': Command('read some characters from input buffer', - argument=IntRange(0), result=StringType()), - 'write': Command('write some chars to output', - argument=StringType(), result=None), - 'readLine': Command('Read sol - a whole line - eol', - argument=None, result=StringType()), - 'writeLine': Command('write sol + a whole line + eol', - argument=StringType(), result=None), - 'availableChars': Command('return number of chars in input buffer', - argument=None, result=IntRange(0)), - 'availableLines': Command('return number of lines in input buffer', - argument=None, result=IntRange(0)), - 'multiCommunicate': Command('perform a sequence of communications', - argument=ArrayOf( - TupleOf(StringType(), IntRange()), 100), - result=ArrayOf(StringType(), 100)), - } - - def do_communicate(self, value=StringType()): + @Command(argument=StringType(), result=StringType()) + def communicate(self, value=StringType()): + """Send a string and return the reply""" return self._dev.Communicate(value) - def do_flush(self): + @Command(argument=None, result=None) + def flush(self): + """Flush output buffer""" self._dev.Flush() - def do_read(self, value): + @Command(argument=IntRange(0), result=StringType()) + def read(self, value): + """read some characters from input buffer""" return self._dev.Read(value) - def do_write(self, value): + @Command(argument=StringType(), result=None) + def write(self, value): + """write some chars to output""" return self._dev.Write(value) - def do_readLine(self): + @Command(argument=None, result=StringType()) + def readLine(self): + """Read sol - a whole line - eol""" return self._dev.ReadLine() - def do_writeLine(self, value): + @Command(argument=StringType(), result=None) + def writeLine(self, value): + """write sol + a whole line + eol""" return self._dev.WriteLine(value) - def do_multiCommunicate(self, value): + @Command(argument=ArrayOf(TupleOf(StringType(), IntRange()), 100), + result=ArrayOf(StringType(), 100)) + def multiCommunicate(self, value): + """perform a sequence of communications""" return self._dev.MultiCommunicate(value) - def do_availableChars(self): + @Command(argument=None, result=IntRange(0)) + def availableChars(self): + """return number of chars in input buffer""" return self._dev.availableChars - def do_availableLines(self): + @Command(argument=None, result=IntRange(0)) + def availableLines(self): + """return number of lines in input buffer""" return self._dev.availableLines diff --git a/secop_psi/ah2700.py b/secop_psi/ah2700.py index 689b1e1..1b48175 100644 --- a/secop_psi/ah2700.py +++ b/secop_psi/ah2700.py @@ -20,7 +20,7 @@ # ***************************************************************************** """Andeen Hagerling capacitance bridge""" -from secop.core import Readable, Parameter, Override, FloatRange, HasIodev, StringIO, Done +from secop.core import Readable, Parameter, FloatRange, HasIodev, StringIO, Done class Ah2700IO(StringIO): @@ -29,12 +29,12 @@ class Ah2700IO(StringIO): class Capacitance(HasIodev, Readable): - parameters = { - 'value': Override('capacitance', FloatRange(unit='pF'), poll=True), - 'freq': Parameter('frequency', FloatRange(unit='Hz'), readonly=False, default=0), - 'voltage': Parameter('voltage', FloatRange(unit='V'), readonly=False, default=0), - 'loss': Parameter('loss', FloatRange(unit='deg'), default=0), - } + + value = Parameter('capacitance', FloatRange(unit='pF'), poll=True) + freq = Parameter('frequency', FloatRange(unit='Hz'), readonly=False, default=0) + voltage = Parameter('voltage', FloatRange(unit='V'), readonly=False, default=0) + loss = Parameter('loss', FloatRange(unit='deg'), default=0) + iodevClass = Ah2700IO def parse_reply(self, reply): diff --git a/secop_psi/k2601b.py b/secop_psi/k2601b.py index d7f818b..e8e8001 100644 --- a/secop_psi/k2601b.py +++ b/secop_psi/k2601b.py @@ -22,7 +22,7 @@ not tested yet""" -from secop.core import Writable, Module, Parameter, Override, Attached,\ +from secop.core import Writable, Module, Parameter, Attached,\ BoolType, FloatRange, EnumType, HasIodev, StringIO @@ -42,13 +42,13 @@ SOURCECMDS = { class SourceMeter(HasIodev, Module): - parameters = { - 'resistivity': Parameter('readback resistivity', FloatRange(unit='Ohm'), poll=True), - 'power': Parameter('readback power', FloatRange(unit='W'), poll=True), - 'mode': Parameter('measurement mode', EnumType(off=0, current=1, voltage=2), - readonly=False, default=0), - 'active': Parameter('output enable', BoolType(), readonly=False, poll=True), - } + + resistivity = Parameter('readback resistivity', FloatRange(unit='Ohm'), poll=True) + power = Parameter('readback power', FloatRange(unit='W'), poll=True) + mode = Parameter('measurement mode', EnumType(off=0, current=1, voltage=2), + readonly=False, default=0) + active = Parameter('output enable', BoolType(), readonly=False, poll=True) + iodevClass = K2601bIO def read_resistivity(self): @@ -74,15 +74,12 @@ class SourceMeter(HasIodev, Module): class Current(HasIodev, Writable): - properties = { - 'sourcemeter': Attached(), - } - parameters = { - 'value': Override('measured current', FloatRange(unit='A'), poll=True), - 'target': Override('set current', FloatRange(unit='A'), poll=True), - 'active': Parameter('current is controlled', BoolType(), default=False), # polled from Current/Voltage - 'limit': Parameter('current limit', FloatRange(0, 2.0, unit='A'), default=2, poll=True), - } + sourcemeter = Attached() + + value = Parameter('measured current', FloatRange(unit='A'), poll=True) + target = Parameter('set current', FloatRange(unit='A'), poll=True) + active = Parameter('current is controlled', BoolType(), default=False) # polled from Current/Voltage + limit = Parameter('current limit', FloatRange(0, 2.0, unit='A'), default=2, poll=True) def read_value(self): return self.sendRecv('print(smua.measure.i())') @@ -120,15 +117,12 @@ class Current(HasIodev, Writable): class Voltage(HasIodev, Writable): - properties = { - 'sourcemeter': Attached(), - } - parameters = { - 'value': Override('measured voltage', FloatRange(unit='V'), poll=True), - 'target': Override('set voltage', FloatRange(unit='V'), poll=True), - 'active': Parameter('voltage is controlled', BoolType(), poll=True), - 'limit': Parameter('current limit', FloatRange(0, 2.0, unit='V'), default=2, poll=True), - } + sourcemeter = Attached() + + value = Parameter('measured voltage', FloatRange(unit='V'), poll=True) + target = Parameter('set voltage', FloatRange(unit='V'), poll=True) + active = Parameter('voltage is controlled', BoolType(), poll=True) + limit = Parameter('current limit', FloatRange(0, 2.0, unit='V'), default=2, poll=True) def read_value(self): return self.sendRecv('print(smua.measure.v())') @@ -159,7 +153,7 @@ class Voltage(HasIodev, Writable): def write_active(self, value): if self._sourcemeter.mode != 2: if value: - self._sourcemeter.write_mode(2) # switch to voltage + self._sourcemeter.write_mode(2) # switch to voltage else: return 0 return self._sourcemeter.write_active(value) diff --git a/secop_psi/ls370res.py b/secop_psi/ls370res.py index c453a8d..830e061 100644 --- a/secop_psi/ls370res.py +++ b/secop_psi/ls370res.py @@ -22,8 +22,7 @@ import time -from secop.modules import Readable, Drivable, Parameter, Override, Property, Attached -from secop.metaclass import Done +from secop.modules import Readable, Drivable, Parameter, Property, Attached, Done from secop.datatypes import FloatRange, IntRange, EnumType, BoolType from secop.stringio import HasIodev from secop.poller import Poller, REGULAR @@ -59,13 +58,11 @@ class StringIO(secop.stringio.StringIO): class Main(HasIodev, Drivable): - parameters = { - 'value': Override('the current channel', poll=REGULAR, datatype=IntRange(0, 17)), - 'target': Override('channel to select', datatype=IntRange(0, 17)), - 'autoscan': - Parameter('whether to scan automatically', datatype=BoolType(), readonly=False, default=False), - 'pollinterval': Override('sleeptime between polls', default=1), - } + + value = Parameter('the current channel', poll=REGULAR, datatype=IntRange(0, 17)) + target = Parameter('channel to select', datatype=IntRange(0, 17)) + autoscan = Parameter('whether to scan automatically', datatype=BoolType(), readonly=False, default=False) + pollinterval = Parameter('sleeptime between polls', default=1) pollerClass = Poller iodevClass = StringIO @@ -142,40 +139,23 @@ class ResChannel(HasIodev, Readable): _main = None # main module _last_range_change = 0 # time of last range change - properties = { - 'channel': - Property('the Lakeshore channel', datatype=IntRange(1, 16), export=False), - 'main': - Attached() - } + channel = Property('the Lakeshore channel', datatype=IntRange(1, 16), export=False) + main = Attached() - parameters = { - 'value': - Override(datatype=FloatRange(unit='Ohm')), - 'pollinterval': - Override(visibility=3), - 'range': - Parameter('reading range', readonly=False, - datatype=EnumType(**RES_RANGE), handler=rdgrng), - 'minrange': - Parameter('minimum range for software autorange', readonly=False, default=1, - datatype=EnumType(**RES_RANGE)), - 'autorange': - Parameter('autorange', datatype=EnumType(off=0, hard=1, soft=2), - readonly=False, handler=rdgrng, default=2), - 'iexc': - Parameter('current excitation', datatype=EnumType(off=0, **CUR_RANGE), readonly=False, handler=rdgrng), - 'vexc': - Parameter('voltage excitation', datatype=EnumType(off=0, **VOLT_RANGE), readonly=False, handler=rdgrng), - 'enabled': - Parameter('is this channel enabled?', datatype=BoolType(), readonly=False, handler=inset), - 'pause': - Parameter('pause after channel change', datatype=FloatRange(3, 60), readonly=False, handler=inset), - 'dwell': - Parameter('dwell time with autoscan', datatype=FloatRange(1, 200), readonly=False, handler=inset), - 'filter': - Parameter('filter time', datatype=FloatRange(1, 200), readonly=False, handler=filterhdl), - } + value = Parameter(datatype=FloatRange(unit='Ohm')) + pollinterval = Parameter(visibility=3) + range = Parameter('reading range', readonly=False, + datatype=EnumType(**RES_RANGE), handler=rdgrng) + minrange = Parameter('minimum range for software autorange', readonly=False, default=1, + datatype=EnumType(**RES_RANGE)) + autorange = Parameter('autorange', datatype=EnumType(off=0, hard=1, soft=2), + readonly=False, handler=rdgrng, default=2) + iexc = Parameter('current excitation', datatype=EnumType(off=0, **CUR_RANGE), readonly=False, handler=rdgrng) + vexc = Parameter('voltage excitation', datatype=EnumType(off=0, **VOLT_RANGE), readonly=False, handler=rdgrng) + enabled = Parameter('is this channel enabled?', datatype=BoolType(), readonly=False, handler=inset) + pause = Parameter('pause after channel change', datatype=FloatRange(3, 60), readonly=False, handler=inset) + dwell = Parameter('dwell time with autoscan', datatype=FloatRange(1, 200), readonly=False, handler=inset) + filter = Parameter('filter time', datatype=FloatRange(1, 200), readonly=False, handler=filterhdl) def initModule(self): self._main = self.DISPATCHER.get_module(self.main) diff --git a/secop_psi/ls370sim.py b/secop_psi/ls370sim.py index c55e298..c141d79 100644 --- a/secop_psi/ls370sim.py +++ b/secop_psi/ls370sim.py @@ -41,7 +41,7 @@ class Ls370Sim(Communicator): self._data[fmt % chan] = v # mkthread(self.run) - def do_communicate(self, command): + def communicate(self, command): # simulation part, time independent for channel in range(1,17): _, _, _, _, excoff = self._data['RDGRNG?%d' % channel].split(',') diff --git a/secop_psi/ppms.py b/secop_psi/ppms.py index 76e4af2..5e6545a 100644 --- a/secop_psi/ppms.py +++ b/secop_psi/ppms.py @@ -34,8 +34,8 @@ Polling of value and status is done commonly for all modules. For each registere import time import threading -from secop.modules import Module, Readable, Drivable, Parameter, Override,\ - Communicator, Property, Attached +from secop.modules import Readable, Drivable, Parameter,\ + Communicator, Property, Attached, HasAccessibles, Done from secop.datatypes import EnumType, FloatRange, IntRange, StringType,\ BoolType, StatusType from secop.lib.enum import Enum @@ -44,7 +44,6 @@ from secop.errors import HardwareError from secop.poller import Poller import secop.iohandler from secop.stringio import HasIodev -from secop.metaclass import Done try: import secop_psi.ppmswindows as ppmshw @@ -73,19 +72,14 @@ class IOHandler(secop.iohandler.IOHandler): class Main(Communicator): """ppms communicator module""" - parameters = { - 'pollinterval': Parameter('poll interval', readonly=False, - datatype=FloatRange(), default=2), - 'communicate': Override('GBIP command'), - 'data': Parameter('internal', poll=True, export=True, # export for test only - default="", readonly=True, datatype=StringType()), - } - properties = { - 'class_id': Property('Quantum Design class id', export=False, - datatype=StringType()), - } + pollinterval = Parameter('poll interval', FloatRange(), readonly=False, default=2) + data = Parameter('internal', StringType(), poll=True, export=True, # export for test only + default="", readonly=True) - _channel_names = ['packed_status', 'temp', 'field', 'position', 'r1', 'i1', 'r2', 'i2', + class_id = Property('Quantum Design class id', StringType(), export=False) + + _channel_names = [ + 'packed_status', 'temp', 'field', 'position', 'r1', 'i1', 'r2', 'i2', 'r3', 'i3', 'r4', 'i4', 'v1', 'v2', 'digital', 'cur1', 'pow1', 'cur2', 'pow2', 'p', 'u20', 'u21', 'u22', 'ts', 'u24', 'u25', 'u26', 'u27', 'u28', 'u29'] assert len(_channel_names) == 30 @@ -102,7 +96,8 @@ class Main(Communicator): def register(self, other): self.modules[other.channel] = other - def do_communicate(self, command): + def communicate(self, command): + """GPIB command""" with self.lock: reply = self._ppms_device.send(command) self.log.debug("%s|%s", command, reply) @@ -114,7 +109,7 @@ class Main(Communicator): if channel.enabled: mask |= 1 << self._channel_to_index.get(channelname, 0) # send, read and convert to floats and ints - data = self.do_communicate('GETDAT? %d' % mask) + data = self.communicate('GETDAT? %d' % mask) reply = data.split(',') mask = int(reply.pop(0)) reply.pop(0) # pop timestamp @@ -133,11 +128,9 @@ class Main(Communicator): return data # return data as string -class PpmsMixin(HasIodev, Module): +class PpmsMixin(HasIodev, HasAccessibles): """common methods for ppms modules""" - properties = { - 'iodev': Attached(), - } + iodev = Attached() pollerClass = Poller enabled = True # default, if no parameter enable is defined @@ -177,28 +170,21 @@ class PpmsMixin(HasIodev, Module): class Channel(PpmsMixin, Readable): """channel base class""" - parameters = { - 'value': - Override('main value of channels', poll=True), - 'enabled': - Parameter('is this channel used?', readonly=False, poll=False, - datatype=BoolType(), default=False), - 'pollinterval': - Override(visibility=3), - } - properties = { - 'channel': - Property('channel name', - datatype=StringType(), export=False, default=''), - 'no': - Property('channel number', - datatype=IntRange(1, 4), export=False), - } + + value = Parameter('main value of channels', poll=True) + enabled = Parameter('is this channel used?', readonly=False, poll=False, + datatype=BoolType(), default=False) + pollinterval = Parameter(visibility=3) + + channel = Property('channel name', + datatype=StringType(), export=False, default='') + no = Property('channel number', + datatype=IntRange(1, 4), export=False) def earlyInit(self): Readable.earlyInit(self) if not self.channel: - self.properties['channel'] = self.name + self.channel = self.name def get_settings(self, pname): return '' @@ -207,19 +193,12 @@ class Channel(PpmsMixin, Readable): class UserChannel(Channel): """user channel""" - parameters = { - 'pollinterval': - Override(visibility=3), - } - properties = { - 'no': - Property('channel number', - datatype=IntRange(0, 0), export=False, default=0), - 'linkenable': - Property('name of linked channel for enabling', - datatype=StringType(), export=False, default=''), + pollinterval = Parameter(visibility=3) - } + no = Property('channel number', + datatype=IntRange(0, 0), export=False, default=0) + linkenable = Property('name of linked channel for enabling', + datatype=StringType(), export=False, default='') def write_enabled(self, enabled): other = self._iodev.modules.get(self.linkenable, None) @@ -233,16 +212,11 @@ class DriverChannel(Channel): drvout = IOHandler('drvout', 'DRVOUT? %(no)d', '%d,%g,%g') - parameters = { - 'current': - Parameter('driver current', readonly=False, handler=drvout, - datatype=FloatRange(0., 5000., unit='uA')), - 'powerlimit': - Parameter('power limit', readonly=False, handler=drvout, - datatype=FloatRange(0., 1000., unit='uW')), - 'pollinterval': - Override(visibility=3), - } + current = Parameter('driver current', readonly=False, handler=drvout, + datatype=FloatRange(0., 5000., unit='uA')) + powerlimit = Parameter('power limit', readonly=False, handler=drvout, + datatype=FloatRange(0., 1000., unit='uW')) + pollinterval = Parameter(visibility=3) def analyze_drvout(self, no, current, powerlimit): if self.no != no: @@ -260,27 +234,19 @@ class BridgeChannel(Channel): bridge = IOHandler('bridge', 'BRIDGE? %(no)d', '%d,%g,%g,%d,%d,%g') # pylint: disable=invalid-name ReadingMode = Enum('ReadingMode', standard=0, fast=1, highres=2) - parameters = { - 'enabled': - Override(handler=bridge), - 'excitation': - Parameter('excitation current', readonly=False, handler=bridge, - datatype=FloatRange(0.01, 5000., unit='uA')), - 'powerlimit': - Parameter('power limit', readonly=False, handler=bridge, - datatype=FloatRange(0.001, 1000., unit='uW')), - 'dcflag': - Parameter('True when excitation is DC (else AC)', readonly=False, handler=bridge, - datatype=BoolType()), - 'readingmode': - Parameter('reading mode', readonly=False, handler=bridge, - datatype=EnumType(ReadingMode)), - 'voltagelimit': - Parameter('voltage limit', readonly=False, handler=bridge, - datatype=FloatRange(0.0001, 100., unit='mV')), - 'pollinterval': - Override(visibility=3), - } + + enabled = Parameter(handler=bridge) + excitation = Parameter('excitation current', readonly=False, handler=bridge, + datatype=FloatRange(0.01, 5000., unit='uA')) + powerlimit = Parameter('power limit', readonly=False, handler=bridge, + datatype=FloatRange(0.001, 1000., unit='uW')) + dcflag = Parameter('True when excitation is DC (else AC)', readonly=False, handler=bridge, + datatype=BoolType()) + readingmode = Parameter('reading mode', readonly=False, handler=bridge, + datatype=EnumType(ReadingMode)) + voltagelimit = Parameter('voltage limit', readonly=False, handler=bridge, + datatype=FloatRange(0.0001, 100., unit='mV')) + pollinterval = Parameter(visibility=3) def analyze_bridge(self, no, excitation, powerlimit, dcflag, readingmode, voltagelimit): if self.no != no: @@ -306,12 +272,9 @@ class Level(PpmsMixin, Readable): level = IOHandler('level', 'LEVEL?', '%g,%d') - parameters = { - 'value': Override(datatype=FloatRange(unit='%'), handler=level), - 'status': Override(handler=level), - 'pollinterval': - Override(visibility=3), - } + value = Parameter(datatype=FloatRange(unit='%'), handler=level) + status = Parameter(handler=level) + pollinterval = Parameter(visibility=3) channel = 'level' @@ -360,16 +323,13 @@ class Chamber(PpmsMixin, Drivable): venting_continuously=9, general_failure=15, ) - parameters = { - 'value': - Override(description='chamber state', handler=chamber, - datatype=EnumType(StatusCode)), - 'target': - Override(description='chamber command', handler=chamber, - datatype=EnumType(Operation)), - 'pollinterval': - Override(visibility=3), - } + + value = Parameter(description='chamber state', handler=chamber, + datatype=EnumType(StatusCode)) + target = Parameter(description='chamber command', handler=chamber, + datatype=EnumType(Operation)) + pollinterval = Parameter(visibility=3) + STATUS_MAP = { StatusCode.purged_and_sealed: (Status.IDLE, 'purged and sealed'), StatusCode.vented_and_sealed: (Status.IDLE, 'vented and sealed'), @@ -409,37 +369,29 @@ class Temp(PpmsMixin, Drivable): """temperature""" temp = IOHandler('temp', 'TEMP?', '%g,%g,%d') - Status = Enum(Drivable.Status, - RAMPING = 370, - STABILIZING = 380, + Status = Enum( + Drivable.Status, + RAMPING=370, + STABILIZING=380, ) # pylint: disable=invalid-name ApproachMode = Enum('ApproachMode', fast_settle=0, no_overshoot=1) - parameters = { - 'value': - Override(datatype=FloatRange(unit='K'), poll=True), - 'status': - Override(datatype=StatusType(Status), poll=True), - 'target': - Override(datatype=FloatRange(1.7, 402.0, unit='K'), poll=False, needscfg=False), - 'setpoint': - Parameter('intermediate set point', - datatype=FloatRange(1.7, 402.0, unit='K'), handler=temp), - 'ramp': - Parameter('ramping speed', readonly=False, default=0, - datatype=FloatRange(0, 20, unit='K/min')), - 'workingramp': - Parameter('intermediate ramp value', - datatype=FloatRange(0, 20, unit='K/min'), handler=temp), - 'approachmode': - Parameter('how to approach target!', readonly=False, handler=temp, - datatype=EnumType(ApproachMode)), - 'pollinterval': - Override(visibility=3), - 'timeout': - Parameter('drive timeout, in addition to ramp time', readonly=False, - datatype=FloatRange(0, unit='sec'), default=3600), - } + + value = Parameter(datatype=FloatRange(unit='K'), poll=True) + status = Parameter(datatype=StatusType(Status), poll=True) + target = Parameter(datatype=FloatRange(1.7, 402.0, unit='K'), poll=False, needscfg=False) + setpoint = Parameter('intermediate set point', + datatype=FloatRange(1.7, 402.0, unit='K'), handler=temp) + ramp = Parameter('ramping speed', readonly=False, default=0, + datatype=FloatRange(0, 20, unit='K/min')) + workingramp = Parameter('intermediate ramp value', + datatype=FloatRange(0, 20, unit='K/min'), handler=temp) + approachmode = Parameter('how to approach target!', readonly=False, handler=temp, + datatype=EnumType(ApproachMode)) + pollinterval = Parameter(visibility=3) + timeout = Parameter('drive timeout, in addition to ramp time', readonly=False, + datatype=FloatRange(0, unit='sec'), default=3600) + # pylint: disable=invalid-name TempStatus = Enum( 'TempStatus', @@ -464,17 +416,14 @@ class Temp(PpmsMixin, Drivable): 14: (Status.ERROR, 'can not complete'), 15: (Status.ERROR, 'general failure'), } - properties = { - 'general_stop': Property('respect general stop', datatype=BoolType(), - export=True, default=True) - } + general_stop = Property('respect general stop', datatype=BoolType(), + default=True, value=False) channel = 'temp' _stopped = False _expected_target_time = 0 _last_change = 0 # 0 means no target change is pending _last_target = None # last reached target - general_stop = False _cool_deadline = 0 _wait_at10 = False _ramp_at_limit = False @@ -588,7 +537,7 @@ class Temp(PpmsMixin, Drivable): def calc_expected(self, target, ramp): self._expected_target_time = time.time() + abs(target - self.value) * 60.0 / max(0.1, ramp) - def do_stop(self): + def stop(self): if not self.isDriving(): return if self.status[0] != self.Status.STABILIZING: @@ -605,35 +554,27 @@ class Field(PpmsMixin, Drivable): """magnetic field""" field = IOHandler('field', 'FIELD?', '%g,%g,%d,%d') - Status = Enum(Drivable.Status, - PREPARED = 150, - PREPARING = 340, - RAMPING = 370, - FINALIZING = 390, + Status = Enum( + Drivable.Status, + PREPARED=150, + PREPARING=340, + RAMPING=370, + FINALIZING=390, ) # pylint: disable=invalid-name PersistentMode = Enum('PersistentMode', persistent=0, driven=1) ApproachMode = Enum('ApproachMode', linear=0, no_overshoot=1, oscillate=2) - parameters = { - 'value': - Override(datatype=FloatRange(unit='T'), poll=True), - 'status': - Override(datatype=StatusType(Status), poll=True), - 'target': - Override(datatype=FloatRange(-15, 15, unit='T'), handler=field), - 'ramp': - Parameter('ramping speed', readonly=False, handler=field, - datatype=FloatRange(0.064, 1.19, unit='T/min')), - 'approachmode': - Parameter('how to approach target', readonly=False, handler=field, - datatype=EnumType(ApproachMode)), - 'persistentmode': - Parameter('what to do after changing field', readonly=False, handler=field, - datatype=EnumType(PersistentMode)), - 'pollinterval': - Override(visibility=3), - } + value = Parameter(datatype=FloatRange(unit='T'), poll=True) + status = Parameter(datatype=StatusType(Status), poll=True) + target = Parameter(datatype=FloatRange(-15, 15, unit='T'), handler=field) + ramp = Parameter('ramping speed', readonly=False, handler=field, + datatype=FloatRange(0.064, 1.19, unit='T/min')) + approachmode = Parameter('how to approach target', readonly=False, handler=field, + datatype=EnumType(ApproachMode)) + persistentmode = Parameter('what to do after changing field', readonly=False, handler=field, + datatype=EnumType(PersistentMode)) + pollinterval = Parameter(visibility=3) STATUS_MAP = { 1: (Status.IDLE, 'persistent mode'), @@ -669,7 +610,7 @@ class Field(PpmsMixin, Drivable): else: status = (self.Status.WARN, 'timeout when ramping leads') elif now > self._last_change + 5: - self._last_change = 0 # give up waiting for driving + self._last_change = 0 # give up waiting for driving elif self.isDriving(status) and status != self._status_before_change: self._last_change = 0 self.log.debug('time needed to change to busy: %.3g', now - self._last_change) @@ -735,7 +676,7 @@ class Field(PpmsMixin, Drivable): return Done return None # do not execute FIELD command, as this would trigger a ramp up of leads current - def do_stop(self): + def stop(self): if not self.isDriving(): return newtarget = clamp(self._last_target, self.value, self.target) @@ -751,20 +692,15 @@ class Position(PpmsMixin, Drivable): move = IOHandler('move', 'MOVE?', '%g,%g,%g') Status = Drivable.Status - parameters = { - 'value': - Override(datatype=FloatRange(unit='deg'), poll=True), - 'target': - Override(datatype=FloatRange(-720., 720., unit='deg'), handler=move), - 'enabled': - Parameter('is this channel used?', readonly=False, poll=False, - datatype=BoolType(), default=True), - 'speed': - Parameter('motor speed', readonly=False, handler=move, - datatype=FloatRange(0.8, 12, unit='deg/sec')), - 'pollinterval': - Override(visibility=3), - } + + value = Parameter(datatype=FloatRange(unit='deg'), poll=True) + target = Parameter(datatype=FloatRange(-720., 720., unit='deg'), handler=move) + enabled = Parameter('is this channel used?', readonly=False, poll=False, + datatype=BoolType(), default=True) + speed = Parameter('motor speed', readonly=False, handler=move, + datatype=FloatRange(0.8, 12, unit='deg/sec')) + pollinterval = Parameter(visibility=3) + STATUS_MAP = { 1: (Status.IDLE, 'at target'), 5: (Status.BUSY, 'moving'), @@ -843,7 +779,7 @@ class Position(PpmsMixin, Drivable): self.speed = value return None # do not execute MOVE command, as this would trigger an unnecessary move - def do_stop(self): + def stop(self): if not self.isDriving(): return newtarget = clamp(self._last_target, self.value, self.target) diff --git a/secop_psi/softcal.py b/secop_psi/softcal.py index dcc4a91..c47b2e5 100644 --- a/secop_psi/softcal.py +++ b/secop_psi/softcal.py @@ -26,7 +26,7 @@ import math import numpy as np from scipy.interpolate import splrep, splev # pylint: disable=import-error -from secop.core import Readable, Parameter, Override, Attached, StringType, BoolType +from secop.core import Readable, Parameter, Attached, StringType, BoolType def linear(x): @@ -102,6 +102,7 @@ class CalCurve: sensopt = calibspec.split(',') calibname = sensopt.pop(0) _, dot, ext = basename(calibname).rpartition('.') + kind = None for path in os.environ.get('FRAPPY_CALIB_PATH', '').split(','): # first try without adding kind filename = join(path.strip(), calibname) @@ -150,16 +151,14 @@ class CalCurve: class Sensor(Readable): - properties = { - 'rawsensor': Attached(), - } - parameters = { - 'calib': Parameter('calibration name', datatype=StringType(), readonly=False), - 'abs': Parameter('True: take abs(raw) before calib', datatype=BoolType(), readonly=False, default=True), - 'value': Override(unit='K'), - 'pollinterval': Override(export=False), - 'status': Override(default=(Readable.Status.ERROR, 'unintialized')) - } + rawsensor = Attached() + + calib = Parameter('calibration name', datatype=StringType(), readonly=False) + abs = Parameter('True: take abs(raw) before calib', datatype=BoolType(), readonly=False, default=True) + value = Parameter(unit='K') + pollinterval = Parameter(export=False) + status = Parameter(default=(Readable.Status.ERROR, 'unintialized')) + pollerClass = None description = 'a calibrated sensor value' _value_error = None diff --git a/test/test_datatypes.py b/test/test_datatypes.py index dddab7a..59d6fa1 100644 --- a/test/test_datatypes.py +++ b/test/test_datatypes.py @@ -25,7 +25,7 @@ # no fixtures needed import pytest -from secop.datatypes import ArrayOf, BLOBType, BoolType, \ +from secop.datatypes import ArrayOf, BLOBType, BoolType, Enum, StatusType, \ DataType, EnumType, FloatRange, IntRange, ProgrammingError, ConfigError, \ ScaledInteger, StringType, TextType, StructOf, TupleOf, get_datatype, CommandType @@ -359,6 +359,7 @@ def test_BoolType(): # pylint: disable=unexpected-keyword-arg BoolType(unit='K') + def test_ArrayOf(): # test constructor catching illegal arguments with pytest.raises(ValueError): @@ -478,6 +479,14 @@ def test_Command(): 'result':{'type': 'int', 'min':-3, 'max':3}} +def test_StatusType(): + status_codes = Enum('Status', IDLE=100, WARN=200, BUSY=300, ERROR=400) + dt = StatusType(status_codes) + assert dt.IDLE == status_codes.IDLE + assert dt.ERROR == status_codes.ERROR + assert dt._enum == status_codes + + def test_get_datatype(): with pytest.raises(ValueError): get_datatype(1) diff --git a/test/test_iohandler.py b/test/test_iohandler.py index 18ff740..4b7ba77 100644 --- a/test/test_iohandler.py +++ b/test/test_iohandler.py @@ -107,15 +107,11 @@ def test_IOHandler(): class Module1(Module): - properties = { - 'channel': Property('the channel', IntRange(), default=3), - 'loop': Property('the loop', IntRange(), default=2), - } - parameters = { - 'simple': Parameter('a readonly', FloatRange(), default=0.77, handler=group1), - 'real': Parameter('a float value', FloatRange(), default=12.3, handler=group2, readonly=False), - 'text': Parameter('a string value', StringType(), default='x', handler=group2, readonly=False), - } + channel = Property('the channel', IntRange(), default=3) + loop = Property('the loop', IntRange(), default=2) + simple = Parameter('a readonly', FloatRange(), default=0.77, handler=group1) + real = Parameter('a float value', FloatRange(), default=12.3, handler=group2, readonly=False) + text = Parameter('a string value', StringType(), default='x', handler=group2, readonly=False) def sendRecv(self, command): assert data.pop('command') == command @@ -196,6 +192,4 @@ def test_IOHandler(): with pytest.raises(ProgrammingError): # can not use a handler for different modules # pylint: disable=unused-variable class Module2(Module): - parameters = { - 'simple': Parameter('a readonly', FloatRange(), default=0.77, handler=group1), - } + simple = Parameter('a readonly', FloatRange(), default=0.77, handler=group1) diff --git a/test/test_modules.py b/test/test_modules.py index f5c534a..06c3094 100644 --- a/test/test_modules.py +++ b/test/test_modules.py @@ -22,14 +22,14 @@ # ***************************************************************************** """test data types.""" -# no fixtures needed -#import pytest - import threading +import pytest + from secop.datatypes import BoolType, FloatRange, StringType from secop.modules import Communicator, Drivable, Module -from secop.params import Command, Override, Parameter, usercommand +from secop.params import Command, Parameter from secop.poller import BasicPoller +from secop.errors import ProgrammingError class DispatcherStub: @@ -64,30 +64,27 @@ def test_Communicator(): assert event.is_set() # event should be set immediately -def test_ModuleMeta(): +def test_ModuleMagic(): class Newclass1(Drivable): - parameters = { - 'pollinterval': Override(reorder=True), - 'param1' : Parameter('param1', datatype=BoolType(), default=False), - 'param2': Parameter('param2', datatype=FloatRange(unit='Ohm'), default=True), - "cmd": Command('stuff', argument=BoolType(), result=BoolType()) - } - commands = { - # intermixing parameters with commands is not recommended, - # but acceptable for influencing the order - 'a1': Parameter('a1', datatype=BoolType(), default=False), - 'a2': Parameter('a2', datatype=BoolType(), default=True), - 'value': Override(datatype=StringType(), default='first'), - 'cmd2': Command('another stuff', argument=BoolType(), result=BoolType()), - } + param1 = Parameter('param1', datatype=BoolType(), default=False) + param2 = Parameter('param2', datatype=FloatRange(unit='Ohm'), default=True) + + @Command(argument=BoolType(), result=BoolType()) + def cmd(self, arg): + """stuff""" + return not arg + + a1 = Parameter('a1', datatype=BoolType(), default=False) + a2 = Parameter('a2', datatype=BoolType(), default=True) + value = Parameter(datatype=StringType(), default='first') + + @Command(argument=BoolType(), result=BoolType()) + def cmd2(self, arg): + """another stuff""" + return not arg + pollerClass = BasicPoller - def do_cmd(self, arg): - return not arg - - def do_cmd2(self, arg): - return not arg - def read_param1(self): return True @@ -103,19 +100,31 @@ def test_ModuleMeta(): def read_value(self): return 'second' + with pytest.raises(ProgrammingError): + class Mod1(Module): # pylint: disable=unused-variable + def do_this(self): # old style command + pass - # first inherited accessibles, then Overrides with reorder=True and new accessibles - sortcheck1 = ['value', 'status', 'target', 'pollinterval', + with pytest.raises(ProgrammingError): + class Mod2(Module): # pylint: disable=unused-variable + param = Parameter(), # pylint: disable=trailing-comma-tuple + + + # first inherited accessibles + sortcheck1 = ['value', 'status', 'pollinterval', 'target', 'stop', 'param1', 'param2', 'cmd', 'a1', 'a2', 'cmd2'] class Newclass2(Newclass1): - parameters = { - 'cmd2': Override('another stuff'), - 'value': Override(datatype=FloatRange(unit='deg'), reorder=True), - 'a1': Override(datatype=FloatRange(unit='$/s'), reorder=True, readonly=False), - 'b2': Parameter('', datatype=BoolType(), default=True, - poll=True, readonly=False, initwrite=True), - } + paramOrder = 'param1', 'param2', 'cmd', 'value' + + @Command(description='another stuff') + def cmd2(self, arg): + return arg + + value = Parameter(datatype=FloatRange(unit='deg')) + a1 = Parameter(datatype=FloatRange(unit='$/s'), readonly=False) + b2 = Parameter('', datatype=BoolType(), default=True, + poll=True, readonly=False, initwrite=True) def write_a1(self, value): self._a1_written = value @@ -128,47 +137,15 @@ def test_ModuleMeta(): def read_value(self): return 0 - sortcheck2 = ['status', 'target', 'pollinterval', - 'param1', 'param2', 'cmd', 'a2', 'cmd2', 'value', 'a1', 'b2'] - - # check consistency of new syntax: - class Testclass1(Drivable): - pollinterval = Parameter(reorder=True) - param1 = Parameter('param1', datatype=BoolType(), default=False) - param2 = Parameter('param2', datatype=FloatRange(unit='Ohm'), default=True) - - @usercommand(BoolType(), BoolType()) - def cmd(self, arg): - """stuff""" - return not arg - - a1 = Parameter('a1', datatype=BoolType(), default=False) - a2 = Parameter('a2', datatype=BoolType(), default=True) - value = Parameter(datatype=StringType(), default='first') - - @usercommand(BoolType(), BoolType()) - def cmd2(self, arg): - """another stuff""" - return not arg - - class Testclass2(Testclass1): - cmd2 = Command('another stuff') - value = Parameter(datatype=FloatRange(unit='deg'), reorder=True) - a1 = Parameter(datatype=FloatRange(unit='$/s'), reorder=True, readonly=False) - b2 = Parameter('', datatype=BoolType(), default=True, - poll=True, readonly=False, initwrite=True) - - for old, new in (Newclass1, Testclass1), (Newclass2, Testclass2): - assert len(old.accessibles) == len(new.accessibles) - for (oname, oobj), (nname, nobj) in zip(old.accessibles.items(), new.accessibles.items()): - assert oname == nname - assert oobj.for_export() == nobj.for_export() + # first inherited items not mentioned, then the ones mentioned in paramOrder, then the other new ones + sortcheck2 = ['status', 'pollinterval', 'target', 'stop', + 'a1', 'a2', 'cmd2', 'param1', 'param2', 'cmd', 'value', 'b2'] logger = LoggerStub() updates = {} srv = ServerStub(updates) - params_found = set() # set of instance accessibles + params_found = set() # set of instance accessibles objects = [] for newclass, sortcheck in [(Newclass1, sortcheck1), (Newclass2, sortcheck2)]: @@ -176,15 +153,11 @@ def test_ModuleMeta(): o2 = newclass('o2', logger, {'.description':''}, srv) for obj in [o1, o2]: objects.append(obj) - ctr_found = set() - for n, o in obj.accessibles.items(): + for o in obj.accessibles.values(): # check that instance accessibles are unique objects assert o not in params_found params_found.add(o) - assert o.ctr not in ctr_found - ctr_found.add(o.ctr) - check_order = [(obj.accessibles[n].ctr, n) for n in sortcheck] - assert check_order == sorted(check_order) + assert list(obj.accessibles) == sortcheck # check for inital updates working properly o1 = Newclass1('o1', logger, {'.description':''}, srv) @@ -246,7 +219,7 @@ def test_ModuleMeta(): assert acs is not None else: # do not check object or mixin acs = {} - for n, o in acs.items(): + for o in acs.values(): # check that class accessibles are not reused as instance accessibles assert o not in params_found diff --git a/test/test_params.py b/test/test_params.py index d57d3d3..0f9bfef 100644 --- a/test/test_params.py +++ b/test/test_params.py @@ -25,68 +25,78 @@ # no fixtures needed import pytest -from secop.datatypes import BoolType, IntRange -from secop.params import Command, Override, Parameter, Parameters +from secop.datatypes import BoolType, IntRange, FloatRange +from secop.params import Command, Parameter +from secop.modules import HasAccessibles from secop.errors import ProgrammingError def test_Command(): - cmd = Command('do_something') - assert cmd.description == 'do_something' - assert cmd.ctr - assert cmd.argument is None - assert cmd.result is None - assert cmd.for_export() == {'datainfo': {'type': 'command'}, - 'description': 'do_something'} + class Mod(HasAccessibles): + @Command() + def cmd(self): + """do something""" + @Command(IntRange(-9,9), result=IntRange(-1,1), description='do some other thing') + def cmd2(self): + pass - cmd = Command('do_something', argument=IntRange(-9,9), result=IntRange(-1,1)) - assert cmd.description - assert isinstance(cmd.argument, IntRange) - assert isinstance(cmd.result, IntRange) - assert cmd.for_export() == {'datainfo': {'type': 'command', 'argument': {'type': 'int', 'min':-9, 'max':9}, - 'result': {'type': 'int', 'min':-1, 'max':1}}, - 'description': 'do_something'} - assert cmd.exportProperties() == {'datainfo': {'type': 'command', 'argument': {'type': 'int', 'max': 9, 'min': -9}, - 'result': {'type': 'int', 'max': 1, 'min': -1}}, - 'description': 'do_something'} + assert Mod.cmd.description == 'do something' + assert Mod.cmd.argument is None + assert Mod.cmd.result is None + assert Mod.cmd.for_export() == {'datainfo': {'type': 'command'}, + 'description': 'do something'} + + assert Mod.cmd2.description == 'do some other thing' + assert isinstance(Mod.cmd2.argument, IntRange) + assert isinstance(Mod.cmd2.result, IntRange) + assert Mod.cmd2.for_export() == {'datainfo': {'type': 'command', 'argument': {'type': 'int', 'min': -9, 'max': 9}, + 'result': {'type': 'int', 'min': -1, 'max': 1}}, + 'description': 'do some other thing'} + assert Mod.cmd2.exportProperties() == {'datainfo': {'type': 'command', 'argument': {'type': 'int', 'max': 9, 'min': -9}, + 'result': {'type': 'int', 'max': 1, 'min': -1}}, + 'description': 'do some other thing'} def test_Parameter(): - p1 = Parameter('description1', datatype=IntRange(), default=0) - p2 = Parameter('description2', datatype=IntRange(), constant=1) - assert p1 != p2 - assert p1.ctr != p2.ctr + class Mod(HasAccessibles): + p1 = Parameter('desc1', datatype=FloatRange(), default=0) + p2 = Parameter('desc2', datatype=FloatRange(), default=0, readonly=True) + p3 = Parameter('desc3', datatype=FloatRange(), default=0, readonly=False) + p4 = Parameter('desc4', datatype=FloatRange(), constant=1) + assert repr(Mod.p1) != repr(Mod.p3) + assert id(Mod.p1.datatype) != id(Mod.p2.datatype) + assert Mod.p1.exportProperties() == {'datainfo': {'type': 'double'}, 'description': 'desc1', 'readonly': True} + assert Mod.p2.exportProperties() == {'datainfo': {'type': 'double'}, 'description': 'desc2', 'readonly': True} + assert Mod.p3.exportProperties() == {'datainfo': {'type': 'double'}, 'description': 'desc3', 'readonly': False} + assert Mod.p4.exportProperties() == {'datainfo': {'type': 'double'}, 'description': 'desc4', 'readonly': True, + 'constant': 1.0} + p3 = Mod.p1.copy() + assert id(p3) != id(Mod.p1) + assert repr(Mod.p1) == repr(p3) + with pytest.raises(ProgrammingError): Parameter(None, datatype=float, inherit=False) - p3 = p1.copy() - assert p1.ctr == p3.ctr - p3.ctr = p1.ctr # manipulate ctr for next line - assert repr(p1) == repr(p3) - assert p1.datatype != p2.datatype def test_Override(): - p = Parameter('description1', datatype=BoolType, default=False) + class Base(HasAccessibles): + p1 = Parameter('description1', datatype=BoolType, default=False) + p2 = Parameter('description1', datatype=BoolType, default=False) + p3 = Parameter('description1', datatype=BoolType, default=False) - o = Override(default=True, reorder=True) - q = o.apply(p) - qctr = q.ctr - assert q.ctr > p.ctr # reorder=True: take ctr from override object - assert q != p - assert qctr == o.apply(p).ctr # do not create a new ctr when applied again + class Mod(Base): + p1 = Parameter(default=True) + p2 = Parameter() # override without change - o2 = Override(default=True) - q2 = o2.apply(p) - assert q2.ctr == p.ctr # reorder=False: take ctr from inherited param - assert q2 != p - assert repr(q2) != repr(p) + assert Mod.p1 != Base.p1 + assert Mod.p2 != Base.p2 + assert Mod.p3 == Base.p3 - q3 = Override().apply(p) # Override without change - assert id(q2) != id(p) # must be a new object - assert repr(q3) == repr(p) # but must be a clone + assert id(Mod.p2) != id(Base.p2) # must be a new object + assert repr(Mod.p2) == repr(Base.p2) # but must be a clone -def test_Parameters(): - ps = Parameters(dict(p1=Parameter('p1', datatype=BoolType, default=True))) - ps['p2'] = Parameter('p2', datatype=BoolType, default=True, export=True) - assert ps['_p2'].export == '_p2' +def test_Export(): + class Mod: + param = Parameter('description1', datatype=BoolType, default=False) + assert Mod.param.export == '_param' diff --git a/test/test_properties.py b/test/test_properties.py index 6c2d226..9245059 100644 --- a/test/test_properties.py +++ b/test/test_properties.py @@ -24,38 +24,58 @@ import pytest from secop.datatypes import IntRange, StringType, FloatRange, ValueType -from secop.errors import ProgrammingError, ConfigError -from secop.properties import Property, Properties, HasProperties +from secop.errors import ProgrammingError, ConfigError, BadValueError +from secop.properties import Property, HasProperties -# args are: datatype, default, extname, export, mandatory, settable + +def Prop(*args, name=None, **kwds): + # collect the args for Property + return name, args, kwds + + +# Property(description, datatype, default, ...) V_test_Property = [ - [(StringType(), 'default', 'extname', False, False), - dict(default='default', extname='extname', export=True, mandatory=False)], - [(IntRange(), '42', '_extname', False, True), - dict(default=42, extname='_extname', export=True, mandatory=True)], - [(IntRange(), '42', '_extname', True, False), - dict(default=42, extname='_extname', export=True, mandatory=False)], - [(IntRange(), 42, '_extname', True, True), - dict(default=42, extname='_extname', export=True, mandatory=True)], - [(IntRange(), 0, '', True, True), - dict(default=0, extname='', export=True, mandatory=True)], - [(IntRange(), 0, '', True, False), - dict(default=0, extname='', export=True, mandatory=False)], - [(IntRange(), 0, '', False, True), - dict(default=0, extname='', export=False, mandatory=True)], - [(IntRange(), 0, '', False, False), - dict(default=0, extname='', export=False, mandatory=False)], - [(IntRange(), None, '', None), - dict(default=0, extname='', export=False, mandatory=True)], # mandatory not given, no default -> mandatory - [(ValueType(), 1, '', False), - dict(default=1, extname='', export=False, mandatory=False)], # mandatory not given, default given -> NOT mandatory + [Prop(StringType(), 'default', extname='extname', mandatory=False), + dict(default='default', extname='extname', export=True, mandatory=False) + ], + [Prop(IntRange(), '42', export=True, name='custom', mandatory=True), + dict(default=42, extname='_custom', export=True, mandatory=True), + ], + [Prop(IntRange(), '42', export=True, name='name'), + dict(default=42, extname='_name', export=True, mandatory=False) + ], + [Prop(IntRange(), 42, '_extname', mandatory=True), + dict(default=42, extname='_extname', export=True, mandatory=True) + ], + [Prop(IntRange(), 0, export=True, mandatory=True), + dict(default=0, extname='', export=True, mandatory=True) + ], + [Prop(IntRange(), 0, export=True, mandatory=False), + dict(default=0, extname='', export=True, mandatory=False) + ], + [Prop(IntRange(), 0, export=False, mandatory=True), + dict(default=0, extname='', export=False, mandatory=True) + ], + [Prop(IntRange(), 0, export=False, mandatory=False), + dict(default=0, extname='', export=False, mandatory=False) + ], + [Prop(IntRange()), + dict(default=0, extname='', export=False, mandatory=True) # mandatory not given, no default -> mandatory + ], + [Prop(ValueType(), 1), + dict(default=1, extname='', export=False, mandatory=False) # mandatory not given, default given -> NOT mandatory + ], ] -@pytest.mark.parametrize('args, check', V_test_Property) -def test_Property(args, check): - p = Property('', *args) +@pytest.mark.parametrize('propargs, check', V_test_Property) +def test_Property(propargs, check): + name, args, kwds = propargs + p = Property('', *args, **kwds) + if name: + p.__set_name__(None, name) result = {k: getattr(p, k) for k in check} assert result == check + def test_Property_basic(): with pytest.raises(TypeError): # pylint: disable=no-value-for-parameter @@ -67,47 +87,47 @@ def test_Property_basic(): Property('', 1) Property('', IntRange(), '42', 'extname', False, False) + def test_Properties(): - p = Properties() - with pytest.raises(ProgrammingError): - p[1] = 2 - p['a'] = Property('', IntRange(), '42', export=True) - assert p['a'].default == 42 - assert p['a'].export is True - assert p['a'].extname == '_a' - with pytest.raises(ProgrammingError): - p['a'] = 137 - with pytest.raises(ProgrammingError): - del p[1] - with pytest.raises(ProgrammingError): - del p['a'] - p['a'] = Property('', IntRange(), 0, export=False) - assert p['a'].default == 0 - assert p['a'].export is False - assert p['a'].extname == '' + class Cls(HasProperties): + aa = Property('', IntRange(0, 99), '42', export=True) + bb = Property('', IntRange(), 0, export=False) + + assert Cls.aa.default == 42 + assert Cls.aa.export is True + assert Cls.aa.extname == '_aa' + + cc = Cls() + with pytest.raises(BadValueError): + cc.aa = 137 + + assert Cls.bb.default == 0 + assert Cls.bb.export is False + assert Cls.bb.extname == '' class c(HasProperties): - properties = { - 'a' : Property('', IntRange(), 1), - } + # properties + a = Property('', IntRange(), 1) + class cl(c): - properties = { - 'a' : Property('', IntRange(), 3), - 'b' : Property('', FloatRange(), 3.14), - 'minabc': Property('', IntRange(), 8), - 'maxabc': Property('', IntRange(), 9), - 'minx': Property('', IntRange(), 2), - 'maxy': Property('', IntRange(), 1), - } + # properties + a = Property('', IntRange(), 3) + b = Property('', FloatRange(), 3.14) + minabc = Property('', IntRange(), 8) + maxabc = Property('', IntRange(), 9) + minx = Property('', IntRange(), 2) + maxy = Property('', IntRange(), 1) + def test_HasProperties(): o = c() - assert o.properties['a'] == 1 + assert o.a == 1 o = cl() - assert o.properties['a'] == 3 - assert o.properties['b'] == 3.14 + assert o.a == 3 + assert o.b == 3.14 + def test_Property_checks(): o = c() @@ -119,6 +139,7 @@ def test_Property_checks(): with pytest.raises(ConfigError): o.checkProperties() + def test_Property_override(): o1 = c() class co(c): @@ -131,10 +152,10 @@ def test_Property_override(): class cx(c): # pylint: disable=unused-variable def a(self): pass - assert 'collides with method' in str(e.value) + assert 'collides with' in str(e.value) with pytest.raises(ProgrammingError) as e: class cz(c): # pylint: disable=unused-variable a = 's' - assert 'can not be set to' in str(e.value) + assert 'can not set' in str(e.value)