Merge "rework datatypes (setter should not check limits)"
This commit is contained in:
commit
82957c287d
@ -50,6 +50,17 @@ class DiscouragedConversion(BadValueError):
|
|||||||
log_message = True
|
log_message = True
|
||||||
|
|
||||||
|
|
||||||
|
def shortrepr(value):
|
||||||
|
"""shortened repr for error messages
|
||||||
|
|
||||||
|
avoid lengthy error message in case a value is too complex
|
||||||
|
"""
|
||||||
|
r = repr(value)
|
||||||
|
if len(r) > 40:
|
||||||
|
return r[:40] + '...'
|
||||||
|
return r
|
||||||
|
|
||||||
|
|
||||||
# base class for all DataTypes
|
# base class for all DataTypes
|
||||||
class DataType(HasProperties):
|
class DataType(HasProperties):
|
||||||
"""base class for all data types"""
|
"""base class for all data types"""
|
||||||
@ -58,11 +69,24 @@ class DataType(HasProperties):
|
|||||||
default = None
|
default = None
|
||||||
|
|
||||||
def __call__(self, value):
|
def __call__(self, value):
|
||||||
"""check if given value (a python obj) is valid for this datatype
|
"""convert given value to our datatype and validate
|
||||||
|
|
||||||
returns the (possibly converted) value or raises an appropriate exception"""
|
:param value: the value to be converted
|
||||||
|
:return: the converted type
|
||||||
|
|
||||||
|
check if given value (a python obj) is valid for this datatype,
|
||||||
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def validate(self, value, previous=None):
|
||||||
|
"""convert value to datatype and check for limits
|
||||||
|
|
||||||
|
:param value: the value to be converted
|
||||||
|
:param previous: previous value (used for optional struct members)
|
||||||
|
"""
|
||||||
|
# default: no limits to check
|
||||||
|
return self(value)
|
||||||
|
|
||||||
def from_string(self, text):
|
def from_string(self, text):
|
||||||
"""interprets a given string and returns a validated (internal) value"""
|
"""interprets a given string and returns a validated (internal) value"""
|
||||||
# to evaluate values from configfiles, ui, etc...
|
# to evaluate values from configfiles, ui, etc...
|
||||||
@ -205,18 +229,22 @@ class FloatRange(HasUnit, DataType):
|
|||||||
try:
|
try:
|
||||||
value = float(value)
|
value = float(value)
|
||||||
except Exception:
|
except Exception:
|
||||||
raise BadValueError('Can not convert %r to float' % value) from None
|
raise BadValueError('can not convert %s to a float' % shortrepr(value)) from None
|
||||||
if not generalConfig.lazy_number_validation:
|
if not generalConfig.lazy_number_validation:
|
||||||
raise DiscouragedConversion('automatic string to float conversion no longer supported') from None
|
raise DiscouragedConversion('automatic string to float conversion no longer supported') from None
|
||||||
|
|
||||||
# map +/-infty to +/-max possible number
|
# map +/-infty to +/-max possible number
|
||||||
value = clamp(-sys.float_info.max, value, sys.float_info.max)
|
return clamp(-sys.float_info.max, value, sys.float_info.max)
|
||||||
|
|
||||||
# now check the limits
|
def validate(self, value, previous=None):
|
||||||
|
# convert
|
||||||
|
value = self(value)
|
||||||
|
# check the limits
|
||||||
prec = max(abs(value * self.relative_resolution), self.absolute_resolution)
|
prec = max(abs(value * self.relative_resolution), self.absolute_resolution)
|
||||||
if self.min - prec <= value <= self.max + prec:
|
if self.min - prec <= value <= self.max + prec:
|
||||||
return min(max(value, self.min), self.max)
|
# silently clamp when outside by not more than prec
|
||||||
raise BadValueError('%.14g should be a float between %.14g and %.14g' %
|
return clamp(self.min, value, self.max)
|
||||||
|
raise BadValueError('%.14g must be between %d and %d' %
|
||||||
(value, self.min, self.max))
|
(value, self.min, self.max))
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
@ -246,24 +274,12 @@ class FloatRange(HasUnit, DataType):
|
|||||||
return ' '.join([self.fmtstr % value, unit])
|
return ' '.join([self.fmtstr % value, unit])
|
||||||
return self.fmtstr % value
|
return self.fmtstr % value
|
||||||
|
|
||||||
def problematic_range(self, target_type):
|
|
||||||
"""check problematic range
|
|
||||||
|
|
||||||
returns True when self.min or self.max is given, not 0 and equal to the same limit on target_type.
|
|
||||||
"""
|
|
||||||
value_info = self.get_info()
|
|
||||||
target_info = target_type.get_info()
|
|
||||||
minval = value_info.get('min') # None when -infinite
|
|
||||||
maxval = value_info.get('max') # None when +infinite
|
|
||||||
return ((minval and minval == target_info.get('min')) or
|
|
||||||
(maxval and maxval == target_info.get('max')))
|
|
||||||
|
|
||||||
def compatible(self, other):
|
def compatible(self, other):
|
||||||
if not isinstance(other, (FloatRange, ScaledInteger)):
|
if not isinstance(other, (FloatRange, ScaledInteger)):
|
||||||
raise BadValueError('incompatible datatypes')
|
raise BadValueError('incompatible datatypes')
|
||||||
# avoid infinity
|
# avoid infinity
|
||||||
other(max(sys.float_info.min, self.min))
|
other.validate(max(sys.float_info.min, self.min))
|
||||||
other(min(sys.float_info.max, self.max))
|
other.validate(min(sys.float_info.max, self.max))
|
||||||
|
|
||||||
|
|
||||||
class IntRange(DataType):
|
class IntRange(DataType):
|
||||||
@ -298,14 +314,22 @@ class IntRange(DataType):
|
|||||||
fvalue = float(value)
|
fvalue = float(value)
|
||||||
value = int(value)
|
value = int(value)
|
||||||
except Exception:
|
except Exception:
|
||||||
raise BadValueError('Can not convert %r to int' % value) from None
|
raise BadValueError('can not convert %s to an int' % shortrepr(value)) from None
|
||||||
if not generalConfig.lazy_number_validation:
|
if not generalConfig.lazy_number_validation:
|
||||||
raise DiscouragedConversion('automatic string to float conversion no longer supported') from None
|
raise DiscouragedConversion('automatic string to float conversion no longer supported') from None
|
||||||
if not self.min <= value <= self.max or round(fvalue) != fvalue:
|
if round(fvalue) != fvalue:
|
||||||
raise BadValueError('%r should be an int between %d and %d' %
|
raise BadValueError('%r should be an int')
|
||||||
(value, self.min, self.max))
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
def validate(self, value, previous=None):
|
||||||
|
# convert
|
||||||
|
value = self(value)
|
||||||
|
# check the limits
|
||||||
|
if self.min <= value <= self.max:
|
||||||
|
return value
|
||||||
|
raise BadValueError('%r must be between %d and %d' %
|
||||||
|
(value, self.min, self.max))
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
args = (self.min, self.max)
|
args = (self.min, self.max)
|
||||||
if args[1] == DEFAULT_MAX_INT:
|
if args[1] == DEFAULT_MAX_INT:
|
||||||
@ -331,8 +355,8 @@ class IntRange(DataType):
|
|||||||
|
|
||||||
def compatible(self, other):
|
def compatible(self, other):
|
||||||
if isinstance(other, (IntRange, FloatRange, ScaledInteger)):
|
if isinstance(other, (IntRange, FloatRange, ScaledInteger)):
|
||||||
other(self.min)
|
other.validate(self.min)
|
||||||
other(self.max)
|
other.validate(self.max)
|
||||||
return
|
return
|
||||||
if isinstance(other, (EnumType, BoolType)):
|
if isinstance(other, (EnumType, BoolType)):
|
||||||
# the following loop will not cycle more than the number of Enum elements
|
# the following loop will not cycle more than the number of Enum elements
|
||||||
@ -397,8 +421,8 @@ class ScaledInteger(HasUnit, DataType):
|
|||||||
|
|
||||||
def export_datatype(self):
|
def export_datatype(self):
|
||||||
return self.get_info(type='scaled',
|
return self.get_info(type='scaled',
|
||||||
min=int((self.min + self.scale * 0.5) // self.scale),
|
min=int(round(self.min / self.scale)),
|
||||||
max=int((self.max + self.scale * 0.5) // self.scale))
|
max=int(round(self.max / self.scale)))
|
||||||
|
|
||||||
def __call__(self, value):
|
def __call__(self, value):
|
||||||
try:
|
try:
|
||||||
@ -407,30 +431,30 @@ class ScaledInteger(HasUnit, DataType):
|
|||||||
try:
|
try:
|
||||||
value = float(value)
|
value = float(value)
|
||||||
except Exception:
|
except Exception:
|
||||||
raise BadValueError('Can not convert %r to float' % value) from None
|
raise BadValueError('can not convert %s to float' % shortrepr(value)) from None
|
||||||
if not generalConfig.lazy_number_validation:
|
if not generalConfig.lazy_number_validation:
|
||||||
raise DiscouragedConversion('automatic string to float conversion no longer supported') from None
|
raise DiscouragedConversion('automatic string to float conversion no longer supported') from None
|
||||||
prec = max(self.scale, abs(value * self.relative_resolution),
|
intval = int(round(value / self.scale))
|
||||||
self.absolute_resolution)
|
return float(intval * self.scale) # return 'actual' value (which is more discrete than a float)
|
||||||
if self.min - prec <= value <= self.max + prec:
|
|
||||||
value = min(max(value, self.min), self.max)
|
def validate(self, value, previous=None):
|
||||||
else:
|
# convert
|
||||||
raise BadValueError('%g should be a float between %g and %g' %
|
result = self(value)
|
||||||
|
if self.min - self.scale < value < self.max + self.scale:
|
||||||
|
# silently clamp when outside by not more than self.scale
|
||||||
|
return clamp(self(self.min), result, self(self.max))
|
||||||
|
raise BadValueError('%.14g must be between between %g and %g' %
|
||||||
(value, self.min, self.max))
|
(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):
|
def __repr__(self):
|
||||||
hints = self.get_info(scale=float('%g' % self.scale),
|
hints = self.get_info(scale=float('%g' % self.scale),
|
||||||
min=int((self.min + self.scale * 0.5) // self.scale),
|
min=int(round(self.min / self.scale)),
|
||||||
max=int((self.max + self.scale * 0.5) // self.scale))
|
max=int(round(self.max / 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()))
|
||||||
|
|
||||||
def export_value(self, value):
|
def export_value(self, value):
|
||||||
"""returns a python object fit for serialisation"""
|
"""returns a python object fit for serialisation"""
|
||||||
# note: round behaves different in Py2 vs. Py3, so use floor division
|
return int(round(value / self.scale))
|
||||||
return int((value + self.scale * 0.5) // self.scale)
|
|
||||||
|
|
||||||
def import_value(self, value):
|
def import_value(self, value):
|
||||||
"""returns a python object from serialisation"""
|
"""returns a python object from serialisation"""
|
||||||
@ -450,8 +474,8 @@ class ScaledInteger(HasUnit, DataType):
|
|||||||
def compatible(self, other):
|
def compatible(self, other):
|
||||||
if not isinstance(other, (FloatRange, ScaledInteger)):
|
if not isinstance(other, (FloatRange, ScaledInteger)):
|
||||||
raise BadValueError('incompatible datatypes')
|
raise BadValueError('incompatible datatypes')
|
||||||
other(self.min)
|
other.validate(self.min)
|
||||||
other(self.max)
|
other.validate(self.max)
|
||||||
|
|
||||||
|
|
||||||
class EnumType(DataType):
|
class EnumType(DataType):
|
||||||
@ -461,7 +485,7 @@ class EnumType(DataType):
|
|||||||
:param members: members dict or None when using kwds only
|
:param members: members dict or None when using kwds only
|
||||||
:param kwds: (additional) members
|
:param kwds: (additional) members
|
||||||
"""
|
"""
|
||||||
def __init__(self, enum_or_name='', *, members=None, **kwds):
|
def __init__(self, enum_or_name='', members=None, **kwds):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
if members is not None:
|
if members is not None:
|
||||||
kwds.update(members)
|
kwds.update(members)
|
||||||
@ -495,7 +519,7 @@ class EnumType(DataType):
|
|||||||
try:
|
try:
|
||||||
return self._enum[value]
|
return self._enum[value]
|
||||||
except (KeyError, TypeError): # TypeError will be raised when value is not hashable
|
except (KeyError, TypeError): # TypeError will be raised when value is not hashable
|
||||||
raise BadValueError('%r is not a member of enum %r' % (value, self._enum)) from None
|
raise BadValueError('%s is not a member of enum %r' % (shortrepr(value), self._enum)) from None
|
||||||
|
|
||||||
def from_string(self, text):
|
def from_string(self, text):
|
||||||
return self(text)
|
return self(text)
|
||||||
@ -543,7 +567,7 @@ class BLOBType(DataType):
|
|||||||
def __call__(self, value):
|
def __call__(self, value):
|
||||||
"""return the validated (internal) value or raise"""
|
"""return the validated (internal) value or raise"""
|
||||||
if not isinstance(value, bytes):
|
if not isinstance(value, bytes):
|
||||||
raise BadValueError('%s has the wrong type!' % repr(value))
|
raise BadValueError('%s must be of type bytes' % shortrepr(value))
|
||||||
size = len(value)
|
size = len(value)
|
||||||
if size < self.minbytes:
|
if size < self.minbytes:
|
||||||
raise BadValueError(
|
raise BadValueError(
|
||||||
@ -608,19 +632,19 @@ class StringType(DataType):
|
|||||||
def __call__(self, value):
|
def __call__(self, value):
|
||||||
"""return the validated (internal) value or raise"""
|
"""return the validated (internal) value or raise"""
|
||||||
if not isinstance(value, str):
|
if not isinstance(value, str):
|
||||||
raise BadValueError('%s has the wrong type!' % repr(value))
|
raise BadValueError('%s has the wrong type!' % shortrepr(value))
|
||||||
if not self.isUTF8:
|
if not self.isUTF8:
|
||||||
try:
|
try:
|
||||||
value.encode('ascii')
|
value.encode('ascii')
|
||||||
except UnicodeEncodeError:
|
except UnicodeEncodeError:
|
||||||
raise BadValueError('%r contains non-ascii character!' % value) from None
|
raise BadValueError('%s contains non-ascii character!' % shortrepr(value)) from None
|
||||||
size = len(value)
|
size = len(value)
|
||||||
if size < self.minchars:
|
if size < self.minchars:
|
||||||
raise BadValueError(
|
raise BadValueError(
|
||||||
'%r must be at least %d bytes long!' % (value, self.minchars))
|
'%s must be at least %d chars long!' % (shortrepr(value), self.minchars))
|
||||||
if size > self.maxchars:
|
if size > self.maxchars:
|
||||||
raise BadValueError(
|
raise BadValueError(
|
||||||
'%r must be at most %d bytes long!' % (value, self.maxchars))
|
'%s must be at most %d chars long!' % (shortrepr(value), self.maxchars))
|
||||||
if '\0' in value:
|
if '\0' in value:
|
||||||
raise BadValueError(
|
raise BadValueError(
|
||||||
'Strings are not allowed to embed a \\0! Use a Blob instead!')
|
'Strings are not allowed to embed a \\0! Use a Blob instead!')
|
||||||
@ -687,7 +711,7 @@ class BoolType(DataType):
|
|||||||
return False
|
return False
|
||||||
if value in [1, '1', 'True', 'true', 'yes', 'on', True]:
|
if value in [1, '1', 'True', 'true', 'yes', 'on', True]:
|
||||||
return True
|
return True
|
||||||
raise BadValueError('%r is not a boolean value!' % value)
|
raise BadValueError('%s is not a boolean value!' % shortrepr(value))
|
||||||
|
|
||||||
def export_value(self, value):
|
def export_value(self, value):
|
||||||
"""returns a python object fit for serialisation"""
|
"""returns a python object fit for serialisation"""
|
||||||
@ -768,23 +792,36 @@ class ArrayOf(DataType):
|
|||||||
return 'ArrayOf(%s, %s, %s)' % (
|
return 'ArrayOf(%s, %s, %s)' % (
|
||||||
repr(self.members), self.minlen, self.maxlen)
|
repr(self.members), self.minlen, self.maxlen)
|
||||||
|
|
||||||
def __call__(self, value):
|
def check_type(self, value):
|
||||||
"""validate an external representation to an internal one"""
|
|
||||||
try:
|
try:
|
||||||
# check number of elements
|
# check number of elements
|
||||||
if self.minlen is not None and len(value) < self.minlen:
|
if self.minlen is not None and len(value) < self.minlen:
|
||||||
raise BadValueError(
|
raise BadValueError(
|
||||||
'Array too small, needs at least %d elements!' %
|
'array too small, needs at least %d elements!' %
|
||||||
self.minlen)
|
self.minlen)
|
||||||
if self.maxlen is not None and len(value) > self.maxlen:
|
if self.maxlen is not None and len(value) > self.maxlen:
|
||||||
raise BadValueError(
|
raise BadValueError(
|
||||||
'Array too big, holds at most %d elements!' % self.maxlen)
|
'array too big, holds at most %d elements!' % self.maxlen)
|
||||||
# apply subtype valiation to all elements and return as list
|
|
||||||
return tuple(self.members(elem) for elem in value)
|
|
||||||
except TypeError:
|
except TypeError:
|
||||||
raise BadValueError('%s can not be converted to ArrayOf DataType!'
|
raise BadValueError('%s can not be converted to ArrayOf DataType!'
|
||||||
% type(value).__name__) from None
|
% type(value).__name__) from None
|
||||||
|
|
||||||
|
def __call__(self, value):
|
||||||
|
self.check_type(value)
|
||||||
|
try:
|
||||||
|
return tuple(self.members(v) for v in value)
|
||||||
|
except Exception as e:
|
||||||
|
raise BadValueError('can not convert some array elements') from e
|
||||||
|
|
||||||
|
def validate(self, value, previous=None):
|
||||||
|
self.check_type(value)
|
||||||
|
try:
|
||||||
|
if previous:
|
||||||
|
return tuple(self.members.validate(v, p) for v, p in zip(value, previous))
|
||||||
|
return tuple(self.members.validate(v) for v in value)
|
||||||
|
except Exception as e:
|
||||||
|
raise BadValueError('some array elements are invalid') from e
|
||||||
|
|
||||||
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 [self.members.export_value(elem) for elem in value]
|
return [self.members.export_value(elem) for elem in value]
|
||||||
@ -845,18 +882,30 @@ class TupleOf(DataType):
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return 'TupleOf(%s)' % ', '.join([repr(st) for st in self.members])
|
return 'TupleOf(%s)' % ', '.join([repr(st) for st in self.members])
|
||||||
|
|
||||||
def __call__(self, value):
|
def check_type(self, value):
|
||||||
"""return the validated value or raise"""
|
|
||||||
# keep the ordering!
|
|
||||||
try:
|
try:
|
||||||
if len(value) != len(self.members):
|
if len(value) != len(self.members):
|
||||||
raise BadValueError(
|
raise BadValueError(
|
||||||
'Illegal number of Arguments! Need %d arguments.' % len(self.members))
|
'tuple needs %d elements' % len(self.members))
|
||||||
# validate elements and return as list
|
except TypeError:
|
||||||
return tuple(sub(elem)
|
raise BadValueError('%s can not be converted to TupleOf DataType!'
|
||||||
for sub, elem in zip(self.members, value))
|
% type(value).__name__) from None
|
||||||
except Exception as exc:
|
|
||||||
raise BadValueError('Can not validate:', str(exc)) from None
|
def __call__(self, value):
|
||||||
|
self.check_type(value)
|
||||||
|
try:
|
||||||
|
return tuple(sub(elem) for sub, elem in zip(self.members, value))
|
||||||
|
except Exception as e:
|
||||||
|
raise BadValueError('can not convert some tuple elements') from e
|
||||||
|
|
||||||
|
def validate(self, value, previous=None):
|
||||||
|
self.check_type(value)
|
||||||
|
try:
|
||||||
|
if previous is None:
|
||||||
|
return tuple(sub.validate(elem) for sub, elem in zip(self.members, value))
|
||||||
|
return tuple(sub.validate(v, p) for sub, v, p in zip(self.members, value, previous))
|
||||||
|
except Exception as e:
|
||||||
|
raise BadValueError('some tuple elements are invalid') from e
|
||||||
|
|
||||||
def export_value(self, value):
|
def export_value(self, value):
|
||||||
"""returns a python object fit for serialisation"""
|
"""returns a python object fit for serialisation"""
|
||||||
@ -908,7 +957,7 @@ class StructOf(DataType):
|
|||||||
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!')
|
||||||
self.optional = list(optional or [])
|
self.optional = list(members if optional is None else optional)
|
||||||
for name, subtype in list(members.items()):
|
for name, subtype in list(members.items()):
|
||||||
if not isinstance(subtype, DataType):
|
if not isinstance(subtype, DataType):
|
||||||
raise ProgrammingError(
|
raise ProgrammingError(
|
||||||
@ -926,7 +975,7 @@ class StructOf(DataType):
|
|||||||
def export_datatype(self):
|
def export_datatype(self):
|
||||||
res = dict(type='struct', members=dict((n, s.export_datatype())
|
res = dict(type='struct', members=dict((n, s.export_datatype())
|
||||||
for n, s in list(self.members.items())))
|
for n, s in list(self.members.items())))
|
||||||
if self.optional:
|
if set(self.optional) != set(self.members):
|
||||||
res['optional'] = self.optional
|
res['optional'] = self.optional
|
||||||
return res
|
return res
|
||||||
|
|
||||||
@ -936,16 +985,38 @@ class StructOf(DataType):
|
|||||||
['%s=%s' % (n, repr(st)) for n, st in list(self.members.items())]), opt)
|
['%s=%s' % (n, repr(st)) for n, st in list(self.members.items())]), opt)
|
||||||
|
|
||||||
def __call__(self, value):
|
def __call__(self, value):
|
||||||
"""return the validated value or raise"""
|
|
||||||
try:
|
try:
|
||||||
missing = set(self.members) - set(value) - set(self.optional)
|
if set(dict(value)) != set(self.members):
|
||||||
if missing:
|
raise BadValueError('member names do not match') from None
|
||||||
raise BadValueError('missing values for keys %r' % list(missing))
|
except TypeError:
|
||||||
# validate elements and return as dict
|
raise BadValueError('%s can not be converted a StructOf'
|
||||||
|
% type(value).__name__) from None
|
||||||
|
try:
|
||||||
return ImmutableDict((str(k), self.members[k](v))
|
return ImmutableDict((str(k), self.members[k](v))
|
||||||
for k, v in list(value.items()))
|
for k, v in list(value.items()))
|
||||||
except Exception as exc:
|
except Exception as e:
|
||||||
raise BadValueError('Can not validate %s: %s' % (repr(value), str(exc))) from None
|
raise BadValueError('can not convert some struct element') from e
|
||||||
|
|
||||||
|
def validate(self, value, previous=None):
|
||||||
|
try:
|
||||||
|
superfluous = set(dict(value)) - set(self.members)
|
||||||
|
except TypeError:
|
||||||
|
raise BadValueError('%s can not be converted a StructOf'
|
||||||
|
% type(value).__name__) from None
|
||||||
|
if superfluous - set(self.optional):
|
||||||
|
raise BadValueError('struct contains superfluous members: %s' % ', '.join(superfluous))
|
||||||
|
missing = set(self.members) - set(value) - set(self.optional)
|
||||||
|
if missing:
|
||||||
|
raise BadValueError('missing struct elements: %s' % ', '.join(missing))
|
||||||
|
try:
|
||||||
|
if previous is None:
|
||||||
|
return ImmutableDict((str(k), self.members[k].validate(v))
|
||||||
|
for k, v in list(value.items()))
|
||||||
|
result = dict(previous)
|
||||||
|
result.update(((k, self.members[k].validate(v, previous[k])) for k, v in value.items()))
|
||||||
|
return ImmutableDict(result)
|
||||||
|
except Exception as e:
|
||||||
|
raise BadValueError('some struct elements are invalid') from e
|
||||||
|
|
||||||
def export_value(self, value):
|
def export_value(self, value):
|
||||||
"""returns a python object fit for serialisation"""
|
"""returns a python object fit for serialisation"""
|
||||||
@ -1015,8 +1086,7 @@ class CommandType(DataType):
|
|||||||
return 'CommandType(%s, %s)' % (repr(self.argument), repr(self.result))
|
return 'CommandType(%s, %s)' % (repr(self.argument), repr(self.result))
|
||||||
|
|
||||||
def __call__(self, value):
|
def __call__(self, value):
|
||||||
"""return the validated argument value or raise"""
|
raise ProgrammingError('commands can not be converted to a value')
|
||||||
return self.argument(value)
|
|
||||||
|
|
||||||
def export_value(self, value):
|
def export_value(self, value):
|
||||||
raise ProgrammingError('values of type command can not be transported!')
|
raise ProgrammingError('values of type command can not be transported!')
|
||||||
@ -1147,7 +1217,7 @@ class LimitsType(TupleOf):
|
|||||||
super().__init__(members, members)
|
super().__init__(members, members)
|
||||||
|
|
||||||
def __call__(self, value):
|
def __call__(self, value):
|
||||||
limits = TupleOf.__call__(self, value)
|
limits = TupleOf.validate(self, value)
|
||||||
if limits[1] < limits[0]:
|
if limits[1] < limits[0]:
|
||||||
raise BadValueError('Maximum Value %s must be greater than minimum value %s!' % (limits[1], limits[0]))
|
raise BadValueError('Maximum Value %s must be greater than minimum value %s!' % (limits[1], limits[0]))
|
||||||
return limits
|
return limits
|
||||||
|
@ -32,14 +32,12 @@ from frappy.datatypes import ArrayOf, BoolType, EnumType, FloatRange, \
|
|||||||
IntRange, StatusType, StringType, TextType, TupleOf, DiscouragedConversion
|
IntRange, StatusType, StringType, TextType, TupleOf, DiscouragedConversion
|
||||||
from frappy.errors import BadValueError, CommunicationFailedError, ConfigError, \
|
from frappy.errors import BadValueError, CommunicationFailedError, ConfigError, \
|
||||||
ProgrammingError, SECoPError, secop_error
|
ProgrammingError, SECoPError, secop_error
|
||||||
from frappy.lib import formatException, mkthread, UniqueObject, generalConfig
|
from frappy.lib import formatException, mkthread, UniqueObject
|
||||||
from frappy.lib.enum import Enum
|
from frappy.lib.enum import Enum
|
||||||
from frappy.params import Accessible, Command, Parameter
|
from frappy.params import Accessible, Command, Parameter
|
||||||
from frappy.properties import HasProperties, Property
|
from frappy.properties import HasProperties, Property
|
||||||
from frappy.logging import RemoteLogHandler, HasComlog
|
from frappy.logging import RemoteLogHandler, HasComlog
|
||||||
|
|
||||||
generalConfig.set_default('disable_value_range_check', False) # check for problematic value range by default
|
|
||||||
|
|
||||||
Done = UniqueObject('Done')
|
Done = UniqueObject('Done')
|
||||||
"""a special return value for a read/write function
|
"""a special return value for a read/write function
|
||||||
|
|
||||||
@ -805,7 +803,6 @@ class Readable(Module):
|
|||||||
|
|
||||||
class Writable(Readable):
|
class Writable(Readable):
|
||||||
"""basic writable module"""
|
"""basic writable module"""
|
||||||
disable_value_range_check = Property('disable value range check', BoolType(), default=False)
|
|
||||||
target = Parameter('target value of the module',
|
target = Parameter('target value of the module',
|
||||||
default=0, readonly=False, datatype=FloatRange(unit='$'))
|
default=0, readonly=False, datatype=FloatRange(unit='$'))
|
||||||
|
|
||||||
@ -821,13 +818,6 @@ class Writable(Readable):
|
|||||||
if type(value_dt) == type(target_dt):
|
if type(value_dt) == type(target_dt):
|
||||||
raise ConfigError('the target range extends beyond the value range') from None
|
raise ConfigError('the target range extends beyond the value range') from None
|
||||||
raise ProgrammingError('the datatypes of target and value are not compatible') from None
|
raise ProgrammingError('the datatypes of target and value are not compatible') from None
|
||||||
if isinstance(value_dt, FloatRange):
|
|
||||||
if (not self.disable_value_range_check and not generalConfig.disable_value_range_check
|
|
||||||
and value_dt.problematic_range(target_dt)):
|
|
||||||
self.log.error('the value range must be bigger than the target range!')
|
|
||||||
self.log.error('you may disable this error message by running the server with --relaxed')
|
|
||||||
self.log.error('or by setting the disable_value_range_check property of the module to True')
|
|
||||||
raise ConfigError('the value range must be bigger than the target range')
|
|
||||||
|
|
||||||
|
|
||||||
class Drivable(Writable):
|
class Drivable(Writable):
|
||||||
|
@ -66,7 +66,7 @@ class Property:
|
|||||||
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 = inspect.cleandoc(description)
|
self.description = inspect.cleandoc(description)
|
||||||
self.default = datatype.default if default is UNSET else datatype(default)
|
self.default = datatype.default if default is UNSET else datatype.validate(default)
|
||||||
self.datatype = datatype
|
self.datatype = datatype
|
||||||
self.extname = extname
|
self.extname = extname
|
||||||
self.export = export or bool(extname)
|
self.export = export or bool(extname)
|
||||||
@ -74,7 +74,7 @@ class Property:
|
|||||||
mandatory = default is UNSET
|
mandatory = default is UNSET
|
||||||
self.mandatory = mandatory
|
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
|
||||||
self.value = UNSET if value is UNSET else datatype(value)
|
self.value = UNSET if value is UNSET else datatype.validate(value)
|
||||||
self.name = name
|
self.name = name
|
||||||
|
|
||||||
def __get__(self, instance, owner):
|
def __get__(self, instance, owner):
|
||||||
@ -83,7 +83,7 @@ class Property:
|
|||||||
return instance.propertyValues.get(self.name, self.default)
|
return instance.propertyValues.get(self.name, self.default)
|
||||||
|
|
||||||
def __set__(self, instance, value):
|
def __set__(self, instance, value):
|
||||||
instance.propertyValues[self.name] = self.datatype(value)
|
instance.propertyValues[self.name] = self.datatype.validate(value)
|
||||||
|
|
||||||
def __set_name__(self, owner, name):
|
def __set_name__(self, owner, name):
|
||||||
self.name = name
|
self.name = name
|
||||||
@ -144,7 +144,7 @@ class HasProperties(HasDescriptors):
|
|||||||
po = po.copy()
|
po = po.copy()
|
||||||
try:
|
try:
|
||||||
# try to apply bare value to Property
|
# try to apply bare value to Property
|
||||||
po.value = po.datatype(value)
|
po.value = po.datatype.validate(value)
|
||||||
except BadValueError:
|
except BadValueError:
|
||||||
if callable(value):
|
if callable(value):
|
||||||
raise ProgrammingError('method %s.%s collides with property of %s' %
|
raise ProgrammingError('method %s.%s collides with property of %s' %
|
||||||
@ -158,7 +158,7 @@ class HasProperties(HasDescriptors):
|
|||||||
for pn, po in self.propertyDict.items():
|
for pn, po in self.propertyDict.items():
|
||||||
if po.mandatory:
|
if po.mandatory:
|
||||||
try:
|
try:
|
||||||
self.propertyValues[pn] = po.datatype(self.propertyValues[pn])
|
self.propertyValues[pn] = po.datatype.validate(self.propertyValues[pn])
|
||||||
except (KeyError, BadValueError):
|
except (KeyError, BadValueError):
|
||||||
raise ConfigError('%s needs a value of type %r!' % (pn, po.datatype)) from None
|
raise ConfigError('%s needs a value of type %r!' % (pn, po.datatype)) from None
|
||||||
for pn, po in self.propertyDict.items():
|
for pn, po in self.propertyDict.items():
|
||||||
@ -191,4 +191,4 @@ class HasProperties(HasDescriptors):
|
|||||||
# this is overwritten by Param.setProperty and DataType.setProperty
|
# this is overwritten by Param.setProperty and DataType.setProperty
|
||||||
# in oder to extend setting to inner properties
|
# in oder to extend setting to inner properties
|
||||||
# otherwise direct setting of self.<key> = value is preferred
|
# otherwise direct setting of self.<key> = value is preferred
|
||||||
self.propertyValues[key] = self.propertyDict[key].datatype(value)
|
self.propertyValues[key] = self.propertyDict[key].datatype.validate(value)
|
||||||
|
@ -249,7 +249,7 @@ class Dispatcher:
|
|||||||
% (modulename, pname))
|
% (modulename, pname))
|
||||||
|
|
||||||
# validate!
|
# validate!
|
||||||
value = pobj.datatype(value)
|
value = pobj.datatype.validate(value, previous=pobj.value)
|
||||||
# note: exceptions are handled in handle_request, not here!
|
# note: exceptions are handled in handle_request, not here!
|
||||||
getattr(moduleobj, 'write_' + pname)(value)
|
getattr(moduleobj, 'write_' + pname)(value)
|
||||||
# return value is ignored here, as already handled
|
# return value is ignored here, as already handled
|
||||||
|
@ -55,9 +55,11 @@ def test_FloatRange():
|
|||||||
assert dt.export_datatype() == {'type': 'double', 'min':-3.14, 'max':3.14}
|
assert dt.export_datatype() == {'type': 'double', 'min':-3.14, 'max':3.14}
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
dt(9)
|
dt.validate(9)
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
dt(-9)
|
dt.validate(-9)
|
||||||
|
dt(9) # convert, but do not check limits
|
||||||
|
dt(-9) # convert, but do not check limits
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
dt('XX')
|
dt('XX')
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
@ -106,9 +108,11 @@ def test_IntRange():
|
|||||||
assert dt.export_datatype() == {'type': 'int', 'min':-3, 'max':3}
|
assert dt.export_datatype() == {'type': 'int', 'min':-3, 'max':3}
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
dt(9)
|
dt.validate(9)
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
dt(-9)
|
dt.validate(-9)
|
||||||
|
dt(9) # convert, but do not check limits
|
||||||
|
dt(-9) # convert, but do not check limits
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
dt('XX')
|
dt('XX')
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
@ -142,9 +146,11 @@ def test_ScaledInteger():
|
|||||||
assert dt.export_datatype() == {'type': 'scaled', 'scale':0.01, 'min':-300, 'max':300}
|
assert dt.export_datatype() == {'type': 'scaled', 'scale':0.01, 'min':-300, 'max':300}
|
||||||
|
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
dt(9)
|
dt.validate(9)
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
dt(-9)
|
dt.validate(-9)
|
||||||
|
dt(9) # convert, but do not check limits
|
||||||
|
dt(-9) # convert, but do not check limits
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
dt('XX')
|
dt('XX')
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
@ -171,20 +177,23 @@ def test_ScaledInteger():
|
|||||||
'unit':'A'}
|
'unit':'A'}
|
||||||
assert dt.absolute_resolution == dt.scale
|
assert dt.absolute_resolution == dt.scale
|
||||||
|
|
||||||
dt = ScaledInteger(0.003, 0, 1, unit='X', fmtstr='%.1f',
|
dt = ScaledInteger(0.003, 0.4, 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)
|
||||||
assert dt.export_datatype() == {'type': 'scaled', 'scale':0.003, 'min':0, 'max':333,
|
assert dt.export_datatype() == {'type': 'scaled', 'scale':0.003, 'min':133, 'max':333,
|
||||||
'unit':'X', 'fmtstr':'%.1f',
|
'unit':'X', 'fmtstr':'%.1f',
|
||||||
'absolute_resolution':0.001,
|
'absolute_resolution':0.001,
|
||||||
'relative_resolution':1e-5}
|
'relative_resolution':1e-5}
|
||||||
assert dt(0.4) == 0.399
|
assert round(dt(0.7), 5) == 0.699
|
||||||
assert dt.format_value(0.4) == '0.4 X'
|
assert dt.format_value(0.6) == '0.6 X'
|
||||||
assert dt.format_value(0.4, '') == '0.4'
|
assert dt.format_value(0.6, '') == '0.6'
|
||||||
assert dt.format_value(0.4, 'Z') == '0.4 Z'
|
assert dt.format_value(0.6, 'Z') == '0.6 Z'
|
||||||
assert dt(1.0029) == 0.999
|
assert round(dt.validate(1.0004), 5) == 0.999 # rounded value within limit
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
dt(1.004)
|
dt.validate(1.006) # rounded value outside limit
|
||||||
|
assert round(dt.validate(0.398), 5) == 0.399 # rounded value within rounded limit
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
dt.validate(0.395) # rounded value outside limit
|
||||||
|
|
||||||
dt.setProperty('min', 1)
|
dt.setProperty('min', 1)
|
||||||
dt.setProperty('max', 0)
|
dt.setProperty('max', 0)
|
||||||
@ -456,7 +465,7 @@ def test_StructOf():
|
|||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
dt([99, 'X'])
|
dt([99, 'X'])
|
||||||
with pytest.raises(ValueError):
|
with pytest.raises(ValueError):
|
||||||
dt(dict(a_string='XXX', an_int=1811))
|
dt.validate(dict(a_string='XXX', an_int=1811))
|
||||||
|
|
||||||
assert dt(dict(a_string='XXX', an_int=8)) == {'a_string': 'XXX',
|
assert dt(dict(a_string='XXX', an_int=8)) == {'a_string': 'XXX',
|
||||||
'an_int': 8}
|
'an_int': 8}
|
||||||
@ -638,7 +647,7 @@ def test_get_datatype():
|
|||||||
(StringType(10, 10), StringType()),
|
(StringType(10, 10), StringType()),
|
||||||
(ArrayOf(StringType(), 3, 5), ArrayOf(StringType(), 3, 6)),
|
(ArrayOf(StringType(), 3, 5), ArrayOf(StringType(), 3, 6)),
|
||||||
(TupleOf(StringType(), BoolType()), TupleOf(StringType(), IntRange())),
|
(TupleOf(StringType(), BoolType()), TupleOf(StringType(), IntRange())),
|
||||||
(StructOf(a=FloatRange(-1,1)), StructOf(a=FloatRange(), b=BoolType(), optional=['b'])),
|
(StructOf(a=FloatRange(-1,1), b=BoolType()), StructOf(a=FloatRange(), b=BoolType(), optional=['b'])),
|
||||||
])
|
])
|
||||||
def test_oneway_compatible(dt, contained_in):
|
def test_oneway_compatible(dt, contained_in):
|
||||||
dt.compatible(contained_in)
|
dt.compatible(contained_in)
|
||||||
|
@ -31,7 +31,6 @@ from frappy.errors import ProgrammingError, ConfigError
|
|||||||
from frappy.modules import Communicator, Drivable, Readable, Module
|
from frappy.modules import Communicator, Drivable, Readable, Module
|
||||||
from frappy.params import Command, Parameter
|
from frappy.params import Command, Parameter
|
||||||
from frappy.rwhandler import ReadHandler, WriteHandler, nopoll
|
from frappy.rwhandler import ReadHandler, WriteHandler, nopoll
|
||||||
from frappy.lib import generalConfig
|
|
||||||
|
|
||||||
|
|
||||||
class DispatcherStub:
|
class DispatcherStub:
|
||||||
@ -235,7 +234,7 @@ def test_ModuleMagic():
|
|||||||
assert o2.parameters['a1'].datatype.unit == 'mm/s'
|
assert o2.parameters['a1'].datatype.unit == 'mm/s'
|
||||||
cfg = Newclass2.configurables
|
cfg = Newclass2.configurables
|
||||||
assert set(cfg.keys()) == {
|
assert set(cfg.keys()) == {
|
||||||
'export', 'group', 'description', 'disable_value_range_check', 'features',
|
'export', 'group', 'description', 'features',
|
||||||
'meaning', 'visibility', 'implementation', 'interface_classes', 'target', 'stop',
|
'meaning', 'visibility', 'implementation', 'interface_classes', 'target', 'stop',
|
||||||
'status', 'param1', 'param2', 'cmd', 'a2', 'pollinterval', 'slowinterval', 'b2',
|
'status', 'param1', 'param2', 'cmd', 'a2', 'pollinterval', 'slowinterval', 'b2',
|
||||||
'cmd2', 'value', 'a1'}
|
'cmd2', 'value', 'a1'}
|
||||||
@ -631,7 +630,7 @@ def test_problematic_value_range():
|
|||||||
obj = Mod('obj', logger, {'description': '', 'value':{'max': 10.1}}, srv) # pylint: disable=unused-variable
|
obj = Mod('obj', logger, {'description': '', 'value':{'max': 10.1}}, srv) # pylint: disable=unused-variable
|
||||||
|
|
||||||
with pytest.raises(ConfigError):
|
with pytest.raises(ConfigError):
|
||||||
obj = Mod('obj', logger, {'description': ''}, srv)
|
obj = Mod('obj', logger, {'description': '', 'value.max': 9.9}, srv)
|
||||||
|
|
||||||
class Mod2(Drivable):
|
class Mod2(Drivable):
|
||||||
value = Parameter('', FloatRange(), default=0)
|
value = Parameter('', FloatRange(), default=0)
|
||||||
@ -640,17 +639,10 @@ def test_problematic_value_range():
|
|||||||
obj = Mod2('obj', logger, {'description': ''}, srv)
|
obj = Mod2('obj', logger, {'description': ''}, srv)
|
||||||
obj = Mod2('obj', logger, {'description': '', 'target':{'min': 0, 'max': 10}}, srv)
|
obj = Mod2('obj', logger, {'description': '', 'target':{'min': 0, 'max': 10}}, srv)
|
||||||
|
|
||||||
with pytest.raises(ConfigError):
|
|
||||||
obj = Mod('obj', logger, {
|
obj = Mod('obj', logger, {
|
||||||
'value': {'min': 0, 'max': 10},
|
'value': {'min': 0, 'max': 10},
|
||||||
'target': {'min': 0, 'max': 10}, 'description': ''}, srv)
|
'target': {'min': 0, 'max': 10}, 'description': ''}, srv)
|
||||||
|
|
||||||
obj = Mod('obj', logger, {'disable_value_range_check': True,
|
|
||||||
'value': {'min': 0, 'max': 10},
|
|
||||||
'target': {'min': 0, 'max': 10}, 'description': ''}, srv)
|
|
||||||
|
|
||||||
generalConfig.defaults['disable_value_range_check'] = True
|
|
||||||
|
|
||||||
class Mod4(Drivable):
|
class Mod4(Drivable):
|
||||||
value = Parameter('', FloatRange(0, 10), default=0)
|
value = Parameter('', FloatRange(0, 10), default=0)
|
||||||
target = Parameter('', FloatRange(0, 10), default=0)
|
target = Parameter('', FloatRange(0, 10), default=0)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user