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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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