use Properties from secop.properties for datatypes
this change is triggered by the fact, that assigining a unit in the config file did no longer work. this change has several implications: 1) secop.properties must not import secop.datatypes: - as ValueType can not be imported, the default behaviour with 'mandatory' and 'default' arguments was slightly changed - instead of checking for DataType when exporting, a try/except was used 2) the datatype of datatype properties is sometimes not yet defined. a stub is used in this cases instead, which is later replaced by the proper datatype. The number of stubs may be reduced, but this should be done in a later change, as the diff will be much less readable. 3) in config files, datatype properties can be changed like parameter properties. HasProperties.setProperties/checkProperties/getProperties are overridden for this. the config editor seems still to work, an issue (probably py3) had to be fixed there Change-Id: I1efddf51f2c760510e913dbcaa099e8a89c9cab5 Reviewed-on: https://forge.frm2.tum.de/review/c/sine2020/secop/playground/+/21399 Tested-by: JenkinsCodeReview <bjoern_pedersen@frm2.tum.de> Reviewed-by: Enrico Faulhaber <enrico.faulhaber@frm2.tum.de> Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
This commit is contained in:
parent
7688abfc8d
commit
f4d572966c
@ -24,11 +24,13 @@
|
|||||||
# pylint: disable=abstract-method
|
# pylint: disable=abstract-method
|
||||||
|
|
||||||
|
|
||||||
|
import sys
|
||||||
from base64 import b64decode, b64encode
|
from base64 import b64decode, b64encode
|
||||||
|
|
||||||
from secop.errors import ProgrammingError, ProtocolError, BadValueError
|
from secop.errors import ProgrammingError, ProtocolError, BadValueError, ConfigError
|
||||||
from secop.lib.enum import Enum
|
from secop.lib.enum import Enum
|
||||||
from secop.parse import Parser
|
from secop.parse import Parser
|
||||||
|
from secop.properties import HasProperties, Property
|
||||||
|
|
||||||
|
|
||||||
# Only export these classes for 'from secop.datatypes import *'
|
# Only export these classes for 'from secop.datatypes import *'
|
||||||
@ -44,14 +46,14 @@ __all__ = [
|
|||||||
# *DEFAULT* limits for IntRange/ScaledIntegers transport serialisation
|
# *DEFAULT* limits for IntRange/ScaledIntegers transport serialisation
|
||||||
DEFAULT_MIN_INT = -16777216
|
DEFAULT_MIN_INT = -16777216
|
||||||
DEFAULT_MAX_INT = 16777216
|
DEFAULT_MAX_INT = 16777216
|
||||||
|
UNLIMITED = 1 << 64 # internal limit for integers, is probably high enough for any datatype size
|
||||||
|
|
||||||
Parser = Parser()
|
Parser = Parser()
|
||||||
|
|
||||||
# base class for all DataTypes
|
# base class for all DataTypes
|
||||||
class DataType:
|
class DataType(HasProperties):
|
||||||
IS_COMMAND = False
|
IS_COMMAND = False
|
||||||
unit = ''
|
unit = ''
|
||||||
fmtstr = '%r'
|
|
||||||
default = None
|
default = None
|
||||||
|
|
||||||
def __call__(self, value):
|
def __call__(self, value):
|
||||||
@ -89,23 +91,23 @@ class DataType:
|
|||||||
if unit is given, use it, else use the unit of the datatype (if any)"""
|
if unit is given, use it, else use the unit of the datatype (if any)"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def set_prop(self, key, value, default, func=lambda x:x):
|
def set_properties(self, **kwds):
|
||||||
"""set an optional datatype property and store the default"""
|
"""init datatype properties"""
|
||||||
self._defaults[key] = default
|
try:
|
||||||
if value is None:
|
for k,v in kwds.items():
|
||||||
value = default
|
self.setProperty(k, v)
|
||||||
setattr(self, key, func(value))
|
self.checkProperties()
|
||||||
|
except Exception as e:
|
||||||
|
raise ProgrammingError(str(e))
|
||||||
|
|
||||||
def get_info(self, **kwds):
|
def get_info(self, **kwds):
|
||||||
"""prepare dict for export or repr
|
"""prepare dict for export or repr
|
||||||
|
|
||||||
get a dict with all items different from default
|
get a dict with all items different from default
|
||||||
plus mandatory keys from kwds"""
|
plus mandatory keys from kwds"""
|
||||||
for k,v in self._defaults.items():
|
result = self.exportProperties()
|
||||||
value = getattr(self, k)
|
result.update(kwds)
|
||||||
if value != v:
|
return result
|
||||||
kwds[k] = value
|
|
||||||
return kwds
|
|
||||||
|
|
||||||
def copy(self):
|
def copy(self):
|
||||||
"""make a deep copy of the datatype"""
|
"""make a deep copy of the datatype"""
|
||||||
@ -114,29 +116,62 @@ class DataType:
|
|||||||
return get_datatype(self.export_datatype())
|
return get_datatype(self.export_datatype())
|
||||||
|
|
||||||
|
|
||||||
|
class Stub(DataType):
|
||||||
|
"""incomplete datatype, to be replaced with a proper one later during module load
|
||||||
|
|
||||||
|
this workaround because datatypes need properties with datatypes defined later
|
||||||
|
"""
|
||||||
|
def __init__(self, datatype_name, *args):
|
||||||
|
super().__init__()
|
||||||
|
self.name = datatype_name
|
||||||
|
self.args = args
|
||||||
|
|
||||||
|
def __call__(self, value):
|
||||||
|
"""validate"""
|
||||||
|
return value
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def fix_datatypes(cls):
|
||||||
|
"""replace stubs with real datatypes
|
||||||
|
|
||||||
|
for all DataType classes in this module
|
||||||
|
to be called after all involved datatypes are defined
|
||||||
|
"""
|
||||||
|
for dtcls in globals().values():
|
||||||
|
if isinstance(dtcls, type) and issubclass(dtcls, DataType):
|
||||||
|
for prop in dtcls.properties.values():
|
||||||
|
stub = prop.datatype
|
||||||
|
if isinstance(stub, cls):
|
||||||
|
prop.datatype = globals()[stub.name](*stub.args)
|
||||||
|
|
||||||
|
# SECoP types:
|
||||||
|
|
||||||
class FloatRange(DataType):
|
class FloatRange(DataType):
|
||||||
"""Restricted float type"""
|
"""Restricted float type"""
|
||||||
|
properties = {
|
||||||
|
'min': Property('low limit', Stub('FloatRange'), extname='min', default=float('-inf')),
|
||||||
|
'max': Property('high limit', Stub('FloatRange'), extname='max', default=float('+inf')),
|
||||||
|
'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, unit=None, fmtstr=None,
|
def __init__(self, minval=None, maxval=None, **kwds):
|
||||||
absolute_resolution=None, relative_resolution=None,):
|
super().__init__()
|
||||||
self._defaults = {}
|
if minval is not None:
|
||||||
self.set_prop('min', minval, float('-inf'), float)
|
kwds['min'] = minval
|
||||||
self.set_prop('max', maxval, float('+inf'), float)
|
if maxval is not None:
|
||||||
self.set_prop('unit', unit, '', str)
|
kwds['max'] = maxval
|
||||||
self.set_prop('fmtstr', fmtstr, '%g', str)
|
self.set_properties(**kwds)
|
||||||
self.set_prop('absolute_resolution', absolute_resolution, 0.0, float)
|
|
||||||
self.set_prop('relative_resolution', relative_resolution, 1.2e-7, float)
|
def checkProperties(self):
|
||||||
self.default = 0 if self.min <= 0 <= self.max else self.min
|
self.default = 0 if self.min <= 0 <= self.max else self.min
|
||||||
|
super().checkProperties()
|
||||||
# check values
|
|
||||||
if self.min > self.max:
|
|
||||||
raise BadValueError('max must be larger then min!')
|
|
||||||
if '%' not in self.fmtstr:
|
if '%' not in self.fmtstr:
|
||||||
raise BadValueError('Invalid fmtstr!')
|
raise ConfigError('Invalid fmtstr!')
|
||||||
if self.absolute_resolution < 0:
|
|
||||||
raise BadValueError('absolute_resolution MUST be >=0')
|
|
||||||
if self.relative_resolution < 0:
|
|
||||||
raise BadValueError('relative_resolution MUST be >=0')
|
|
||||||
|
|
||||||
def export_datatype(self):
|
def export_datatype(self):
|
||||||
return self.get_info(type='double')
|
return self.get_info(type='double')
|
||||||
@ -180,32 +215,34 @@ class FloatRange(DataType):
|
|||||||
return self.fmtstr % value
|
return self.fmtstr % value
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class IntRange(DataType):
|
class IntRange(DataType):
|
||||||
"""Restricted int type"""
|
"""Restricted int type"""
|
||||||
|
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=''),
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, minval=None, maxval=None):
|
def __init__(self, minval=None, maxval=None):
|
||||||
self.min = DEFAULT_MIN_INT if minval is None else int(minval)
|
super().__init__()
|
||||||
self.max = DEFAULT_MAX_INT if maxval is None else int(maxval)
|
self.set_properties(min=DEFAULT_MIN_INT if minval is None else minval,
|
||||||
self.default = 0 if self.min <= 0 <= self.max else self.min
|
max=DEFAULT_MAX_INT if maxval is None else maxval)
|
||||||
# a unit on an int is now allowed in SECoP, but do we need them in Frappy?
|
|
||||||
# self.set_prop('unit', unit, '', str)
|
|
||||||
|
|
||||||
# check values
|
def checkProperties(self):
|
||||||
if self.min > self.max:
|
self.default = 0 if self.min <= 0 <= self.max else self.min
|
||||||
raise BadValueError('Max must be larger then min!')
|
super().checkProperties()
|
||||||
|
|
||||||
def export_datatype(self):
|
def export_datatype(self):
|
||||||
return dict(type='int', min=self.min, max=self.max)
|
return self.get_info(type='int')
|
||||||
|
|
||||||
def __call__(self, value):
|
def __call__(self, value):
|
||||||
try:
|
try:
|
||||||
value = int(value)
|
value = int(value)
|
||||||
if value < self.min:
|
if not (self.min <= value <= self.max) or int(value) != value:
|
||||||
raise BadValueError('%r should be an int between %d and %d' %
|
raise BadValueError('%r should be an int between %d and %d' %
|
||||||
(value, self.min, self.max or 0))
|
(value, self.min, self.max))
|
||||||
if value > self.max:
|
|
||||||
raise BadValueError('%r should be an int between %d and %d' %
|
|
||||||
(value, self.min or 0, self.max))
|
|
||||||
return value
|
return value
|
||||||
except Exception:
|
except Exception:
|
||||||
raise BadValueError('Can not convert %r to int' % value)
|
raise BadValueError('Can not convert %r to int' % value)
|
||||||
@ -234,36 +271,54 @@ class ScaledInteger(DataType):
|
|||||||
|
|
||||||
note: limits are for the scaled value (i.e. the internal value)
|
note: limits are for the scaled value (i.e. the internal value)
|
||||||
the scale is only used for calculating to/from transport serialisation"""
|
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),
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, scale, minval=None, maxval=None, unit=None, fmtstr=None,
|
def __init__(self, scale, minval=None, maxval=None, absolute_resolution=None, **kwds):
|
||||||
absolute_resolution=None, relative_resolution=None,):
|
super().__init__()
|
||||||
self._defaults = {}
|
scale = float(scale)
|
||||||
self.scale = float(scale)
|
if absolute_resolution is None:
|
||||||
if not self.scale > 0:
|
absolute_resolution = scale
|
||||||
raise BadValueError('Scale MUST be positive!')
|
self.set_properties(scale=scale,
|
||||||
self.set_prop('unit', unit, '', str)
|
min=DEFAULT_MIN_INT * scale if minval is None else float(minval),
|
||||||
self.set_prop('fmtstr', fmtstr, '%g', str)
|
max=DEFAULT_MAX_INT * scale if maxval is None else float(maxval),
|
||||||
self.set_prop('absolute_resolution', absolute_resolution, self.scale, float)
|
absolute_resolution=absolute_resolution,
|
||||||
self.set_prop('relative_resolution', relative_resolution, 1.2e-7, float)
|
**kwds)
|
||||||
|
|
||||||
self.min = DEFAULT_MIN_INT * self.scale if minval is None else float(minval)
|
def checkProperties(self):
|
||||||
self.max = DEFAULT_MAX_INT * self.scale if maxval is None else float(maxval)
|
|
||||||
self.default = 0 if self.min <= 0 <= self.max else self.min
|
self.default = 0 if self.min <= 0 <= self.max else self.min
|
||||||
|
super().checkProperties()
|
||||||
|
|
||||||
# check values
|
# check values
|
||||||
if self.min > self.max:
|
|
||||||
raise BadValueError('Max must be larger then min!')
|
|
||||||
if '%' not in self.fmtstr:
|
if '%' not in self.fmtstr:
|
||||||
raise BadValueError('Invalid fmtstr!')
|
raise BadValueError('Invalid fmtstr!')
|
||||||
if self.absolute_resolution < 0:
|
|
||||||
raise BadValueError('absolute_resolution MUST be >=0')
|
|
||||||
if self.relative_resolution < 0:
|
|
||||||
raise BadValueError('relative_resolution MUST be >=0')
|
|
||||||
# Remark: Datatype.copy() will round min, max to a multiple of self.scale
|
# Remark: Datatype.copy() will round min, max to a multiple of self.scale
|
||||||
# this should be o.k.
|
# this should be o.k.
|
||||||
|
|
||||||
|
def exportProperties(self):
|
||||||
|
result = super().exportProperties()
|
||||||
|
if self.absolute_resolution == 0:
|
||||||
|
result['absolute_resolution'] = 0
|
||||||
|
elif self.absolute_resolution == self.scale:
|
||||||
|
result.pop('absolute_resolution', 0)
|
||||||
|
return result
|
||||||
|
|
||||||
|
def setProperty(self, key, value):
|
||||||
|
if key == 'scale' and self.absolute_resolution == self.scale:
|
||||||
|
super().setProperty('absolute_resolution', value)
|
||||||
|
super().setProperty(key, value)
|
||||||
|
|
||||||
def export_datatype(self):
|
def export_datatype(self):
|
||||||
return self.get_info(type='scaled', scale=self.scale,
|
return self.get_info(type='scaled',
|
||||||
min = int((self.min + 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))
|
max = int((self.max + self.scale * 0.5) // self.scale))
|
||||||
|
|
||||||
@ -284,7 +339,7 @@ class ScaledInteger(DataType):
|
|||||||
return value # return 'actual' value (which is more discrete than a float)
|
return value # return 'actual' value (which is more discrete than a float)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
hints = self.get_info(scale='%g' % self.scale,
|
hints = self.get_info(scale=float('%g' % self.scale),
|
||||||
min = int((self.min + 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))
|
max = int((self.max + self.scale * 0.5) // self.scale))
|
||||||
return 'ScaledInteger(%s)' % (', '.join('%s=%r' % kv for kv in hints.items()))
|
return 'ScaledInteger(%s)' % (', '.join('%s=%r' % kv for kv in hints.items()))
|
||||||
@ -313,6 +368,7 @@ class ScaledInteger(DataType):
|
|||||||
class EnumType(DataType):
|
class EnumType(DataType):
|
||||||
|
|
||||||
def __init__(self, enum_or_name='', **kwds):
|
def __init__(self, enum_or_name='', **kwds):
|
||||||
|
super().__init__()
|
||||||
if 'members' in kwds:
|
if 'members' in kwds:
|
||||||
kwds = dict(kwds)
|
kwds = dict(kwds)
|
||||||
kwds.update(kwds['members'])
|
kwds.update(kwds['members'])
|
||||||
@ -353,25 +409,27 @@ class EnumType(DataType):
|
|||||||
|
|
||||||
|
|
||||||
class BLOBType(DataType):
|
class BLOBType(DataType):
|
||||||
minbytes = 0
|
properties = {
|
||||||
maxbytes = None
|
'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):
|
def __init__(self, minbytes=0, maxbytes=None):
|
||||||
|
super().__init__()
|
||||||
# if only one argument is given, use exactly that many bytes
|
# if only one argument is given, use exactly that many bytes
|
||||||
# if nothing is given, default to 255
|
# if nothing is given, default to 255
|
||||||
if maxbytes is None:
|
if maxbytes is None:
|
||||||
maxbytes = minbytes or 255
|
maxbytes = minbytes or 255
|
||||||
self._defaults = {}
|
self.set_properties(minbytes=minbytes, maxbytes=maxbytes)
|
||||||
self.set_prop('minbytes', minbytes, 0, int)
|
|
||||||
self.maxbytes = int(maxbytes)
|
def checkProperties(self):
|
||||||
if self.minbytes < 0:
|
|
||||||
raise BadValueError('sizes must be bigger than or equal to 0!')
|
|
||||||
if self.minbytes > self.maxbytes:
|
|
||||||
raise BadValueError('maxbytes must be bigger than or equal to minbytes!')
|
|
||||||
self.default = b'\0' * self.minbytes
|
self.default = b'\0' * self.minbytes
|
||||||
|
super().checkProperties()
|
||||||
|
|
||||||
def export_datatype(self):
|
def export_datatype(self):
|
||||||
return self.get_info(type='blob', maxbytes=self.maxbytes)
|
return self.get_info(type='blob')
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return 'BLOBType(%d, %d)' % (self.minbytes, self.maxbytes)
|
return 'BLOBType(%d, %d)' % (self.minbytes, self.maxbytes)
|
||||||
@ -407,20 +465,24 @@ class BLOBType(DataType):
|
|||||||
|
|
||||||
|
|
||||||
class StringType(DataType):
|
class StringType(DataType):
|
||||||
MAXCHARS = 0xffffffff
|
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),
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, minchars=0, maxchars=None, isUTF8=False):
|
def __init__(self, minchars=0, maxchars=None, **kwds):
|
||||||
|
super().__init__()
|
||||||
if maxchars is None:
|
if maxchars is None:
|
||||||
maxchars = minchars or self.MAXCHARS
|
maxchars = minchars or UNLIMITED
|
||||||
self._defaults = {}
|
self.set_properties(minchars=minchars, maxchars=maxchars, **kwds)
|
||||||
self.set_prop('minchars', minchars, 0, int)
|
|
||||||
self.set_prop('maxchars', maxchars, self.MAXCHARS, int)
|
def checkProperties(self):
|
||||||
self.set_prop('isUTF8', isUTF8, False, bool)
|
|
||||||
if self.minchars < 0:
|
|
||||||
raise BadValueError('sizes must be bigger than or equal to 0!')
|
|
||||||
if self.minchars > self.maxchars:
|
|
||||||
raise BadValueError('maxchars must be bigger than or equal to minchars!')
|
|
||||||
self.default = ' ' * self.minchars
|
self.default = ' ' * self.minchars
|
||||||
|
super().checkProperties()
|
||||||
|
|
||||||
def export_datatype(self):
|
def export_datatype(self):
|
||||||
return self.get_info(type='string')
|
return self.get_info(type='string')
|
||||||
@ -455,7 +517,6 @@ class StringType(DataType):
|
|||||||
|
|
||||||
def import_value(self, value):
|
def import_value(self, value):
|
||||||
"""returns a python object from serialisation"""
|
"""returns a python object from serialisation"""
|
||||||
# XXX: do we keep it as str str, or convert it to something else? (UTF-8 maybe?)
|
|
||||||
return str(value)
|
return str(value)
|
||||||
|
|
||||||
def from_string(self, text):
|
def from_string(self, text):
|
||||||
@ -473,7 +534,7 @@ class StringType(DataType):
|
|||||||
class TextType(StringType):
|
class TextType(StringType):
|
||||||
def __init__(self, maxchars=None):
|
def __init__(self, maxchars=None):
|
||||||
if maxchars is None:
|
if maxchars is None:
|
||||||
maxchars = self.MAXCHARS
|
maxchars = UNLIMITED
|
||||||
super(TextType, self).__init__(0, maxchars)
|
super(TextType, self).__init__(0, maxchars)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
@ -484,7 +545,6 @@ class TextType(StringType):
|
|||||||
return TextType(self.maxchars)
|
return TextType(self.maxchars)
|
||||||
|
|
||||||
|
|
||||||
# Bool is a special enum
|
|
||||||
class BoolType(DataType):
|
class BoolType(DataType):
|
||||||
default = False
|
default = False
|
||||||
|
|
||||||
@ -504,7 +564,7 @@ class BoolType(DataType):
|
|||||||
|
|
||||||
def export_value(self, value):
|
def export_value(self, value):
|
||||||
"""returns a python object fit for serialisation"""
|
"""returns a python object fit for serialisation"""
|
||||||
return bool(self(value))
|
return self(value)
|
||||||
|
|
||||||
def import_value(self, value):
|
def import_value(self, value):
|
||||||
"""returns a python object from serialisation"""
|
"""returns a python object from serialisation"""
|
||||||
@ -514,21 +574,26 @@ class BoolType(DataType):
|
|||||||
value = text
|
value = text
|
||||||
return self(value)
|
return self(value)
|
||||||
|
|
||||||
|
|
||||||
def format_value(self, value, unit=None):
|
def format_value(self, value, unit=None):
|
||||||
return repr(bool(value))
|
return repr(bool(value))
|
||||||
|
|
||||||
|
Stub.fix_datatypes()
|
||||||
|
|
||||||
#
|
#
|
||||||
# nested types
|
# nested types
|
||||||
#
|
#
|
||||||
|
|
||||||
|
|
||||||
class ArrayOf(DataType):
|
class ArrayOf(DataType):
|
||||||
minlen = None
|
properties = {
|
||||||
maxlen = None
|
'minlen': Property('minimum number of elements', IntRange(0), extname='minlen',
|
||||||
members = None
|
default=0),
|
||||||
|
'maxlen': Property('maximum number of elements', IntRange(0), extname='maxlen',
|
||||||
|
mandatory=True),
|
||||||
|
}
|
||||||
|
|
||||||
def __init__(self, members, minlen=0, maxlen=None, unit=None):
|
def __init__(self, members, minlen=0, maxlen=None):
|
||||||
|
super().__init__()
|
||||||
if not isinstance(members, DataType):
|
if not isinstance(members, DataType):
|
||||||
raise BadValueError(
|
raise BadValueError(
|
||||||
'ArrayOf only works with a DataType as first argument!')
|
'ArrayOf only works with a DataType as first argument!')
|
||||||
@ -537,18 +602,22 @@ class ArrayOf(DataType):
|
|||||||
if maxlen is None:
|
if maxlen is None:
|
||||||
maxlen = minlen or 100
|
maxlen = minlen or 100
|
||||||
self.members = members
|
self.members = members
|
||||||
if unit:
|
self.set_properties(minlen=minlen, maxlen=maxlen)
|
||||||
self.members.unit = unit
|
|
||||||
|
|
||||||
self.minlen = int(minlen)
|
def checkProperties(self):
|
||||||
self.maxlen = int(maxlen)
|
self.default = [self.members.default] * self.minlen
|
||||||
if self.minlen < 0:
|
super().checkProperties()
|
||||||
raise BadValueError('sizes must be > 0')
|
|
||||||
if self.maxlen < 1:
|
def getProperties(self):
|
||||||
raise BadValueError('Maximum size must be >= 1!')
|
"""get also properties of members"""
|
||||||
if self.minlen > self.maxlen:
|
return {**super().getProperties(), **self.members.getProperties()}
|
||||||
raise BadValueError('maxlen must be bigger than or equal to minlen!')
|
|
||||||
self.default = [members.default] * self.minlen
|
def setProperty(self, key, value):
|
||||||
|
"""set also properties of members"""
|
||||||
|
if key in self.__class__.properties:
|
||||||
|
super().setProperty(key, value)
|
||||||
|
else:
|
||||||
|
self.members.setProperty(key, value)
|
||||||
|
|
||||||
def export_datatype(self):
|
def export_datatype(self):
|
||||||
return dict(type='array', minlen=self.minlen, maxlen=self.maxlen,
|
return dict(type='array', minlen=self.minlen, maxlen=self.maxlen,
|
||||||
@ -600,6 +669,7 @@ class ArrayOf(DataType):
|
|||||||
class TupleOf(DataType):
|
class TupleOf(DataType):
|
||||||
|
|
||||||
def __init__(self, *members):
|
def __init__(self, *members):
|
||||||
|
super().__init__()
|
||||||
if not members:
|
if not members:
|
||||||
raise BadValueError('Empty tuples are not allowed!')
|
raise BadValueError('Empty tuples are not allowed!')
|
||||||
for subtype in members:
|
for subtype in members:
|
||||||
@ -651,6 +721,7 @@ class TupleOf(DataType):
|
|||||||
class StructOf(DataType):
|
class StructOf(DataType):
|
||||||
|
|
||||||
def __init__(self, optional=None, **members):
|
def __init__(self, optional=None, **members):
|
||||||
|
super().__init__()
|
||||||
self.members = members
|
self.members = members
|
||||||
if not members:
|
if not members:
|
||||||
raise BadValueError('Empty structs are not allowed!')
|
raise BadValueError('Empty structs are not allowed!')
|
||||||
@ -724,10 +795,9 @@ class StructOf(DataType):
|
|||||||
|
|
||||||
class CommandType(DataType):
|
class CommandType(DataType):
|
||||||
IS_COMMAND = True
|
IS_COMMAND = True
|
||||||
argument = None
|
|
||||||
result = None
|
|
||||||
|
|
||||||
def __init__(self, argument=None, result=None):
|
def __init__(self, argument=None, result=None):
|
||||||
|
super().__init__()
|
||||||
if argument is not None:
|
if argument is not None:
|
||||||
if not isinstance(argument, DataType):
|
if not isinstance(argument, DataType):
|
||||||
raise BadValueError('CommandType: Argument type must be a DataType!')
|
raise BadValueError('CommandType: Argument type must be a DataType!')
|
||||||
@ -773,7 +843,7 @@ class CommandType(DataType):
|
|||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
# internally used datatypes (i.e. only for programming the SEC-node
|
# internally used datatypes (i.e. only for programming the SEC-node)
|
||||||
class DataTypeType(DataType):
|
class DataTypeType(DataType):
|
||||||
def __call__(self, value):
|
def __call__(self, value):
|
||||||
"""check if given value (a python obj) is a valid datatype
|
"""check if given value (a python obj) is a valid datatype
|
||||||
@ -816,11 +886,13 @@ class ValueType(DataType):
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
class NoneOr(DataType):
|
class NoneOr(DataType):
|
||||||
"""validates a None or smth. else"""
|
"""validates a None or smth. else"""
|
||||||
default = None
|
default = None
|
||||||
|
|
||||||
def __init__(self, other):
|
def __init__(self, other):
|
||||||
|
super().__init__()
|
||||||
self.other = other
|
self.other = other
|
||||||
|
|
||||||
def __call__(self, value):
|
def __call__(self, value):
|
||||||
@ -834,6 +906,7 @@ class NoneOr(DataType):
|
|||||||
|
|
||||||
class OrType(DataType):
|
class OrType(DataType):
|
||||||
def __init__(self, *types):
|
def __init__(self, *types):
|
||||||
|
super().__init__()
|
||||||
self.types = types
|
self.types = types
|
||||||
self.default = self.types[0].default
|
self.default = self.types[0].default
|
||||||
|
|
||||||
@ -916,5 +989,5 @@ def get_datatype(json):
|
|||||||
raise BadValueError('a data descriptor must be a dict containing a "type" key, not %r' % json)
|
raise BadValueError('a data descriptor must be a dict containing a "type" key, not %r' % json)
|
||||||
try:
|
try:
|
||||||
return DATATYPES[base](**args)
|
return DATATYPES[base](**args)
|
||||||
except (TypeError, AttributeError, KeyError):
|
except Exception as e:
|
||||||
raise BadValueError('invalid data descriptor: %r' % json)
|
raise BadValueError('invalid data descriptor: %r (%s)' % (json, str(e)))
|
||||||
|
@ -57,22 +57,22 @@ def write_config(file_name, tree_widget):
|
|||||||
blank_lines += 1
|
blank_lines += 1
|
||||||
value = value.replace('\n\n', '\n.\n')
|
value = value.replace('\n\n', '\n.\n')
|
||||||
value = value.replace('\n', '\n ')
|
value = value.replace('\n', '\n ')
|
||||||
itm_lines[itm] = '[%s %s]\n' % (itm.kind, itm.name) +\
|
itm_lines[id(itm)] = '[%s %s]\n' % (itm.kind, itm.name) +\
|
||||||
value_str % (SECTIONS[itm.kind], value)
|
value_str % (SECTIONS[itm.kind], value)
|
||||||
# TODO params and props
|
# TODO params and props
|
||||||
elif itm.kind == PARAMETER and value:
|
elif itm.kind == PARAMETER and value:
|
||||||
itm_lines[itm] = value_str % (itm.name, value)
|
itm_lines[id(itm)] = value_str % (itm.name, value)
|
||||||
elif itm.kind == PROPERTY:
|
elif itm.kind == PROPERTY:
|
||||||
prop_name = '.%s' % itm.name
|
prop_name = '.%s' % itm.name
|
||||||
if par.kind == PARAMETER:
|
if par.kind == PARAMETER:
|
||||||
prop_name = par.name + prop_name
|
prop_name = par.name + prop_name
|
||||||
itm_lines[itm] = value_str % (prop_name, value)
|
itm_lines[id(itm)] = value_str % (prop_name, value)
|
||||||
elif itm.kind == COMMENT:
|
elif itm.kind == COMMENT:
|
||||||
temp_itm_lines = OrderedDict()
|
temp_itm_lines = OrderedDict()
|
||||||
for key, dict_value in itm_lines.items():
|
for key, dict_value in itm_lines.items():
|
||||||
if key == par:
|
if key == id(par):
|
||||||
value = value.replace('\n', '\n# ')
|
value = value.replace('\n', '\n# ')
|
||||||
temp_itm_lines[itm] = '# %s' % value
|
temp_itm_lines[id(itm)] = '# %s' % value
|
||||||
temp_itm_lines[key] = dict_value
|
temp_itm_lines[key] = dict_value
|
||||||
itm_lines.clear()
|
itm_lines.clear()
|
||||||
itm_lines.update(temp_itm_lines)
|
itm_lines.update(temp_itm_lines)
|
||||||
|
@ -199,6 +199,6 @@ class ModuleMeta(PropertyMeta):
|
|||||||
# collect info about parameters and their properties
|
# collect info about parameters and their properties
|
||||||
for param, pobj in cls.accessibles.items():
|
for param, pobj in cls.accessibles.items():
|
||||||
res[param] = {}
|
res[param] = {}
|
||||||
for pn, pv in pobj.__class__.properties.items():
|
for pn, pv in pobj.getProperties().items():
|
||||||
res[param][pn] = pv
|
res[param][pn] = pv
|
||||||
return res
|
return res
|
||||||
|
@ -160,7 +160,7 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
|||||||
if paramobj:
|
if paramobj:
|
||||||
if propname == 'datatype':
|
if propname == 'datatype':
|
||||||
paramobj.setProperty('datatype', get_datatype(cfgdict.pop(k)))
|
paramobj.setProperty('datatype', get_datatype(cfgdict.pop(k)))
|
||||||
elif propname in paramobj.__class__.properties:
|
elif propname in paramobj.getProperties():
|
||||||
paramobj.setProperty(propname, cfgdict.pop(k))
|
paramobj.setProperty(propname, cfgdict.pop(k))
|
||||||
else:
|
else:
|
||||||
raise ConfigError('Module %s: Parameter %r has no property %r!' %
|
raise ConfigError('Module %s: Parameter %r has no property %r!' %
|
||||||
@ -208,8 +208,9 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
|||||||
|
|
||||||
# Modify units AFTER applying the cfgdict
|
# Modify units AFTER applying the cfgdict
|
||||||
for k, v in self.parameters.items():
|
for k, v in self.parameters.items():
|
||||||
if '$' in v.unit:
|
dt = v.datatype
|
||||||
v.unit = v.unit.replace('$', self.parameters['value'].unit)
|
if '$' in dt.unit:
|
||||||
|
dt.setProperty('unit', dt.unit.replace('$', self.parameters['value'].datatype.unit))
|
||||||
|
|
||||||
# 6) check complete configuration of * properties
|
# 6) check complete configuration of * properties
|
||||||
self.checkProperties()
|
self.checkProperties()
|
||||||
|
@ -45,7 +45,10 @@ class Accessible(HasProperties, CountedObj):
|
|||||||
|
|
||||||
def __init__(self, **kwds):
|
def __init__(self, **kwds):
|
||||||
super(Accessible, self).__init__()
|
super(Accessible, self).__init__()
|
||||||
self.properties.update(kwds)
|
# do not use self.properties.update here, as no invalid values should be
|
||||||
|
# assigned to properties, even not before checkProperties
|
||||||
|
for k,v in kwds.items():
|
||||||
|
self.setProperty(k, v)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return '%s_%d(%s)' % (self.__class__.__name__, self.ctr, ',\n\t'.join(
|
return '%s_%d(%s)' % (self.__class__.__name__, self.ctr, ',\n\t'.join(
|
||||||
@ -54,6 +57,8 @@ class Accessible(HasProperties, CountedObj):
|
|||||||
def copy(self):
|
def copy(self):
|
||||||
# return a copy of ourselfs
|
# return a copy of ourselfs
|
||||||
props = dict(self.properties, ctr=self.ctr)
|
props = dict(self.properties, ctr=self.ctr)
|
||||||
|
# deep copy, as datatype might be altered from config
|
||||||
|
props['datatype'] = props['datatype'].copy()
|
||||||
return type(self)(**props)
|
return type(self)(**props)
|
||||||
|
|
||||||
def for_export(self):
|
def for_export(self):
|
||||||
@ -90,8 +95,6 @@ class Parameter(Accessible):
|
|||||||
extname='description', mandatory=True),
|
extname='description', mandatory=True),
|
||||||
'datatype': Property('Datatype of the Parameter', DataTypeType(),
|
'datatype': Property('Datatype of the Parameter', DataTypeType(),
|
||||||
extname='datainfo', mandatory=True),
|
extname='datainfo', mandatory=True),
|
||||||
'unit': Property('[legacy] unit of the parameter. This should now be on the datatype!', StringType(),
|
|
||||||
extname='unit', default=''), # goodie, should be on the datatype!
|
|
||||||
'readonly': Property('Is the Parameter readonly? (vs. changeable via SECoP)', BoolType(),
|
'readonly': Property('Is the Parameter readonly? (vs. changeable via SECoP)', BoolType(),
|
||||||
extname='readonly', default=True),
|
extname='readonly', default=True),
|
||||||
'group': Property('Optional parameter group this parameter belongs to', StringType(),
|
'group': Property('Optional parameter group this parameter belongs to', StringType(),
|
||||||
@ -99,7 +102,7 @@ class Parameter(Accessible):
|
|||||||
'visibility': Property('Optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3),
|
'visibility': Property('Optional visibility hint', EnumType('visibility', user=1, advanced=2, expert=3),
|
||||||
extname='visibility', default=1),
|
extname='visibility', default=1),
|
||||||
'constant': Property('Optional constant value for constant parameters', ValueType(),
|
'constant': Property('Optional constant value for constant parameters', ValueType(),
|
||||||
extname='constant', default=None),
|
extname='constant', default=None, mandatory=False),
|
||||||
'default': Property('Default (startup) value of this parameter if it can not be read from the hardware.',
|
'default': Property('Default (startup) value of this parameter if it can not be read from the hardware.',
|
||||||
ValueType(), export=False, default=None, mandatory=False),
|
ValueType(), export=False, default=None, mandatory=False),
|
||||||
'export': Property('Is this parameter accessible via SECoP? (vs. internal parameter)',
|
'export': Property('Is this parameter accessible via SECoP? (vs. internal parameter)',
|
||||||
@ -110,7 +113,7 @@ class Parameter(Accessible):
|
|||||||
|
|
||||||
value = None
|
value = None
|
||||||
timestamp = None
|
timestamp = None
|
||||||
def __init__(self, description, datatype, ctr=None, **kwds):
|
def __init__(self, description, datatype, ctr=None, unit=None, **kwds):
|
||||||
|
|
||||||
if ctr is not None:
|
if ctr is not None:
|
||||||
self.ctr = ctr
|
self.ctr = ctr
|
||||||
@ -125,6 +128,8 @@ class Parameter(Accessible):
|
|||||||
|
|
||||||
kwds['description'] = description
|
kwds['description'] = description
|
||||||
kwds['datatype'] = datatype
|
kwds['datatype'] = datatype
|
||||||
|
if unit is not None: # for legacy code only
|
||||||
|
datatype.setProperty('unit', unit)
|
||||||
super(Parameter, self).__init__(**kwds)
|
super(Parameter, self).__init__(**kwds)
|
||||||
|
|
||||||
# note: auto-converts True/False to 1/0 which yield the expected
|
# note: auto-converts True/False to 1/0 which yield the expected
|
||||||
@ -138,11 +143,6 @@ class Parameter(Accessible):
|
|||||||
constant = self.datatype(kwds['constant'])
|
constant = self.datatype(kwds['constant'])
|
||||||
self.properties['constant'] = self.datatype.export_value(constant)
|
self.properties['constant'] = self.datatype.export_value(constant)
|
||||||
|
|
||||||
# helper: unit should be set on the datatype, not on the parameter!
|
|
||||||
if self.unit:
|
|
||||||
self.datatype.unit = self.unit
|
|
||||||
self.properties['unit'] = ''
|
|
||||||
|
|
||||||
# internal caching: value and timestamp of last change...
|
# internal caching: value and timestamp of last change...
|
||||||
self.value = self.default
|
self.value = self.default
|
||||||
self.timestamp = 0
|
self.timestamp = 0
|
||||||
@ -150,17 +150,20 @@ class Parameter(Accessible):
|
|||||||
def export_value(self):
|
def export_value(self):
|
||||||
return self.datatype.export_value(self.value)
|
return self.datatype.export_value(self.value)
|
||||||
|
|
||||||
# helpers...
|
def getProperties(self):
|
||||||
def _get_unit_(self):
|
"""get also properties of datatype"""
|
||||||
return self.datatype.unit
|
return {**super().getProperties(), **self.datatype.getProperties()}
|
||||||
|
|
||||||
def _set_unit_(self, unit):
|
def setProperty(self, key, value):
|
||||||
print('DeprecationWarning: setting unit on the parameter is going to be removed')
|
"""set also properties of datatype"""
|
||||||
self.datatype.unit = unit
|
if key in self.__class__.properties:
|
||||||
|
super().setProperty(key, value)
|
||||||
|
else:
|
||||||
|
self.datatype.setProperty(key, value)
|
||||||
|
|
||||||
unit = property(_get_unit_, _set_unit_)
|
def checkProperties(self):
|
||||||
del _get_unit_
|
super().checkProperties()
|
||||||
del _set_unit_
|
self.datatype.checkProperties()
|
||||||
|
|
||||||
|
|
||||||
class UnusedClass:
|
class UnusedClass:
|
||||||
@ -220,6 +223,7 @@ class Override(CountedObj):
|
|||||||
def apply(self, obj):
|
def apply(self, obj):
|
||||||
if isinstance(obj, Accessible):
|
if isinstance(obj, Accessible):
|
||||||
props = obj.properties.copy()
|
props = obj.properties.copy()
|
||||||
|
props['datatype'] = props['datatype'].copy()
|
||||||
if isinstance(obj, Parameter):
|
if isinstance(obj, Parameter):
|
||||||
if 'constant' in self.kwds:
|
if 'constant' in self.kwds:
|
||||||
constant = obj.datatype(self.kwds.pop('constant'))
|
constant = obj.datatype(self.kwds.pop('constant'))
|
||||||
|
@ -24,7 +24,6 @@
|
|||||||
|
|
||||||
from collections import OrderedDict
|
from collections import OrderedDict
|
||||||
|
|
||||||
from secop.datatypes import ValueType, DataType
|
|
||||||
from secop.errors import ProgrammingError, ConfigError
|
from secop.errors import ProgrammingError, ConfigError
|
||||||
|
|
||||||
|
|
||||||
@ -39,7 +38,7 @@ class Property:
|
|||||||
'''
|
'''
|
||||||
# note: this is inteded to be used on base classes.
|
# note: this is inteded to be used on base classes.
|
||||||
# the VALUES of the properties are on the instances!
|
# the VALUES of the properties are on the instances!
|
||||||
def __init__(self, description, datatype, default=None, extname='', export=False, mandatory=False, settable=True):
|
def __init__(self, description, datatype, default=None, extname='', export=False, mandatory=None, settable=True):
|
||||||
if not callable(datatype):
|
if not callable(datatype):
|
||||||
raise ValueError('datatype MUST be a valid DataType or a basic_validator')
|
raise ValueError('datatype MUST be a valid DataType or a basic_validator')
|
||||||
self.description = description
|
self.description = description
|
||||||
@ -47,7 +46,9 @@ class Property:
|
|||||||
self.datatype = datatype
|
self.datatype = datatype
|
||||||
self.extname = extname
|
self.extname = extname
|
||||||
self.export = export or bool(extname)
|
self.export = export or bool(extname)
|
||||||
self.mandatory = mandatory or (default is None and not isinstance(datatype, ValueType))
|
if mandatory is None:
|
||||||
|
mandatory = default is None
|
||||||
|
self.mandatory = mandatory
|
||||||
self.settable = settable or mandatory # settable means settable from the cfg file
|
self.settable = settable or mandatory # settable means settable from the cfg file
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
@ -67,7 +68,7 @@ class Properties(OrderedDict):
|
|||||||
raise ProgrammingError('setting property %r on classes is not supported!' % key)
|
raise ProgrammingError('setting property %r on classes is not supported!' % key)
|
||||||
# make sure, extname is valid if export is True
|
# make sure, extname is valid if export is True
|
||||||
if not value.extname and value.export:
|
if not value.extname and value.export:
|
||||||
value.extname = '_%s' % key # generate custom kex
|
value.extname = '_%s' % key # generate custom key
|
||||||
elif value.extname and not value.export:
|
elif value.extname and not value.export:
|
||||||
value.export = True
|
value.export = True
|
||||||
OrderedDict.__setitem__(self, key, value)
|
OrderedDict.__setitem__(self, key, value)
|
||||||
@ -117,9 +118,11 @@ class PropertyMeta(type):
|
|||||||
class HasProperties(metaclass=PropertyMeta):
|
class HasProperties(metaclass=PropertyMeta):
|
||||||
properties = {}
|
properties = {}
|
||||||
|
|
||||||
def __init__(self, supercall_init=True):
|
def __init__(self):
|
||||||
if supercall_init:
|
super(HasProperties, self).__init__()
|
||||||
super(HasProperties, self).__init__()
|
self.initProperties()
|
||||||
|
|
||||||
|
def initProperties(self):
|
||||||
# store property values in the instance, keep descriptors on the class
|
# store property values in the instance, keep descriptors on the class
|
||||||
self.properties = {}
|
self.properties = {}
|
||||||
# pre-init with properties default value (if any)
|
# pre-init with properties default value (if any)
|
||||||
@ -128,16 +131,25 @@ class HasProperties(metaclass=PropertyMeta):
|
|||||||
self.properties[pn] = po.default
|
self.properties[pn] = po.default
|
||||||
|
|
||||||
def checkProperties(self):
|
def checkProperties(self):
|
||||||
|
"""validates properties and checks for min... <= max..."""
|
||||||
for pn, po in self.__class__.properties.items():
|
for pn, po in self.__class__.properties.items():
|
||||||
if po.export and po.mandatory:
|
if po.export and po.mandatory:
|
||||||
if pn not in self.properties:
|
if pn not in self.properties:
|
||||||
name = getattr(self, 'name', repr(self))
|
name = getattr(self, 'name', repr(self))
|
||||||
raise ConfigError('Property %r of %r needs a value of type %r!' % (pn, name, po.datatype))
|
raise ConfigError('Property %r of %s needs a value of type %r!' % (pn, name, po.datatype))
|
||||||
# apply validator (which may complain further)
|
# apply validator (which may complain further)
|
||||||
self.properties[pn] = po.datatype(self.properties[pn])
|
self.properties[pn] = po.datatype(self.properties[pn])
|
||||||
if 'min' in self.properties and 'max' in self.properties:
|
for pn, po in self.__class__.properties.items():
|
||||||
if self.min > self.max:
|
if pn.startswith('min'):
|
||||||
raise ConfigError('min and max of %r need to fulfil min <= max! (is %r>%r)' % (self, self.min, self.max))
|
maxname = 'max' + pn[3:]
|
||||||
|
minval = self.properties[pn]
|
||||||
|
maxval = self.properties.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
|
||||||
|
|
||||||
def exportProperties(self):
|
def exportProperties(self):
|
||||||
# export properties which have
|
# export properties which have
|
||||||
@ -147,8 +159,10 @@ class HasProperties(metaclass=PropertyMeta):
|
|||||||
for pn, po in self.__class__.properties.items():
|
for pn, po in self.__class__.properties.items():
|
||||||
val = self.properties.get(pn, None)
|
val = self.properties.get(pn, None)
|
||||||
if po.export and (po.mandatory or val != po.default):
|
if po.export and (po.mandatory or val != po.default):
|
||||||
if isinstance(po.datatype, DataType):
|
try:
|
||||||
val = po.datatype.export_value(val)
|
val = po.datatype.export_value(val)
|
||||||
|
except AttributeError:
|
||||||
|
pass # for properties, accept simple datatypes without export_value
|
||||||
res[po.extname] = val
|
res[po.extname] = val
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
@ -200,11 +200,12 @@ class TCPServer(HasProperties, socketserver.ThreadingTCPServer):
|
|||||||
# XXX: create configurables from Metaclass!
|
# XXX: create configurables from Metaclass!
|
||||||
configurables = properties
|
configurables = properties
|
||||||
|
|
||||||
def __init__(self, name, logger, options, srv):
|
def __init__(self, name, logger, options, srv): # pylint: disable=super-init-not-called
|
||||||
self.dispatcher = srv.dispatcher
|
self.dispatcher = srv.dispatcher
|
||||||
self.name = name
|
self.name = name
|
||||||
self.log = logger
|
self.log = logger
|
||||||
HasProperties.__init__(self, supercall_init=False)
|
# do not call HasProperties.__init__, as this will supercall ThreadingTCPServer
|
||||||
|
self.initProperties()
|
||||||
bindto = options.pop('bindto', 'localhost')
|
bindto = options.pop('bindto', 'localhost')
|
||||||
bindport = int(options.pop('bindport', DEF_PORT))
|
bindport = int(options.pop('bindport', DEF_PORT))
|
||||||
detailed_errors = options.pop('detailed_errors', False)
|
detailed_errors = options.pop('detailed_errors', False)
|
||||||
|
@ -26,7 +26,7 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from secop.datatypes import ArrayOf, BLOBType, BoolType, \
|
from secop.datatypes import ArrayOf, BLOBType, BoolType, \
|
||||||
DataType, EnumType, FloatRange, IntRange, ProgrammingError, \
|
DataType, EnumType, FloatRange, IntRange, ProgrammingError, ConfigError, \
|
||||||
ScaledInteger, StringType, TextType, StructOf, TupleOf, get_datatype, CommandType
|
ScaledInteger, StringType, TextType, StructOf, TupleOf, get_datatype, CommandType
|
||||||
|
|
||||||
|
|
||||||
@ -63,11 +63,14 @@ def test_FloatRange():
|
|||||||
dt(13.14 - 10) # raises an error, if resolution is not handled correctly
|
dt(13.14 - 10) # raises an error, if resolution is not handled correctly
|
||||||
assert dt.export_value(-2.718) == -2.718
|
assert dt.export_value(-2.718) == -2.718
|
||||||
assert dt.import_value(-2.718) == -2.718
|
assert dt.import_value(-2.718) == -2.718
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ProgrammingError):
|
||||||
FloatRange('x', 'Y')
|
FloatRange('x', 'Y')
|
||||||
# check that unit can be changed
|
# check that unit can be changed
|
||||||
dt.unit = 'K'
|
dt.setProperty('unit', 'K')
|
||||||
assert dt.export_datatype() == {'type': 'double', 'min':-3.14, 'max':3.14, 'unit': 'K'}
|
assert dt.export_datatype() == {'type': 'double', 'min':-3.14, 'max':3.14, 'unit': 'K'}
|
||||||
|
with pytest.raises(KeyError):
|
||||||
|
dt.setProperty('visibility', 0)
|
||||||
|
dt.setProperty('absolute_resolution', 0)
|
||||||
|
|
||||||
dt = FloatRange()
|
dt = FloatRange()
|
||||||
copytest(dt)
|
copytest(dt)
|
||||||
@ -84,6 +87,14 @@ def test_FloatRange():
|
|||||||
assert dt.format_value(3.14, '') == '3.14'
|
assert dt.format_value(3.14, '') == '3.14'
|
||||||
assert dt.format_value(3.14, '#') == '3.14 #'
|
assert dt.format_value(3.14, '#') == '3.14 #'
|
||||||
|
|
||||||
|
dt.setProperty('min', 1)
|
||||||
|
dt.setProperty('max', 0)
|
||||||
|
with pytest.raises(ConfigError):
|
||||||
|
dt.checkProperties()
|
||||||
|
|
||||||
|
with pytest.raises(ProgrammingError):
|
||||||
|
FloatRange(resolution=1)
|
||||||
|
|
||||||
|
|
||||||
def test_IntRange():
|
def test_IntRange():
|
||||||
dt = IntRange(-3, 3)
|
dt = IntRange(-3, 3)
|
||||||
@ -100,7 +111,7 @@ def test_IntRange():
|
|||||||
dt([19, 'X'])
|
dt([19, 'X'])
|
||||||
dt(1)
|
dt(1)
|
||||||
dt(0)
|
dt(0)
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ProgrammingError):
|
||||||
IntRange('xc', 'Yx')
|
IntRange('xc', 'Yx')
|
||||||
|
|
||||||
dt = IntRange()
|
dt = IntRange()
|
||||||
@ -110,6 +121,11 @@ def test_IntRange():
|
|||||||
assert dt.export_datatype() == {'type': 'int', 'max': 16777216,'min': -16777216}
|
assert dt.export_datatype() == {'type': 'int', 'max': 16777216,'min': -16777216}
|
||||||
assert dt.format_value(42) == '42'
|
assert dt.format_value(42) == '42'
|
||||||
|
|
||||||
|
dt.setProperty('min', 1)
|
||||||
|
dt.setProperty('max', 0)
|
||||||
|
with pytest.raises(ConfigError):
|
||||||
|
dt.checkProperties()
|
||||||
|
|
||||||
def test_ScaledInteger():
|
def test_ScaledInteger():
|
||||||
dt = ScaledInteger(0.01, -3, 3)
|
dt = ScaledInteger(0.01, -3, 3)
|
||||||
copytest(dt)
|
copytest(dt)
|
||||||
@ -128,18 +144,24 @@ def test_ScaledInteger():
|
|||||||
dt(0)
|
dt(0)
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
ScaledInteger('xc', 'Yx')
|
ScaledInteger('xc', 'Yx')
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ProgrammingError):
|
||||||
ScaledInteger(scale=0, minval=1, maxval=2)
|
ScaledInteger(scale=0, minval=1, maxval=2)
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ProgrammingError):
|
||||||
ScaledInteger(scale=-10, minval=1, maxval=2)
|
ScaledInteger(scale=-10, minval=1, maxval=2)
|
||||||
# check that unit can be changed
|
# check that unit can be changed
|
||||||
dt.unit = 'A'
|
dt.setProperty('unit', 'A')
|
||||||
assert dt.export_datatype() == {'type': 'scaled', 'scale':0.01, 'min':-300, 'max':300, 'unit': 'A'}
|
assert dt.export_datatype() == {'type': 'scaled', 'scale':0.01, 'min':-300, 'max':300,
|
||||||
|
'unit': 'A'}
|
||||||
|
|
||||||
assert dt.export_value(0.0001) == int(0)
|
assert dt.export_value(0.0001) == int(0)
|
||||||
assert dt.export_value(2.71819) == int(272)
|
assert dt.export_value(2.71819) == int(272)
|
||||||
assert dt.import_value(272) == 2.72
|
assert dt.import_value(272) == 2.72
|
||||||
|
|
||||||
|
dt.setProperty('scale', 0.1)
|
||||||
|
assert dt.export_datatype() == {'type': 'scaled', 'scale':0.1, 'min':-30, 'max':30,
|
||||||
|
'unit':'A'}
|
||||||
|
assert dt.absolute_resolution == dt.scale
|
||||||
|
|
||||||
dt = ScaledInteger(0.003, 0, 1, unit='X', fmtstr='%.1f',
|
dt = ScaledInteger(0.003, 0, 1, unit='X', fmtstr='%.1f',
|
||||||
absolute_resolution=0.001, relative_resolution=1e-5)
|
absolute_resolution=0.001, relative_resolution=1e-5)
|
||||||
copytest(dt)
|
copytest(dt)
|
||||||
@ -155,6 +177,14 @@ def test_ScaledInteger():
|
|||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
dt(1.004)
|
dt(1.004)
|
||||||
|
|
||||||
|
dt.setProperty('min', 1)
|
||||||
|
dt.setProperty('max', 0)
|
||||||
|
with pytest.raises(ConfigError):
|
||||||
|
dt.checkProperties()
|
||||||
|
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
dt.setProperty('scale', None)
|
||||||
|
|
||||||
|
|
||||||
def test_EnumType():
|
def test_EnumType():
|
||||||
# test constructor catching illegal arguments
|
# test constructor catching illegal arguments
|
||||||
@ -219,6 +249,11 @@ def test_BLOBType():
|
|||||||
dt('abcd')
|
dt('abcd')
|
||||||
assert dt(b'abcd') == b'abcd'
|
assert dt(b'abcd') == b'abcd'
|
||||||
|
|
||||||
|
dt.setProperty('minbytes', 1)
|
||||||
|
dt.setProperty('maxbytes', 0)
|
||||||
|
with pytest.raises(ConfigError):
|
||||||
|
dt.checkProperties()
|
||||||
|
|
||||||
assert dt.export_value(b'abcd') == 'YWJjZA=='
|
assert dt.export_value(b'abcd') == 'YWJjZA=='
|
||||||
assert dt.export_value(b'abcd') == 'YWJjZA=='
|
assert dt.export_value(b'abcd') == 'YWJjZA=='
|
||||||
# assert dt.export_value('abcd') == 'YWJjZA=='
|
# assert dt.export_value('abcd') == 'YWJjZA=='
|
||||||
@ -260,6 +295,11 @@ def test_StringType():
|
|||||||
|
|
||||||
assert dt.format_value('abcd') == "'abcd'"
|
assert dt.format_value('abcd') == "'abcd'"
|
||||||
|
|
||||||
|
dt.setProperty('minchars', 1)
|
||||||
|
dt.setProperty('maxchars', 0)
|
||||||
|
with pytest.raises(ConfigError):
|
||||||
|
dt.checkProperties()
|
||||||
|
|
||||||
|
|
||||||
def test_TextType():
|
def test_TextType():
|
||||||
# test constructor catching illegal arguments
|
# test constructor catching illegal arguments
|
||||||
@ -309,6 +349,9 @@ def test_BoolType():
|
|||||||
assert dt.format_value(0) == "False"
|
assert dt.format_value(0) == "False"
|
||||||
assert dt.format_value(True) == "True"
|
assert dt.format_value(True) == "True"
|
||||||
|
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
# pylint: disable=unexpected-keyword-arg
|
||||||
|
BoolType(unit='K')
|
||||||
|
|
||||||
def test_ArrayOf():
|
def test_ArrayOf():
|
||||||
# test constructor catching illegal arguments
|
# test constructor catching illegal arguments
|
||||||
@ -341,6 +384,17 @@ def test_ArrayOf():
|
|||||||
assert dt.format_value([1,2,3], '') == '[1, 2, 3]'
|
assert dt.format_value([1,2,3], '') == '[1, 2, 3]'
|
||||||
assert dt.format_value([1,2,3], 'Q') == '[1, 2, 3] Q'
|
assert dt.format_value([1,2,3], 'Q') == '[1, 2, 3] Q'
|
||||||
|
|
||||||
|
dt = ArrayOf(FloatRange(unit='K'))
|
||||||
|
assert dt.members.unit == 'K'
|
||||||
|
dt.setProperty('unit', 'mm')
|
||||||
|
with pytest.raises(TypeError):
|
||||||
|
# pylint: disable=unexpected-keyword-arg
|
||||||
|
ArrayOf(BoolType(), unit='K')
|
||||||
|
|
||||||
|
dt.setProperty('minlen', 1)
|
||||||
|
dt.setProperty('maxlen', 0)
|
||||||
|
with pytest.raises(ConfigError):
|
||||||
|
dt.checkProperties()
|
||||||
|
|
||||||
def test_TupleOf():
|
def test_TupleOf():
|
||||||
# test constructor catching illegal arguments
|
# test constructor catching illegal arguments
|
||||||
|
@ -31,7 +31,6 @@ from secop.modules import Communicator, Drivable, Module
|
|||||||
from secop.params import Command, Override, Parameter
|
from secop.params import Command, Override, Parameter
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def test_Communicator():
|
def test_Communicator():
|
||||||
logger = type('LoggerStub', (object,), dict(
|
logger = type('LoggerStub', (object,), dict(
|
||||||
debug = lambda self, *a: print(*a),
|
debug = lambda self, *a: print(*a),
|
||||||
@ -54,11 +53,12 @@ def test_Communicator():
|
|||||||
assert event.is_set() # event should be set immediately
|
assert event.is_set() # event should be set immediately
|
||||||
|
|
||||||
def test_ModuleMeta():
|
def test_ModuleMeta():
|
||||||
|
# pylint: disable=too-many-function-args
|
||||||
newclass1 = ModuleMeta.__new__(ModuleMeta, 'TestDrivable', (Drivable,), {
|
newclass1 = ModuleMeta.__new__(ModuleMeta, 'TestDrivable', (Drivable,), {
|
||||||
"parameters" : {
|
"parameters" : {
|
||||||
'pollinterval': Override(reorder=True),
|
'pollinterval': Override(reorder=True),
|
||||||
'param1' : Parameter('param1', datatype=BoolType(), default=False),
|
'param1' : Parameter('param1', datatype=BoolType(), default=False),
|
||||||
'param2': Parameter('param2', datatype=BoolType(), default=True),
|
'param2': Parameter('param2', datatype=FloatRange(unit='Ohm'), default=True),
|
||||||
"cmd": Command('stuff', argument=BoolType(), result=BoolType())
|
"cmd": Command('stuff', argument=BoolType(), result=BoolType())
|
||||||
},
|
},
|
||||||
"commands": {
|
"commands": {
|
||||||
@ -82,10 +82,12 @@ def test_ModuleMeta():
|
|||||||
sortcheck1 = ['value', 'status', 'target', 'pollinterval',
|
sortcheck1 = ['value', 'status', 'target', 'pollinterval',
|
||||||
'param1', 'param2', 'cmd', 'a1', 'a2', 'cmd2']
|
'param1', 'param2', 'cmd', 'a1', 'a2', 'cmd2']
|
||||||
|
|
||||||
|
# pylint: disable=too-many-function-args
|
||||||
newclass2 = ModuleMeta.__new__(ModuleMeta, 'UpperClass', (newclass1,), {
|
newclass2 = ModuleMeta.__new__(ModuleMeta, 'UpperClass', (newclass1,), {
|
||||||
"parameters": {
|
"parameters": {
|
||||||
'cmd2': Override('another stuff'),
|
'cmd2': Override('another stuff'),
|
||||||
'a1': Override(datatype=FloatRange(), reorder=True),
|
'value': Override(datatype=FloatRange(unit='deg'), reorder=True),
|
||||||
|
'a1': Override(datatype=FloatRange(unit='$/s'), reorder=True),
|
||||||
'b2': Parameter('a2', datatype=BoolType(), default=True),
|
'b2': Parameter('a2', datatype=BoolType(), default=True),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -125,6 +127,25 @@ def test_ModuleMeta():
|
|||||||
# HACK: atm. disabled to fix all other problems first.
|
# HACK: atm. disabled to fix all other problems first.
|
||||||
assert check_order + sorted(check_order)
|
assert check_order + sorted(check_order)
|
||||||
|
|
||||||
|
o1 = newclass1('o1', logger, {'.description':''}, srv)
|
||||||
|
o2 = newclass2('o2', logger, {'.description':''}, srv)
|
||||||
|
assert o2.parameters['a1'].datatype.unit == 'deg/s'
|
||||||
|
o2 = newclass2('o2', logger, {'.description':'', 'value.unit':'mm', 'param2.unit':'mm'}, srv)
|
||||||
|
# check datatype is not shared
|
||||||
|
assert o1.parameters['param2'].datatype.unit == 'Ohm'
|
||||||
|
assert o2.parameters['param2'].datatype.unit == 'mm'
|
||||||
|
# check '$' in unit works properly
|
||||||
|
assert o2.parameters['a1'].datatype.unit == 'mm/s'
|
||||||
|
cfg = newclass2.configurables
|
||||||
|
assert set(cfg.keys()) == {'export', 'group', 'description',
|
||||||
|
'meaning', 'visibility', 'implementation', 'interface_class', 'target', 'stop',
|
||||||
|
'status', 'param1', 'param2', 'cmd', 'a2', 'pollinterval', 'b2', 'cmd2', 'value',
|
||||||
|
'a1'}
|
||||||
|
assert set(cfg['value'].keys()) == {'group', 'export', 'relative_resolution',
|
||||||
|
'visibility', 'unit', 'default', 'optional', 'datatype', 'fmtstr',
|
||||||
|
'absolute_resolution', 'poll', 'max', 'min', 'readonly', 'constant',
|
||||||
|
'description'}
|
||||||
|
|
||||||
# check on the level of classes
|
# check on the level of classes
|
||||||
# this checks newclass1 too, as it is inherited by newclass2
|
# this checks newclass1 too, as it is inherited by newclass2
|
||||||
for baseclass in newclass2.__mro__:
|
for baseclass in newclass2.__mro__:
|
||||||
|
@ -24,10 +24,10 @@
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from secop.datatypes import IntRange, StringType, FloatRange, ValueType
|
from secop.datatypes import IntRange, StringType, FloatRange, ValueType
|
||||||
from secop.errors import ProgrammingError
|
from secop.errors import ProgrammingError, ConfigError
|
||||||
from secop.properties import Property, Properties, HasProperties
|
from secop.properties import Property, Properties, HasProperties
|
||||||
|
|
||||||
|
# args are: datatype, default, extname, export, mandatory, settable
|
||||||
V_test_Property = [
|
V_test_Property = [
|
||||||
[(StringType(), 'default', 'extname', False, False),
|
[(StringType(), 'default', 'extname', False, False),
|
||||||
dict(default='default', extname='extname', export=True, mandatory=False)],
|
dict(default='default', extname='extname', export=True, mandatory=False)],
|
||||||
@ -45,16 +45,16 @@ V_test_Property = [
|
|||||||
dict(default=0, extname='', export=False, mandatory=True)],
|
dict(default=0, extname='', export=False, mandatory=True)],
|
||||||
[(IntRange(), 0, '', False, False),
|
[(IntRange(), 0, '', False, False),
|
||||||
dict(default=0, extname='', export=False, mandatory=False)],
|
dict(default=0, extname='', export=False, mandatory=False)],
|
||||||
[(IntRange(), None, '', False, False),
|
[(IntRange(), None, '', None),
|
||||||
dict(default=0, extname='', export=False, mandatory=True)], # 'normal types + no default -> mandatory
|
dict(default=0, extname='', export=False, mandatory=True)], # mandatory not given, no default -> mandatory
|
||||||
[(ValueType(), None, '', False, False),
|
[(ValueType(), 1, '', False),
|
||||||
dict(default=None, extname='', export=False, mandatory=False)], # 'special type + no default -> NOT mandatory
|
dict(default=1, extname='', export=False, mandatory=False)], # mandatory not given, default given -> NOT mandatory
|
||||||
]
|
]
|
||||||
@pytest.mark.parametrize('args, check', V_test_Property)
|
@pytest.mark.parametrize('args, check', V_test_Property)
|
||||||
def test_Property(args, check):
|
def test_Property(args, check):
|
||||||
p = Property('', *args)
|
p = Property('', *args)
|
||||||
for k,v in check.items():
|
result = {k: getattr(p, k) for k in check}
|
||||||
assert getattr(p, k) == v
|
assert result == check
|
||||||
|
|
||||||
def test_Property_basic():
|
def test_Property_basic():
|
||||||
with pytest.raises(TypeError):
|
with pytest.raises(TypeError):
|
||||||
@ -96,9 +96,24 @@ class cl(c):
|
|||||||
properties = {
|
properties = {
|
||||||
'a' : Property('', IntRange(), 3),
|
'a' : Property('', IntRange(), 3),
|
||||||
'b' : Property('', FloatRange(), 3.14),
|
'b' : Property('', FloatRange(), 3.14),
|
||||||
|
'minabc': Property('', IntRange(), 8),
|
||||||
|
'maxabc': Property('', IntRange(), 9),
|
||||||
|
'minx': Property('', IntRange(), 2),
|
||||||
|
'maxy': Property('', IntRange(), 1),
|
||||||
}
|
}
|
||||||
|
|
||||||
def test_HasProperties():
|
def test_HasProperties():
|
||||||
|
o = c()
|
||||||
|
assert o.properties['a'] == 1
|
||||||
o = cl()
|
o = cl()
|
||||||
assert o.properties['a'] == 3
|
assert o.properties['a'] == 3
|
||||||
assert o.properties['b'] == 3.14
|
assert o.properties['b'] == 3.14
|
||||||
|
|
||||||
|
def test_Property_checks():
|
||||||
|
o = c()
|
||||||
|
o.checkProperties()
|
||||||
|
o = cl()
|
||||||
|
o.checkProperties()
|
||||||
|
o.setProperty('maxabc', 1)
|
||||||
|
with pytest.raises(ConfigError):
|
||||||
|
o.checkProperties()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user