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:
zolliker 2019-10-11 15:11:04 +02:00
parent 7688abfc8d
commit f4d572966c
10 changed files with 356 additions and 173 deletions

View File

@ -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)))

View File

@ -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)

View File

@ -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

View File

@ -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()

View File

@ -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'))

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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__:

View File

@ -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()