as_json on FloatRange and ScaledInteger must be a property
as the unit property may be set in the config file, the datatype might change after creation. This proposed implementation changes the internal name of *_precision to its full name, as this is easier for the way it is done. IntRange is not yet modified, as anyway 'unit' (and 'fmtstr') should not be applied to it, this is not forseen in the standard. Questions for further work: - should we also allow to configure 'fmtstr'? - should it be allowed to change min, max from configuration? - if yes, what is the proper way to do it? Change-Id: I1fda7e8274109fdcca3c792c0a6e3dc6664cc45e Reviewed-on: https://forge.frm2.tum.de/review/20304 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:
@@ -97,48 +97,40 @@ class DataType(object):
|
|||||||
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 setprop(self, key, value, default, func=lambda x:x):
|
||||||
|
"""set a datatype property and store the default"""
|
||||||
|
self._defaults[key] = default
|
||||||
|
if value is None:
|
||||||
|
value = default
|
||||||
|
setattr(self, key, func(value))
|
||||||
|
|
||||||
class FloatRange(DataType):
|
class FloatRange(DataType):
|
||||||
"""Restricted float type"""
|
"""Restricted float type"""
|
||||||
|
|
||||||
def __init__(self, minval=None, maxval=None, unit=u'', fmtstr=u'',
|
def __init__(self, minval=None, maxval=None, unit=None, fmtstr=None,
|
||||||
absolute_precision=None, relative_precision=None,):
|
absolute_precision=None, relative_precision=None,):
|
||||||
# store hints
|
self._defaults = {}
|
||||||
self.hints = {}
|
self.setprop('min', minval, float(u'-inf'), float)
|
||||||
self.unit = unicode(unit)
|
self.setprop('max', maxval, float(u'+inf'), float)
|
||||||
self.fmtstr = unicode(fmtstr or u'%g')
|
self.setprop('unit', unit, u'', unicode)
|
||||||
self.abs_prec = float(absolute_precision or 0.0)
|
self.setprop('fmtstr', fmtstr, u'%g', unicode)
|
||||||
self.rel_prec = float(relative_precision or 1.2e-7)
|
self.setprop('absolute_precision', absolute_precision, 0.0, float)
|
||||||
|
self.setprop('relative_precision', relative_precision, 1.2e-7, float)
|
||||||
# store values for the validator
|
|
||||||
self.min = float(u'-inf') if minval is None else float(minval)
|
|
||||||
self.max = float(u'+inf') if maxval is None else float(maxval)
|
|
||||||
|
|
||||||
# check values
|
# check values
|
||||||
if self.min > self.max:
|
if self.min > self.max:
|
||||||
raise ValueError(u'Max must be larger then min!')
|
raise ValueError(u'Max must be larger then min!')
|
||||||
if '%' not in self.fmtstr:
|
if '%' not in self.fmtstr:
|
||||||
raise ValueError(u'Invalid fmtstr!')
|
raise ValueError(u'Invalid fmtstr!')
|
||||||
if self.abs_prec < 0:
|
if self.absolute_precision < 0:
|
||||||
raise ValueError(u'absolute_precision MUST be >=0')
|
raise ValueError(u'absolute_precision MUST be >=0')
|
||||||
if self.rel_prec < 0:
|
if self.relative_precision < 0:
|
||||||
raise ValueError(u'relative_precision MUST be >=0')
|
raise ValueError(u'relative_precision MUST be >=0')
|
||||||
|
|
||||||
info = {}
|
@property
|
||||||
if self.min != float(u'-inf'):
|
def as_json(self):
|
||||||
info[u'min'] = self.min
|
return [u'double', {k: getattr(self, k) for k, v in self._defaults.items()
|
||||||
if self.max != float(u'inf'):
|
if v != getattr(self, k)}]
|
||||||
info[u'max'] = self.max
|
|
||||||
if unit:
|
|
||||||
self.hints[u'unit'] = self.unit
|
|
||||||
if fmtstr:
|
|
||||||
self.hints[u'fmtstr'] = self.fmtstr
|
|
||||||
if absolute_precision is not None:
|
|
||||||
self.hints[u'absolute_precision'] = self.abs_prec
|
|
||||||
if relative_precision is not None:
|
|
||||||
self.hints[u'relative_precision'] = self.rel_prec
|
|
||||||
info.update(self.hints)
|
|
||||||
self.as_json = [u'double', info]
|
|
||||||
|
|
||||||
def validate(self, value):
|
def validate(self, value):
|
||||||
try:
|
try:
|
||||||
@@ -159,11 +151,7 @@ class FloatRange(DataType):
|
|||||||
(value, self.min, self.max))
|
(value, self.min, self.max))
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
items = [] if self.max or self.min is None else \
|
items = [u'%s=%r' % (k,v) for k,v in self.as_json[1].items()]
|
||||||
[u'-inf' if self.min == float(u'-inf') else self.fmtstr % self.min,
|
|
||||||
u'inf' if self.max == float(u'inf') else self.fmtstr % self.max]
|
|
||||||
for k,v in self.hints.items():
|
|
||||||
items.append(u'%s=%r' % (k,v))
|
|
||||||
return u'FloatRange(%s)' % (', '.join(items))
|
return u'FloatRange(%s)' % (', '.join(items))
|
||||||
|
|
||||||
def export_value(self, value):
|
def export_value(self, value):
|
||||||
@@ -189,29 +177,17 @@ class FloatRange(DataType):
|
|||||||
class IntRange(DataType):
|
class IntRange(DataType):
|
||||||
"""Restricted int type"""
|
"""Restricted int type"""
|
||||||
|
|
||||||
def __init__(self, minval=None, maxval=None, fmtstr=u'%d', unit=u''):
|
def __init__(self, minval=None, maxval=None):
|
||||||
self.hints = {}
|
|
||||||
self.fmtstr = unicode(fmtstr)
|
|
||||||
self.unit = unicode(unit)
|
|
||||||
self.min = DEFAULT_MIN_INT if minval is None else int(minval)
|
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.max = DEFAULT_MAX_INT if maxval is None else int(maxval)
|
||||||
|
|
||||||
# check values
|
# check values
|
||||||
if self.min > self.max:
|
if self.min > self.max:
|
||||||
raise ValueError(u'Max must be larger then min!')
|
raise ValueError(u'Max must be larger then min!')
|
||||||
if '%' not in self.fmtstr:
|
|
||||||
raise ValueError(u'Invalid fmtstr!')
|
|
||||||
|
|
||||||
info = {}
|
@property
|
||||||
self.hints = {}
|
def as_json(self):
|
||||||
info[u'min'] = self.min
|
return [u'int', {"min": self.min, "max": self.max}]
|
||||||
info[u'max'] = self.max
|
|
||||||
if unit:
|
|
||||||
self.hints[u'unit'] = self.unit
|
|
||||||
if fmtstr != u'%d':
|
|
||||||
self.hints[u'fmtstr'] = self.fmtstr
|
|
||||||
info.update(self.hints)
|
|
||||||
self.as_json = [u'int', info]
|
|
||||||
|
|
||||||
def validate(self, value):
|
def validate(self, value):
|
||||||
try:
|
try:
|
||||||
@@ -227,10 +203,7 @@ class IntRange(DataType):
|
|||||||
raise ValueError(u'Can not validate %r to int' % value)
|
raise ValueError(u'Can not validate %r to int' % value)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
items = [u"%d, %d" % (self.min, self.max)]
|
return u'IntRange(%d, %d)' % (self.min, self.max)
|
||||||
for k, v in self.hints.items():
|
|
||||||
items.append(u'%s=%r' % (k, v))
|
|
||||||
return u'IntRange(%s)' % (u', '.join(items))
|
|
||||||
|
|
||||||
def export_value(self, value):
|
def export_value(self, value):
|
||||||
"""returns a python object fit for serialisation"""
|
"""returns a python object fit for serialisation"""
|
||||||
@@ -245,11 +218,7 @@ class IntRange(DataType):
|
|||||||
return self.validate(value)
|
return self.validate(value)
|
||||||
|
|
||||||
def format_value(self, value, unit=None):
|
def format_value(self, value, unit=None):
|
||||||
if unit is None:
|
return u'%d' % value
|
||||||
unit = self.unit
|
|
||||||
if unit:
|
|
||||||
return u' '.join([self.fmtstr % value, unit])
|
|
||||||
return self.fmtstr % value
|
|
||||||
|
|
||||||
|
|
||||||
class ScaledInteger(DataType):
|
class ScaledInteger(DataType):
|
||||||
@@ -258,18 +227,16 @@ 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"""
|
||||||
|
|
||||||
def __init__(self, scale, minval=None, maxval=None, unit=u'', fmtstr=u'',
|
def __init__(self, scale, minval=None, maxval=None, unit=None, fmtstr=None,
|
||||||
absolute_precision=None, relative_precision=None,):
|
absolute_precision=None, relative_precision=None,):
|
||||||
|
self._defaults = {}
|
||||||
self.scale = float(scale)
|
self.scale = float(scale)
|
||||||
if not self.scale > 0:
|
if not self.scale > 0:
|
||||||
raise ValueError(u'Scale MUST be positive!')
|
raise ValueError(u'Scale MUST be positive!')
|
||||||
|
self.setprop('unit', unit, u'', unicode)
|
||||||
# store hints
|
self.setprop('fmtstr', fmtstr, u'%g', unicode)
|
||||||
self.hints = {}
|
self.setprop('absolute_precision', absolute_precision, self.scale, float)
|
||||||
self.unit = unicode(unit)
|
self.setprop('relative_precision', relative_precision, 1.2e-7, float)
|
||||||
self.fmtstr = unicode(fmtstr or u'%g')
|
|
||||||
self.abs_prec = float(absolute_precision or self.scale)
|
|
||||||
self.rel_prec = float(relative_precision or 0)
|
|
||||||
|
|
||||||
self.min = DEFAULT_MIN_INT * self.scale if minval is None else float(minval)
|
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)
|
self.max = DEFAULT_MAX_INT * self.scale if maxval is None else float(maxval)
|
||||||
@@ -279,26 +246,19 @@ class ScaledInteger(DataType):
|
|||||||
raise ValueError(u'Max must be larger then min!')
|
raise ValueError(u'Max must be larger then min!')
|
||||||
if '%' not in self.fmtstr:
|
if '%' not in self.fmtstr:
|
||||||
raise ValueError(u'Invalid fmtstr!')
|
raise ValueError(u'Invalid fmtstr!')
|
||||||
if self.abs_prec < 0:
|
if self.absolute_precision < 0:
|
||||||
raise ValueError(u'absolute_precision MUST be >=0')
|
raise ValueError(u'absolute_precision MUST be >=0')
|
||||||
if self.rel_prec < 0:
|
if self.relative_precision < 0:
|
||||||
raise ValueError(u'relative_precision MUST be >=0')
|
raise ValueError(u'relative_precision MUST be >=0')
|
||||||
|
|
||||||
info = {}
|
@property
|
||||||
self.hints = {}
|
def as_json(self):
|
||||||
info[u'min'] = int(self.min // self.scale)
|
info = {k: getattr(self, k) for k, v in self._defaults.items()
|
||||||
info[u'max'] = int((self.max + self.scale * 0.5) // self.scale)
|
if v != getattr(self, k)}
|
||||||
info[u'scale'] = self.scale
|
info['scale'] = self.scale
|
||||||
if unit:
|
info['min'] = int((self.min + self.scale * 0.5) // self.scale)
|
||||||
self.hints[u'unit'] = self.unit
|
info['max'] = int((self.max + self.scale * 0.5) // self.scale)
|
||||||
if fmtstr:
|
return [u'scaled', info]
|
||||||
self.hints[u'fmtstr'] = self.fmtstr
|
|
||||||
if absolute_precision is not None:
|
|
||||||
self.hints[u'absolute_precision'] = self.abs_prec
|
|
||||||
if relative_precision is not None:
|
|
||||||
self.hints[u'relative_precision'] = self.rel_prec
|
|
||||||
info.update(self.hints)
|
|
||||||
self.as_json = [u'scaled', info]
|
|
||||||
|
|
||||||
def validate(self, value):
|
def validate(self, value):
|
||||||
try:
|
try:
|
||||||
@@ -316,8 +276,10 @@ 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):
|
||||||
items = [self.fmtstr % self.scale, self.fmtstr % self.min, self.fmtstr % self.max]
|
hints = self.as_json[1]
|
||||||
for k,v in self.hints.items():
|
hints.pop('scale')
|
||||||
|
items = [self.scale]
|
||||||
|
for k,v in hints.items():
|
||||||
items.append(u'%s=%r' % (k,v))
|
items.append(u'%s=%r' % (k,v))
|
||||||
return u'ScaledInteger(%s)' % (', '.join(items))
|
return u'ScaledInteger(%s)' % (', '.join(items))
|
||||||
|
|
||||||
|
|||||||
@@ -95,13 +95,8 @@ def test_IntRange():
|
|||||||
dt = IntRange()
|
dt = IntRange()
|
||||||
assert dt.as_json[0] == u'int'
|
assert dt.as_json[0] == u'int'
|
||||||
assert dt.as_json[1][u'min'] < 0 < dt.as_json[1][u'max']
|
assert dt.as_json[1][u'min'] < 0 < dt.as_json[1][u'max']
|
||||||
|
assert dt.as_json == [u'int', {u'max': 16777216, u'min': -16777216}]
|
||||||
dt = IntRange(unit=u'X', fmtstr=u'%r')
|
assert dt.format_value(42) == u'42'
|
||||||
assert dt.as_json == [u'int', {u'fmtstr': u'%r', u'max': 16777216,
|
|
||||||
u'min': -16777216, u'unit': u'X'}]
|
|
||||||
assert dt.format_value(42) == u'42 X'
|
|
||||||
assert dt.format_value(42, u'') == u'42'
|
|
||||||
assert dt.format_value(42, u'Z') == u'42 Z'
|
|
||||||
|
|
||||||
|
|
||||||
def test_ScaledInteger():
|
def test_ScaledInteger():
|
||||||
@@ -277,11 +272,11 @@ def test_ArrayOf():
|
|||||||
u'members':[u'int', {u'min':-10,
|
u'members':[u'int', {u'min':-10,
|
||||||
u'max':10}]}]
|
u'max':10}]}]
|
||||||
|
|
||||||
dt = ArrayOf(IntRange(-10, 10, unit=u'Z'), 1, 3)
|
dt = ArrayOf(FloatRange(-10, 10, unit=u'Z'), 1, 3)
|
||||||
assert dt.as_json == [u'array', {u'min':1, u'max':3,
|
assert dt.as_json == [u'array', {u'min':1, u'max':3,
|
||||||
u'members':[u'int', {u'min':-10,
|
u'members':[u'double', {u'min':-10,
|
||||||
u'max':10,
|
u'max':10,
|
||||||
u'unit':u'Z'}]}]
|
u'unit':u'Z'}]}]
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
dt.validate(9)
|
dt.validate(9)
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
@@ -302,10 +297,9 @@ def test_TupleOf():
|
|||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
TupleOf(2)
|
TupleOf(2)
|
||||||
|
|
||||||
dt = TupleOf(IntRange(-10, 10, unit=u'X'), BoolType())
|
dt = TupleOf(IntRange(-10, 10), BoolType())
|
||||||
assert dt.as_json == [u'tuple', {u'members':[[u'int', {u'min':-10,
|
assert dt.as_json == [u'tuple', {u'members':[[u'int', {u'min':-10,
|
||||||
u'max':10,
|
u'max':10}],
|
||||||
u'unit':u'X'}],
|
|
||||||
[u'bool', {}]]}]
|
[u'bool', {}]]}]
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
@@ -318,7 +312,7 @@ def test_TupleOf():
|
|||||||
assert dt.export_value([1, True]) == [1, True]
|
assert dt.export_value([1, True]) == [1, True]
|
||||||
assert dt.import_value([1, True]) == [1, True]
|
assert dt.import_value([1, True]) == [1, True]
|
||||||
|
|
||||||
assert dt.format_value([3,0]) == u"(3 X, False)"
|
assert dt.format_value([3,0]) == u"(3, False)"
|
||||||
|
|
||||||
|
|
||||||
def test_StructOf():
|
def test_StructOf():
|
||||||
@@ -328,13 +322,12 @@ def test_StructOf():
|
|||||||
with pytest.raises(ProgrammingError):
|
with pytest.raises(ProgrammingError):
|
||||||
StructOf(IntRange=1)
|
StructOf(IntRange=1)
|
||||||
|
|
||||||
dt = StructOf(a_string=StringType(0, 55), an_int=IntRange(0, 999, unit=u'Y'),
|
dt = StructOf(a_string=StringType(0, 55), an_int=IntRange(0, 999),
|
||||||
optional=[u'an_int'])
|
optional=[u'an_int'])
|
||||||
assert dt.as_json == [u'struct', {u'members':{u'a_string':
|
assert dt.as_json == [u'struct', {u'members':{u'a_string':
|
||||||
[u'string', {u'min':0, u'max':55}],
|
[u'string', {u'min':0, u'max':55}],
|
||||||
u'an_int':
|
u'an_int':
|
||||||
[u'int', {u'min':0, u'max':999,
|
[u'int', {u'min':0, u'max':999}],},
|
||||||
u'unit':u'Y'}],},
|
|
||||||
u'optional':[u'an_int'],
|
u'optional':[u'an_int'],
|
||||||
}]
|
}]
|
||||||
|
|
||||||
@@ -352,7 +345,7 @@ def test_StructOf():
|
|||||||
assert dt.import_value({u'an_int': 13, u'a_string': u'WFEC'}) == {
|
assert dt.import_value({u'an_int': 13, u'a_string': u'WFEC'}) == {
|
||||||
u'a_string': u'WFEC', u'an_int': 13}
|
u'a_string': u'WFEC', u'an_int': 13}
|
||||||
|
|
||||||
assert dt.format_value({u'an_int':2, u'a_string':u'Z'}) == u"{a_string=u'Z', an_int=2 Y}"
|
assert dt.format_value({u'an_int':2, u'a_string':u'Z'}) == u"{a_string=u'Z', an_int=2}"
|
||||||
|
|
||||||
|
|
||||||
def test_get_datatype():
|
def test_get_datatype():
|
||||||
|
|||||||
Reference in New Issue
Block a user