datatypes: implement hints (unit, fmtstr, *_precision)

on types where these can be useful.
Also fix some issues with ScaledInteger.

Change-Id: I9d456c4f237da3a37730c3e451e9fb59307ed982
Reviewed-on: https://forge.frm2.tum.de/review/20240
Tested-by: JenkinsCodeReview <bjoern_pedersen@frm2.tum.de>
Reviewed-by: Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
This commit is contained in:
Enrico Faulhaber
2019-03-27 10:49:03 +01:00
parent 58bae697c9
commit 16772609f4
2 changed files with 215 additions and 86 deletions

View File

@ -49,6 +49,10 @@ __all__ = [
u'CommandType',
]
# *DEFAULT* limits for IntRange/ScaledIntegers transport serialisation
DEFAULT_MIN_INT = -16777216
DEFAULT_MAX_INT = 16777216
# base class for all DataTypes
@ -87,18 +91,46 @@ class DataType(object):
class FloatRange(DataType):
"""Restricted float type"""
def __init__(self, minval=None, maxval=None):
self.min = None if minval is None else float(minval)
self.max = None if maxval is None else float(maxval)
# note: as we may compare to Inf all comparisons would be false
if (self.min or float(u'-inf')) <= (self.max or float(u'+inf')):
self.as_json = [u'double', dict()]
if self.min:
self.as_json[1]['min'] = self.min
if self.max:
self.as_json[1]['max'] = self.max
else:
unit = u''
fmtstr = u'%f'
def __init__(self, minval=None, maxval=None, unit=u'', fmtstr=u'',
absolute_precision=None, relative_precision=None,):
# store hints
self.hints = {}
self.unit = unicode(unit)
self.fmtstr = unicode(fmtstr or u'%f')
self.abs_prec = float(absolute_precision or 0.0)
self.rel_prec = float(relative_precision or 1.2e-7)
# 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
if self.min > self.max:
raise ValueError(u'Max must be larger then min!')
if '%' not in self.fmtstr:
raise ValueError(u'Invalid fmtstr!')
if self.abs_prec < 0:
raise ValueError(u'absolute_precision MUST be >=0')
if self.rel_prec < 0:
raise ValueError(u'relative_precision MUST be >=0')
info = {}
if self.min != float(u'-inf'):
info[u'min'] = self.min
if self.max != float(u'inf'):
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):
try:
@ -119,11 +151,12 @@ class FloatRange(DataType):
(value, self.min, self.max))
def __repr__(self):
if self.max is not None:
return u'FloatRange(%r, %r)' % (
float(u'-inf') if self.min is None else self.min,
float(u'inf') if self.max is None else self.max)
return u'FloatRange()'
items = [] if self.max or self.min is None else \
[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))
def export_value(self, value):
"""returns a python object fit for serialisation"""
@ -138,25 +171,43 @@ class FloatRange(DataType):
return self.validate(value)
class IntRange(DataType):
"""Restricted int type"""
def __init__(self, minval=None, maxval=None):
self.min = -16777216 if minval is None else int(minval)
self.max = 16777216 if maxval is None else int(maxval)
unit = u''
fmtstr = u'%f'
def __init__(self, minval=None, maxval=None, fmtstr=u'%d', unit=u''):
self.hints = {}
self.fmtstr = unicode(fmtstr)
self.unit = unicode(unit)
self.min = DEFAULT_MIN_INT if minval is None else int(minval)
self.max = DEFAULT_MAX_INT if maxval is None else int(maxval)
# check values
if self.min > self.max:
raise ValueError(u'Max must be larger then min!')
if None in (self.min, self.max):
raise ValueError(u'Limits can not be None!')
self.as_json = [u'int', {'min':self.min, 'max':self.max}]
if '%' not in self.fmtstr:
raise ValueError(u'Invalid fmtstr!')
info = {}
self.hints = {}
info[u'min'] = self.min
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):
try:
value = int(value)
if self.min is not None and value < self.min:
if value < self.min:
raise ValueError(u'%r should be an int between %d and %d' %
(value, self.min, self.max or 0))
if self.max is not None and value > self.max:
if value > self.max:
raise ValueError(u'%r should be an int between %d and %d' %
(value, self.min or 0, self.max))
return value
@ -164,9 +215,10 @@ class IntRange(DataType):
raise ValueError(u'Can not validate %r to int' % value)
def __repr__(self):
if self.max is not None:
return u'IntRange(%d, %d)' % (self.min, self.max)
return u'IntRange()'
items = [u"%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):
"""returns a python object fit for serialisation"""
@ -187,36 +239,75 @@ 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"""
def __init__(self, scale, minval=-16777216, maxval=16777216):
self.min = int(minval)
self.max = int(maxval)
unit = u''
fmtstr = u'%f'
def __init__(self, scale, minval=None, maxval=None, unit=u'', fmtstr=u'',
absolute_precision=None, relative_precision=None,):
self.scale = float(scale)
if self.min > self.max:
raise ValueError(u'Max must be larger then min!')
if not self.scale > 0:
raise ValueError(u'Scale MUST be positive!')
self.as_json = [u'scaled', dict(min=int(round(minval/scale)), max=int(round(maxval/scale)), scale=scale)]
# store hints
self.hints = {}
self.unit = unicode(unit)
self.fmtstr = unicode(fmtstr or u'%f')
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.max = DEFAULT_MAX_INT * self.scale if maxval is None else float(maxval)
# check values
if self.min > self.max:
raise ValueError(u'Max must be larger then min!')
if '%' not in self.fmtstr:
raise ValueError(u'Invalid fmtstr!')
if self.abs_prec < 0:
raise ValueError(u'absolute_precision MUST be >=0')
if self.rel_prec < 0:
raise ValueError(u'relative_precision MUST be >=0')
info = {}
self.hints = {}
info[u'min'] = int(self.min // self.scale)
info[u'max'] = int((self.max + self.scale * 0.5) // self.scale)
info[u'scale'] = self.scale
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'scaled', info]
def validate(self, value):
try:
value = int(value)
if value < self.min:
raise ValueError(u'%r should be an int between %d and %d' %
(value, self.min, self.max))
if value > self.max:
raise ValueError(u'%r should be an int between %d and %d' %
(value, self.min, self.max))
return value
value = float(value)
except Exception:
raise ValueError(u'Can not validate %r to int' % value)
raise ValueError(u'Can not validate %r to float' % value)
if value < self.min:
raise ValueError(u'%r should be a float between %d and %d' %
(value, self.min, self.max))
if value > self.max:
raise ValueError(u'%r should be a float between %d and %d' %
(value, self.min, self.max))
intval = int((value + self.scale * 0.5) // self.scale)
value = float(intval * self.scale)
return value # return 'actual' value (which is more discrete than a float)
def __repr__(self):
return u'ScaledInteger(%f, %d, %d)' % (self.scale, self.min, self.max)
items = [self.fmtstr % self.scale, self.fmtstr % self.min, self.fmtstr % self.max]
for k,v in self.hints.items():
items.append(u'%s=%r' % (k,v))
return u'ScaledInteger(%s)' % (', '.join(items))
def export_value(self, value):
"""returns a python object fit for serialisation"""
# XXX: rounds toward even !!! (i.e. 12.5 -> 12, 13.5 -> 14)
return round(value / self.scale)
# note: round behaves different in Py2 vs. Py3, so use floor division
return int((value + self.scale * 0.5) // self.scale)
def import_value(self, value):
"""returns a python object from serialisation"""
@ -228,16 +319,17 @@ class ScaledInteger(DataType):
class EnumType(DataType):
def __init__(self, enum_or_name='', **kwds):
if 'members' in kwds:
if u'members' in kwds:
kwds = dict(kwds)
kwds.update(kwds['members'])
kwds.pop('members')
kwds.update(kwds[u'members'])
kwds.pop(u'members')
self._enum = Enum(enum_or_name, **kwds)
@property
def as_json(self):
return [u'enum'] + [{"members":dict((m.name, m.value) for m in self._enum.members)}]
return [u'enum'] + [{u"members":dict((m.name, m.value) for m in self._enum.members)}]
def __repr__(self):
return u"EnumType(%r, %s)" % (self._enum.name, ', '.join(u'%s=%d' %(m.name, m.value) for m in self._enum.members))
@ -279,7 +371,7 @@ class BLOBType(DataType):
self.as_json = [u'blob', dict(min=minsize, max=maxsize)]
def __repr__(self):
return u'BLOB(%s, %s)' % (unicode(self.minsize), unicode(self.maxsize))
return u'BLOB(%d, %d)' % (self.minsize, self.maxsize)
def validate(self, value):
"""return the validated (internal) value or raise"""
@ -289,10 +381,9 @@ class BLOBType(DataType):
if size < self.minsize:
raise ValueError(
u'%r must be at least %d bytes long!' % (value, self.minsize))
if self.maxsize is not None:
if size > self.maxsize:
raise ValueError(
u'%r must be at most %d bytes long!' % (value, self.maxsize))
if size > self.maxsize:
raise ValueError(
u'%r must be at most %d bytes long!' % (value, self.maxsize))
return value
def export_value(self, value):
@ -326,7 +417,7 @@ class StringType(DataType):
self.as_json = [u'string', dict(min=minsize, max=maxsize)]
def __repr__(self):
return u'StringType(%s, %s)' % (unicode(self.minsize), unicode(self.maxsize))
return u'StringType(%d, %d)' % (self.minsize, self.maxsize)
def validate(self, value):
"""return the validated (internal) value or raise"""
@ -336,10 +427,9 @@ class StringType(DataType):
if size < self.minsize:
raise ValueError(
u'%r must be at least %d bytes long!' % (value, self.minsize))
if self.maxsize is not None:
if size > self.maxsize:
raise ValueError(
u'%r must be at most %d bytes long!' % (value, self.maxsize))
if size > self.maxsize:
raise ValueError(
u'%r must be at most %d bytes long!' % (value, self.maxsize))
if u'\0' in value:
raise ValueError(
u'Strings are not allowed to embed a \\0! Use a Blob instead!')
@ -358,13 +448,12 @@ class StringType(DataType):
value = unicode(text)
return self.validate(value)
# Bool is a special enum
class BoolType(DataType):
def __init__(self):
self.as_json = [u'bool', dict()]
self.as_json = [u'bool', {}]
def __repr__(self):
return u'BoolType()'
@ -400,13 +489,13 @@ class ArrayOf(DataType):
subtype = None
def __init__(self, members, minsize=0, maxsize=None):
if not isinstance(members, DataType):
raise ValueError(
u'ArrayOf only works with a DataType as first argument!')
# one argument -> exactly that size
# argument default to 10
if maxsize is None:
maxsize = minsize or 10
if not isinstance(members, DataType):
raise ValueError(
u'ArrayOf only works with a DataType as first argument!')
self.subtype = members
self.minsize = int(minsize)
@ -636,9 +725,9 @@ class Status(TupleOf):
# argumentnames to lambda from spec!
DATATYPES = dict(
bool =BoolType,
int =lambda min, max: IntRange(minval=min,maxval=max),
scaled =lambda scale, min, max: ScaledInteger(scale=scale,minval=min*scale,maxval=max*scale),
double =lambda min=None, max=None: FloatRange(minval=min, maxval=max),
int =lambda min, max, **kwds: IntRange(minval=min, maxval=max, **kwds),
scaled =lambda scale, min, max, **kwds: ScaledInteger(scale=scale, minval=min*scale, maxval=max*scale, **kwds),
double =lambda min=None, max=None, **kwds: FloatRange(minval=min, maxval=max, **kwds),
blob =lambda min=0, max=None: BLOBType(minsize=min, maxsize=max),
string =lambda min=0, max=None: StringType(minsize=min, maxsize=max),
array =lambda min, max, members: ArrayOf(get_datatype(members), minsize=min, maxsize=max),