Merge "rework datatypes (setter should not check limits)"

This commit is contained in:
zolliker 2023-01-19 08:28:30 +01:00 committed by Gerrit Code Review
commit 82957c287d
6 changed files with 188 additions and 127 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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