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
|
||||
|
||||
|
||||
import sys
|
||||
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.parse import Parser
|
||||
from secop.properties import HasProperties, Property
|
||||
|
||||
|
||||
# Only export these classes for 'from secop.datatypes import *'
|
||||
@ -44,14 +46,14 @@ __all__ = [
|
||||
# *DEFAULT* limits for IntRange/ScaledIntegers transport serialisation
|
||||
DEFAULT_MIN_INT = -16777216
|
||||
DEFAULT_MAX_INT = 16777216
|
||||
UNLIMITED = 1 << 64 # internal limit for integers, is probably high enough for any datatype size
|
||||
|
||||
Parser = Parser()
|
||||
|
||||
# base class for all DataTypes
|
||||
class DataType:
|
||||
class DataType(HasProperties):
|
||||
IS_COMMAND = False
|
||||
unit = ''
|
||||
fmtstr = '%r'
|
||||
default = None
|
||||
|
||||
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)"""
|
||||
raise NotImplementedError
|
||||
|
||||
def set_prop(self, key, value, default, func=lambda x:x):
|
||||
"""set an optional datatype property and store the default"""
|
||||
self._defaults[key] = default
|
||||
if value is None:
|
||||
value = default
|
||||
setattr(self, key, func(value))
|
||||
def set_properties(self, **kwds):
|
||||
"""init datatype properties"""
|
||||
try:
|
||||
for k,v in kwds.items():
|
||||
self.setProperty(k, v)
|
||||
self.checkProperties()
|
||||
except Exception as e:
|
||||
raise ProgrammingError(str(e))
|
||||
|
||||
def get_info(self, **kwds):
|
||||
"""prepare dict for export or repr
|
||||
|
||||
get a dict with all items different from default
|
||||
plus mandatory keys from kwds"""
|
||||
for k,v in self._defaults.items():
|
||||
value = getattr(self, k)
|
||||
if value != v:
|
||||
kwds[k] = value
|
||||
return kwds
|
||||
result = self.exportProperties()
|
||||
result.update(kwds)
|
||||
return result
|
||||
|
||||
def copy(self):
|
||||
"""make a deep copy of the datatype"""
|
||||
@ -114,29 +116,62 @@ class 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):
|
||||
"""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,
|
||||
absolute_resolution=None, relative_resolution=None,):
|
||||
self._defaults = {}
|
||||
self.set_prop('min', minval, float('-inf'), float)
|
||||
self.set_prop('max', maxval, float('+inf'), float)
|
||||
self.set_prop('unit', unit, '', str)
|
||||
self.set_prop('fmtstr', fmtstr, '%g', str)
|
||||
self.set_prop('absolute_resolution', absolute_resolution, 0.0, float)
|
||||
self.set_prop('relative_resolution', relative_resolution, 1.2e-7, float)
|
||||
def __init__(self, minval=None, maxval=None, **kwds):
|
||||
super().__init__()
|
||||
if minval is not None:
|
||||
kwds['min'] = minval
|
||||
if maxval is not None:
|
||||
kwds['max'] = maxval
|
||||
self.set_properties(**kwds)
|
||||
|
||||
def checkProperties(self):
|
||||
self.default = 0 if self.min <= 0 <= self.max else self.min
|
||||
|
||||
# check values
|
||||
if self.min > self.max:
|
||||
raise BadValueError('max must be larger then min!')
|
||||
super().checkProperties()
|
||||
if '%' not in self.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')
|
||||
raise ConfigError('Invalid fmtstr!')
|
||||
|
||||
def export_datatype(self):
|
||||
return self.get_info(type='double')
|
||||
@ -180,32 +215,34 @@ class FloatRange(DataType):
|
||||
return self.fmtstr % value
|
||||
|
||||
|
||||
|
||||
class IntRange(DataType):
|
||||
"""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):
|
||||
self.min = DEFAULT_MIN_INT if minval is None else int(minval)
|
||||
self.max = DEFAULT_MAX_INT if maxval is None else int(maxval)
|
||||
self.default = 0 if self.min <= 0 <= self.max else self.min
|
||||
# a unit on an int is now allowed in SECoP, but do we need them in Frappy?
|
||||
# self.set_prop('unit', unit, '', str)
|
||||
super().__init__()
|
||||
self.set_properties(min=DEFAULT_MIN_INT if minval is None else minval,
|
||||
max=DEFAULT_MAX_INT if maxval is None else maxval)
|
||||
|
||||
# check values
|
||||
if self.min > self.max:
|
||||
raise BadValueError('Max must be larger then min!')
|
||||
def checkProperties(self):
|
||||
self.default = 0 if self.min <= 0 <= self.max else self.min
|
||||
super().checkProperties()
|
||||
|
||||
def export_datatype(self):
|
||||
return dict(type='int', min=self.min, max=self.max)
|
||||
return self.get_info(type='int')
|
||||
|
||||
def __call__(self, value):
|
||||
try:
|
||||
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' %
|
||||
(value, self.min, self.max or 0))
|
||||
if value > self.max:
|
||||
raise BadValueError('%r should be an int between %d and %d' %
|
||||
(value, self.min or 0, self.max))
|
||||
(value, self.min, self.max))
|
||||
return value
|
||||
except Exception:
|
||||
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)
|
||||
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,
|
||||
absolute_resolution=None, relative_resolution=None,):
|
||||
self._defaults = {}
|
||||
self.scale = float(scale)
|
||||
if not self.scale > 0:
|
||||
raise BadValueError('Scale MUST be positive!')
|
||||
self.set_prop('unit', unit, '', str)
|
||||
self.set_prop('fmtstr', fmtstr, '%g', str)
|
||||
self.set_prop('absolute_resolution', absolute_resolution, self.scale, float)
|
||||
self.set_prop('relative_resolution', relative_resolution, 1.2e-7, float)
|
||||
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,
|
||||
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,
|
||||
**kwds)
|
||||
|
||||
self.min = DEFAULT_MIN_INT * self.scale if minval is None else float(minval)
|
||||
self.max = DEFAULT_MAX_INT * self.scale if maxval is None else float(maxval)
|
||||
def checkProperties(self):
|
||||
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:
|
||||
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
|
||||
# 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):
|
||||
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),
|
||||
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)
|
||||
|
||||
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),
|
||||
max = int((self.max + self.scale * 0.5) // self.scale))
|
||||
return 'ScaledInteger(%s)' % (', '.join('%s=%r' % kv for kv in hints.items()))
|
||||
@ -313,6 +368,7 @@ class ScaledInteger(DataType):
|
||||
class EnumType(DataType):
|
||||
|
||||
def __init__(self, enum_or_name='', **kwds):
|
||||
super().__init__()
|
||||
if 'members' in kwds:
|
||||
kwds = dict(kwds)
|
||||
kwds.update(kwds['members'])
|
||||
@ -353,25 +409,27 @@ class EnumType(DataType):
|
||||
|
||||
|
||||
class BLOBType(DataType):
|
||||
minbytes = 0
|
||||
maxbytes = None
|
||||
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),
|
||||
}
|
||||
|
||||
def __init__(self, minbytes=0, maxbytes=None):
|
||||
super().__init__()
|
||||
# if only one argument is given, use exactly that many bytes
|
||||
# if nothing is given, default to 255
|
||||
if maxbytes is None:
|
||||
maxbytes = minbytes or 255
|
||||
self._defaults = {}
|
||||
self.set_prop('minbytes', minbytes, 0, int)
|
||||
self.maxbytes = int(maxbytes)
|
||||
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.set_properties(minbytes=minbytes, maxbytes=maxbytes)
|
||||
|
||||
def checkProperties(self):
|
||||
self.default = b'\0' * self.minbytes
|
||||
super().checkProperties()
|
||||
|
||||
def export_datatype(self):
|
||||
return self.get_info(type='blob', maxbytes=self.maxbytes)
|
||||
return self.get_info(type='blob')
|
||||
|
||||
def __repr__(self):
|
||||
return 'BLOBType(%d, %d)' % (self.minbytes, self.maxbytes)
|
||||
@ -407,20 +465,24 @@ class BLOBType(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:
|
||||
maxchars = minchars or self.MAXCHARS
|
||||
self._defaults = {}
|
||||
self.set_prop('minchars', minchars, 0, int)
|
||||
self.set_prop('maxchars', maxchars, self.MAXCHARS, int)
|
||||
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!')
|
||||
maxchars = minchars or UNLIMITED
|
||||
self.set_properties(minchars=minchars, maxchars=maxchars, **kwds)
|
||||
|
||||
def checkProperties(self):
|
||||
self.default = ' ' * self.minchars
|
||||
super().checkProperties()
|
||||
|
||||
def export_datatype(self):
|
||||
return self.get_info(type='string')
|
||||
@ -455,7 +517,6 @@ class StringType(DataType):
|
||||
|
||||
def import_value(self, value):
|
||||
"""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)
|
||||
|
||||
def from_string(self, text):
|
||||
@ -473,7 +534,7 @@ class StringType(DataType):
|
||||
class TextType(StringType):
|
||||
def __init__(self, maxchars=None):
|
||||
if maxchars is None:
|
||||
maxchars = self.MAXCHARS
|
||||
maxchars = UNLIMITED
|
||||
super(TextType, self).__init__(0, maxchars)
|
||||
|
||||
def __repr__(self):
|
||||
@ -484,7 +545,6 @@ class TextType(StringType):
|
||||
return TextType(self.maxchars)
|
||||
|
||||
|
||||
# Bool is a special enum
|
||||
class BoolType(DataType):
|
||||
default = False
|
||||
|
||||
@ -504,7 +564,7 @@ class BoolType(DataType):
|
||||
|
||||
def export_value(self, value):
|
||||
"""returns a python object fit for serialisation"""
|
||||
return bool(self(value))
|
||||
return self(value)
|
||||
|
||||
def import_value(self, value):
|
||||
"""returns a python object from serialisation"""
|
||||
@ -514,21 +574,26 @@ class BoolType(DataType):
|
||||
value = text
|
||||
return self(value)
|
||||
|
||||
|
||||
def format_value(self, value, unit=None):
|
||||
return repr(bool(value))
|
||||
|
||||
Stub.fix_datatypes()
|
||||
|
||||
#
|
||||
# nested types
|
||||
#
|
||||
|
||||
|
||||
class ArrayOf(DataType):
|
||||
minlen = None
|
||||
maxlen = None
|
||||
members = None
|
||||
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),
|
||||
}
|
||||
|
||||
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):
|
||||
raise BadValueError(
|
||||
'ArrayOf only works with a DataType as first argument!')
|
||||
@ -537,18 +602,22 @@ class ArrayOf(DataType):
|
||||
if maxlen is None:
|
||||
maxlen = minlen or 100
|
||||
self.members = members
|
||||
if unit:
|
||||
self.members.unit = unit
|
||||
self.set_properties(minlen=minlen, maxlen=maxlen)
|
||||
|
||||
self.minlen = int(minlen)
|
||||
self.maxlen = int(maxlen)
|
||||
if self.minlen < 0:
|
||||
raise BadValueError('sizes must be > 0')
|
||||
if self.maxlen < 1:
|
||||
raise BadValueError('Maximum size must be >= 1!')
|
||||
if self.minlen > self.maxlen:
|
||||
raise BadValueError('maxlen must be bigger than or equal to minlen!')
|
||||
self.default = [members.default] * self.minlen
|
||||
def checkProperties(self):
|
||||
self.default = [self.members.default] * self.minlen
|
||||
super().checkProperties()
|
||||
|
||||
def getProperties(self):
|
||||
"""get also properties of members"""
|
||||
return {**super().getProperties(), **self.members.getProperties()}
|
||||
|
||||
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):
|
||||
return dict(type='array', minlen=self.minlen, maxlen=self.maxlen,
|
||||
@ -600,6 +669,7 @@ class ArrayOf(DataType):
|
||||
class TupleOf(DataType):
|
||||
|
||||
def __init__(self, *members):
|
||||
super().__init__()
|
||||
if not members:
|
||||
raise BadValueError('Empty tuples are not allowed!')
|
||||
for subtype in members:
|
||||
@ -651,6 +721,7 @@ class TupleOf(DataType):
|
||||
class StructOf(DataType):
|
||||
|
||||
def __init__(self, optional=None, **members):
|
||||
super().__init__()
|
||||
self.members = members
|
||||
if not members:
|
||||
raise BadValueError('Empty structs are not allowed!')
|
||||
@ -724,10 +795,9 @@ class StructOf(DataType):
|
||||
|
||||
class CommandType(DataType):
|
||||
IS_COMMAND = True
|
||||
argument = None
|
||||
result = None
|
||||
|
||||
def __init__(self, argument=None, result=None):
|
||||
super().__init__()
|
||||
if argument is not None:
|
||||
if not isinstance(argument, DataType):
|
||||
raise BadValueError('CommandType: Argument type must be a DataType!')
|
||||
@ -773,7 +843,7 @@ class CommandType(DataType):
|
||||
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):
|
||||
def __call__(self, value):
|
||||
"""check if given value (a python obj) is a valid datatype
|
||||
@ -816,11 +886,13 @@ class ValueType(DataType):
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class NoneOr(DataType):
|
||||
"""validates a None or smth. else"""
|
||||
default = None
|
||||
|
||||
def __init__(self, other):
|
||||
super().__init__()
|
||||
self.other = other
|
||||
|
||||
def __call__(self, value):
|
||||
@ -834,6 +906,7 @@ class NoneOr(DataType):
|
||||
|
||||
class OrType(DataType):
|
||||
def __init__(self, *types):
|
||||
super().__init__()
|
||||
self.types = types
|
||||
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)
|
||||
try:
|
||||
return DATATYPES[base](**args)
|
||||
except (TypeError, AttributeError, KeyError):
|
||||
raise BadValueError('invalid data descriptor: %r' % json)
|
||||
except Exception as e:
|
||||
raise BadValueError('invalid data descriptor: %r (%s)' % (json, str(e)))
|
||||
|
@ -57,22 +57,22 @@ def write_config(file_name, tree_widget):
|
||||
blank_lines += 1
|
||||
value = value.replace('\n\n', '\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)
|
||||
# TODO params and props
|
||||
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:
|
||||
prop_name = '.%s' % itm.name
|
||||
if par.kind == PARAMETER:
|
||||
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:
|
||||
temp_itm_lines = OrderedDict()
|
||||
for key, dict_value in itm_lines.items():
|
||||
if key == par:
|
||||
if key == id(par):
|
||||
value = value.replace('\n', '\n# ')
|
||||
temp_itm_lines[itm] = '# %s' % value
|
||||
temp_itm_lines[id(itm)] = '# %s' % value
|
||||
temp_itm_lines[key] = dict_value
|
||||
itm_lines.clear()
|
||||
itm_lines.update(temp_itm_lines)
|
||||
|
@ -199,6 +199,6 @@ class ModuleMeta(PropertyMeta):
|
||||
# collect info about parameters and their properties
|
||||
for param, pobj in cls.accessibles.items():
|
||||
res[param] = {}
|
||||
for pn, pv in pobj.__class__.properties.items():
|
||||
for pn, pv in pobj.getProperties().items():
|
||||
res[param][pn] = pv
|
||||
return res
|
||||
|
@ -160,7 +160,7 @@ class Module(HasProperties, metaclass=ModuleMeta):
|
||||
if paramobj:
|
||||
if propname == 'datatype':
|
||||
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))
|
||||
else:
|
||||
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
|
||||
for k, v in self.parameters.items():
|
||||
if '$' in v.unit:
|
||||
v.unit = v.unit.replace('$', self.parameters['value'].unit)
|
||||
dt = v.datatype
|
||||
if '$' in dt.unit:
|
||||
dt.setProperty('unit', dt.unit.replace('$', self.parameters['value'].datatype.unit))
|
||||
|
||||
# 6) check complete configuration of * properties
|
||||
self.checkProperties()
|
||||
|
@ -45,7 +45,10 @@ class Accessible(HasProperties, CountedObj):
|
||||
|
||||
def __init__(self, **kwds):
|
||||
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):
|
||||
return '%s_%d(%s)' % (self.__class__.__name__, self.ctr, ',\n\t'.join(
|
||||
@ -54,6 +57,8 @@ class Accessible(HasProperties, CountedObj):
|
||||
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)(**props)
|
||||
|
||||
def for_export(self):
|
||||
@ -90,8 +95,6 @@ class Parameter(Accessible):
|
||||
extname='description', mandatory=True),
|
||||
'datatype': Property('Datatype of the Parameter', DataTypeType(),
|
||||
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(),
|
||||
extname='readonly', default=True),
|
||||
'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),
|
||||
extname='visibility', default=1),
|
||||
'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.',
|
||||
ValueType(), export=False, default=None, mandatory=False),
|
||||
'export': Property('Is this parameter accessible via SECoP? (vs. internal parameter)',
|
||||
@ -110,7 +113,7 @@ class Parameter(Accessible):
|
||||
|
||||
value = 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:
|
||||
self.ctr = ctr
|
||||
@ -125,6 +128,8 @@ class Parameter(Accessible):
|
||||
|
||||
kwds['description'] = description
|
||||
kwds['datatype'] = datatype
|
||||
if unit is not None: # for legacy code only
|
||||
datatype.setProperty('unit', unit)
|
||||
super(Parameter, self).__init__(**kwds)
|
||||
|
||||
# note: auto-converts True/False to 1/0 which yield the expected
|
||||
@ -138,11 +143,6 @@ class Parameter(Accessible):
|
||||
constant = self.datatype(kwds['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...
|
||||
self.value = self.default
|
||||
self.timestamp = 0
|
||||
@ -150,17 +150,20 @@ class Parameter(Accessible):
|
||||
def export_value(self):
|
||||
return self.datatype.export_value(self.value)
|
||||
|
||||
# helpers...
|
||||
def _get_unit_(self):
|
||||
return self.datatype.unit
|
||||
def getProperties(self):
|
||||
"""get also properties of datatype"""
|
||||
return {**super().getProperties(), **self.datatype.getProperties()}
|
||||
|
||||
def _set_unit_(self, unit):
|
||||
print('DeprecationWarning: setting unit on the parameter is going to be removed')
|
||||
self.datatype.unit = unit
|
||||
def setProperty(self, key, value):
|
||||
"""set also properties of datatype"""
|
||||
if key in self.__class__.properties:
|
||||
super().setProperty(key, value)
|
||||
else:
|
||||
self.datatype.setProperty(key, value)
|
||||
|
||||
unit = property(_get_unit_, _set_unit_)
|
||||
del _get_unit_
|
||||
del _set_unit_
|
||||
def checkProperties(self):
|
||||
super().checkProperties()
|
||||
self.datatype.checkProperties()
|
||||
|
||||
|
||||
class UnusedClass:
|
||||
@ -220,6 +223,7 @@ class Override(CountedObj):
|
||||
def apply(self, obj):
|
||||
if isinstance(obj, Accessible):
|
||||
props = obj.properties.copy()
|
||||
props['datatype'] = props['datatype'].copy()
|
||||
if isinstance(obj, Parameter):
|
||||
if 'constant' in self.kwds:
|
||||
constant = obj.datatype(self.kwds.pop('constant'))
|
||||
|
@ -24,7 +24,6 @@
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
from secop.datatypes import ValueType, DataType
|
||||
from secop.errors import ProgrammingError, ConfigError
|
||||
|
||||
|
||||
@ -39,7 +38,7 @@ class Property:
|
||||
'''
|
||||
# note: this is inteded 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=False, settable=True):
|
||||
def __init__(self, description, datatype, default=None, extname='', export=False, mandatory=None, settable=True):
|
||||
if not callable(datatype):
|
||||
raise ValueError('datatype MUST be a valid DataType or a basic_validator')
|
||||
self.description = description
|
||||
@ -47,7 +46,9 @@ class Property:
|
||||
self.datatype = datatype
|
||||
self.extname = 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
|
||||
|
||||
def __repr__(self):
|
||||
@ -67,7 +68,7 @@ class Properties(OrderedDict):
|
||||
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 kex
|
||||
value.extname = '_%s' % key # generate custom key
|
||||
elif value.extname and not value.export:
|
||||
value.export = True
|
||||
OrderedDict.__setitem__(self, key, value)
|
||||
@ -117,9 +118,11 @@ class PropertyMeta(type):
|
||||
class HasProperties(metaclass=PropertyMeta):
|
||||
properties = {}
|
||||
|
||||
def __init__(self, supercall_init=True):
|
||||
if supercall_init:
|
||||
super(HasProperties, self).__init__()
|
||||
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)
|
||||
@ -128,16 +131,25 @@ class HasProperties(metaclass=PropertyMeta):
|
||||
self.properties[pn] = po.default
|
||||
|
||||
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:
|
||||
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)
|
||||
self.properties[pn] = po.datatype(self.properties[pn])
|
||||
if 'min' in self.properties and 'max' in self.properties:
|
||||
if self.min > self.max:
|
||||
raise ConfigError('min and max of %r need to fulfil min <= max! (is %r>%r)' % (self, self.min, self.max))
|
||||
for pn, po in self.__class__.properties.items():
|
||||
if pn.startswith('min'):
|
||||
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):
|
||||
# export properties which have
|
||||
@ -147,8 +159,10 @@ class HasProperties(metaclass=PropertyMeta):
|
||||
for pn, po in self.__class__.properties.items():
|
||||
val = self.properties.get(pn, None)
|
||||
if po.export and (po.mandatory or val != po.default):
|
||||
if isinstance(po.datatype, DataType):
|
||||
try:
|
||||
val = po.datatype.export_value(val)
|
||||
except AttributeError:
|
||||
pass # for properties, accept simple datatypes without export_value
|
||||
res[po.extname] = val
|
||||
return res
|
||||
|
||||
|
@ -200,11 +200,12 @@ class TCPServer(HasProperties, socketserver.ThreadingTCPServer):
|
||||
# XXX: create configurables from Metaclass!
|
||||
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.name = name
|
||||
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')
|
||||
bindport = int(options.pop('bindport', DEF_PORT))
|
||||
detailed_errors = options.pop('detailed_errors', False)
|
||||
|
@ -26,7 +26,7 @@
|
||||
import pytest
|
||||
|
||||
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
|
||||
|
||||
|
||||
@ -63,11 +63,14 @@ def test_FloatRange():
|
||||
dt(13.14 - 10) # raises an error, if resolution is not handled correctly
|
||||
assert dt.export_value(-2.718) == -2.718
|
||||
assert dt.import_value(-2.718) == -2.718
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ProgrammingError):
|
||||
FloatRange('x', 'Y')
|
||||
# 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'}
|
||||
with pytest.raises(KeyError):
|
||||
dt.setProperty('visibility', 0)
|
||||
dt.setProperty('absolute_resolution', 0)
|
||||
|
||||
dt = FloatRange()
|
||||
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 #'
|
||||
|
||||
dt.setProperty('min', 1)
|
||||
dt.setProperty('max', 0)
|
||||
with pytest.raises(ConfigError):
|
||||
dt.checkProperties()
|
||||
|
||||
with pytest.raises(ProgrammingError):
|
||||
FloatRange(resolution=1)
|
||||
|
||||
|
||||
def test_IntRange():
|
||||
dt = IntRange(-3, 3)
|
||||
@ -100,7 +111,7 @@ def test_IntRange():
|
||||
dt([19, 'X'])
|
||||
dt(1)
|
||||
dt(0)
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ProgrammingError):
|
||||
IntRange('xc', 'Yx')
|
||||
|
||||
dt = IntRange()
|
||||
@ -110,6 +121,11 @@ def test_IntRange():
|
||||
assert dt.export_datatype() == {'type': 'int', 'max': 16777216,'min': -16777216}
|
||||
assert dt.format_value(42) == '42'
|
||||
|
||||
dt.setProperty('min', 1)
|
||||
dt.setProperty('max', 0)
|
||||
with pytest.raises(ConfigError):
|
||||
dt.checkProperties()
|
||||
|
||||
def test_ScaledInteger():
|
||||
dt = ScaledInteger(0.01, -3, 3)
|
||||
copytest(dt)
|
||||
@ -128,18 +144,24 @@ def test_ScaledInteger():
|
||||
dt(0)
|
||||
with pytest.raises(ValueError):
|
||||
ScaledInteger('xc', 'Yx')
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ProgrammingError):
|
||||
ScaledInteger(scale=0, minval=1, maxval=2)
|
||||
with pytest.raises(ValueError):
|
||||
with pytest.raises(ProgrammingError):
|
||||
ScaledInteger(scale=-10, minval=1, maxval=2)
|
||||
# check that unit can be changed
|
||||
dt.unit = 'A'
|
||||
assert dt.export_datatype() == {'type': 'scaled', 'scale':0.01, 'min':-300, 'max':300, 'unit': 'A'}
|
||||
dt.setProperty('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(2.71819) == int(272)
|
||||
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',
|
||||
absolute_resolution=0.001, relative_resolution=1e-5)
|
||||
copytest(dt)
|
||||
@ -155,6 +177,14 @@ def test_ScaledInteger():
|
||||
with pytest.raises(ValueError):
|
||||
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():
|
||||
# test constructor catching illegal arguments
|
||||
@ -219,6 +249,11 @@ def test_BLOBType():
|
||||
dt('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('abcd') == 'YWJjZA=='
|
||||
@ -260,6 +295,11 @@ def test_StringType():
|
||||
|
||||
assert dt.format_value('abcd') == "'abcd'"
|
||||
|
||||
dt.setProperty('minchars', 1)
|
||||
dt.setProperty('maxchars', 0)
|
||||
with pytest.raises(ConfigError):
|
||||
dt.checkProperties()
|
||||
|
||||
|
||||
def test_TextType():
|
||||
# test constructor catching illegal arguments
|
||||
@ -309,6 +349,9 @@ def test_BoolType():
|
||||
assert dt.format_value(0) == "False"
|
||||
assert dt.format_value(True) == "True"
|
||||
|
||||
with pytest.raises(TypeError):
|
||||
# pylint: disable=unexpected-keyword-arg
|
||||
BoolType(unit='K')
|
||||
|
||||
def test_ArrayOf():
|
||||
# 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], '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():
|
||||
# test constructor catching illegal arguments
|
||||
|
@ -31,7 +31,6 @@ from secop.modules import Communicator, Drivable, Module
|
||||
from secop.params import Command, Override, Parameter
|
||||
|
||||
|
||||
|
||||
def test_Communicator():
|
||||
logger = type('LoggerStub', (object,), dict(
|
||||
debug = lambda self, *a: print(*a),
|
||||
@ -54,11 +53,12 @@ def test_Communicator():
|
||||
assert event.is_set() # event should be set immediately
|
||||
|
||||
def test_ModuleMeta():
|
||||
# pylint: disable=too-many-function-args
|
||||
newclass1 = ModuleMeta.__new__(ModuleMeta, 'TestDrivable', (Drivable,), {
|
||||
"parameters" : {
|
||||
'pollinterval': Override(reorder=True),
|
||||
'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())
|
||||
},
|
||||
"commands": {
|
||||
@ -82,10 +82,12 @@ def test_ModuleMeta():
|
||||
sortcheck1 = ['value', 'status', 'target', 'pollinterval',
|
||||
'param1', 'param2', 'cmd', 'a1', 'a2', 'cmd2']
|
||||
|
||||
# pylint: disable=too-many-function-args
|
||||
newclass2 = ModuleMeta.__new__(ModuleMeta, 'UpperClass', (newclass1,), {
|
||||
"parameters": {
|
||||
'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),
|
||||
},
|
||||
})
|
||||
@ -125,6 +127,25 @@ def test_ModuleMeta():
|
||||
# HACK: atm. disabled to fix all other problems first.
|
||||
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
|
||||
# this checks newclass1 too, as it is inherited by newclass2
|
||||
for baseclass in newclass2.__mro__:
|
||||
|
@ -24,10 +24,10 @@
|
||||
import pytest
|
||||
|
||||
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
|
||||
|
||||
|
||||
# args are: datatype, default, extname, export, mandatory, settable
|
||||
V_test_Property = [
|
||||
[(StringType(), 'default', 'extname', False, 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)],
|
||||
[(IntRange(), 0, '', False, False),
|
||||
dict(default=0, extname='', export=False, mandatory=False)],
|
||||
[(IntRange(), None, '', False, False),
|
||||
dict(default=0, extname='', export=False, mandatory=True)], # 'normal types + no default -> mandatory
|
||||
[(ValueType(), None, '', False, False),
|
||||
dict(default=None, extname='', export=False, mandatory=False)], # 'special type + no default -> NOT mandatory
|
||||
[(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
|
||||
]
|
||||
@pytest.mark.parametrize('args, check', V_test_Property)
|
||||
def test_Property(args, check):
|
||||
p = Property('', *args)
|
||||
for k,v in check.items():
|
||||
assert getattr(p, k) == v
|
||||
result = {k: getattr(p, k) for k in check}
|
||||
assert result == check
|
||||
|
||||
def test_Property_basic():
|
||||
with pytest.raises(TypeError):
|
||||
@ -96,9 +96,24 @@ 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),
|
||||
}
|
||||
|
||||
def test_HasProperties():
|
||||
o = c()
|
||||
assert o.properties['a'] == 1
|
||||
o = cl()
|
||||
assert o.properties['a'] == 3
|
||||
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