split BadValue into WrongType and RangeError

in order to match SECoP specification

fixes #4668

Change-Id: Ica73a8171536ccc324cf8db915347a6263c2d736
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/30625
Tested-by: Jenkins Automated Tests <pedersen+jenkins@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:
2023-03-08 14:13:40 +01:00
parent 20506a8393
commit 0560668b91
9 changed files with 231 additions and 199 deletions

View File

@@ -28,7 +28,7 @@
import sys
from base64 import b64decode, b64encode
from frappy.errors import BadValueError, \
from frappy.errors import WrongTypeError, RangeError, \
ConfigError, ProgrammingError, ProtocolError
from frappy.lib import clamp, generalConfig
from frappy.lib.enum import Enum
@@ -45,7 +45,7 @@ UNLIMITED = 1 << 64 # internal limit for integers, is probably high enough for
Parser = Parser()
class DiscouragedConversion(BadValueError):
class DiscouragedConversion(WrongTypeError):
"""the discouraged conversion string - > float happened"""
log_message = True
@@ -230,7 +230,7 @@ class FloatRange(HasUnit, DataType):
try:
value = float(value)
except Exception:
raise BadValueError('can not convert %s to a float' % shortrepr(value)) from None
raise WrongTypeError('can not convert %s to a float' % shortrepr(value)) from None
if not generalConfig.lazy_number_validation:
raise DiscouragedConversion('automatic string to float conversion no longer supported') from None
@@ -245,8 +245,8 @@ class FloatRange(HasUnit, DataType):
if self.min - prec <= value <= self.max + prec:
# silently clamp when outside by not more than prec
return clamp(self.min, value, self.max)
raise BadValueError('%.14g must be between %d and %d' %
(value, self.min, self.max))
raise RangeError('%.14g must be between %d and %d' %
(value, self.min, self.max))
def __repr__(self):
hints = self.get_info()
@@ -277,7 +277,7 @@ class FloatRange(HasUnit, DataType):
def compatible(self, other):
if not isinstance(other, (FloatRange, ScaledInteger)):
raise BadValueError('incompatible datatypes')
raise WrongTypeError('incompatible datatypes')
# avoid infinity
other.validate(max(sys.float_info.min, self.min))
other.validate(min(sys.float_info.max, self.max))
@@ -316,11 +316,11 @@ class IntRange(DataType):
fvalue = float(value)
value = int(value)
except Exception:
raise BadValueError('can not convert %s to an int' % shortrepr(value)) from None
raise WrongTypeError('can not convert %s to an int' % shortrepr(value)) from None
if not generalConfig.lazy_number_validation:
raise DiscouragedConversion('automatic string to float conversion no longer supported') from None
if round(fvalue) != fvalue:
raise BadValueError('%r should be an int')
raise WrongTypeError('%r should be an int')
return value
def validate(self, value, previous=None):
@@ -329,8 +329,8 @@ class IntRange(DataType):
# 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))
raise RangeError('%r must be between %d and %d' %
(value, self.min, self.max))
def __repr__(self):
args = (self.min, self.max)
@@ -364,7 +364,7 @@ class IntRange(DataType):
# the following loop will not cycle more than the number of Enum elements
for i in range(self.min, self.max + 1):
other(i)
raise BadValueError('incompatible datatypes')
raise WrongTypeError('incompatible datatypes')
class ScaledInteger(HasUnit, DataType):
@@ -388,7 +388,10 @@ class ScaledInteger(HasUnit, DataType):
def __init__(self, scale, minval=None, maxval=None, absolute_resolution=None, **kwds):
super().__init__()
scale = float(scale)
try:
scale = float(scale)
except (ValueError, TypeError) as e:
raise ProgrammingError(e) from None
if absolute_resolution is None:
absolute_resolution = scale
self.set_properties(
@@ -404,7 +407,7 @@ class ScaledInteger(HasUnit, DataType):
# check values
if '%' not in self.fmtstr:
raise BadValueError('Invalid fmtstr!')
raise ConfigError('Invalid fmtstr!')
# Remark: Datatype.copy() will round min, max to a multiple of self.scale
# this should be o.k.
@@ -434,7 +437,7 @@ class ScaledInteger(HasUnit, DataType):
try:
value = float(value)
except Exception:
raise BadValueError('can not convert %s to float' % shortrepr(value)) from None
raise WrongTypeError('can not convert %s to float' % shortrepr(value)) from None
if not generalConfig.lazy_number_validation:
raise DiscouragedConversion('automatic string to float conversion no longer supported') from None
intval = int(round(value / self.scale))
@@ -446,8 +449,8 @@ class ScaledInteger(HasUnit, DataType):
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))
raise RangeError('%.14g must be between between %g and %g' %
(value, self.min, self.max))
def __repr__(self):
hints = self.get_info(scale=float('%g' % self.scale),
@@ -476,7 +479,7 @@ class ScaledInteger(HasUnit, DataType):
def compatible(self, other):
if not isinstance(other, (FloatRange, ScaledInteger)):
raise BadValueError('incompatible datatypes')
raise WrongTypeError('incompatible datatypes')
other.validate(self.min)
other.validate(self.max)
@@ -522,7 +525,9 @@ class EnumType(DataType):
try:
return self._enum[value]
except (KeyError, TypeError): # TypeError will be raised when value is not hashable
raise BadValueError('%s is not a member of enum %r' % (shortrepr(value), self._enum)) from None
if isinstance(value, (int, str)):
raise RangeError('%s is not a member of enum %r' % (shortrepr(value), self._enum)) from None
raise WrongTypeError('%s must be either int or str for an enum value' % (shortrepr(value))) from None
def from_string(self, text):
return self(text)
@@ -570,13 +575,13 @@ class BLOBType(DataType):
def __call__(self, value):
"""accepts bytes only"""
if not isinstance(value, bytes):
raise BadValueError('%s must be of type bytes' % shortrepr(value))
raise WrongTypeError('%s must be of type bytes' % shortrepr(value))
size = len(value)
if size < self.minbytes:
raise BadValueError(
raise RangeError(
'%r must be at least %d bytes long!' % (value, self.minbytes))
if size > self.maxbytes:
raise BadValueError(
raise RangeError(
'%r must be at most %d bytes long!' % (value, self.maxbytes))
return value
@@ -599,9 +604,9 @@ class BLOBType(DataType):
def compatible(self, other):
try:
if self.minbytes < other.minbytes or self.maxbytes > other.maxbytes:
raise BadValueError('incompatible datatypes')
raise RangeError('incompatible datatypes')
except AttributeError:
raise BadValueError('incompatible datatypes') from None
raise WrongTypeError('incompatible datatypes') from None
class StringType(DataType):
@@ -635,21 +640,21 @@ class StringType(DataType):
def __call__(self, value):
"""accepts strings only"""
if not isinstance(value, str):
raise BadValueError('%s has the wrong type!' % shortrepr(value))
raise WrongTypeError('%s has the wrong type!' % shortrepr(value))
if not self.isUTF8:
try:
value.encode('ascii')
except UnicodeEncodeError:
raise BadValueError('%s contains non-ascii character!' % shortrepr(value)) from None
raise RangeError('%s contains non-ascii character!' % shortrepr(value)) from None
size = len(value)
if size < self.minchars:
raise BadValueError(
raise RangeError(
'%s must be at least %d chars long!' % (shortrepr(value), self.minchars))
if size > self.maxchars:
raise BadValueError(
raise RangeError(
'%s must be at most %d chars long!' % (shortrepr(value), self.maxchars))
if '\0' in value:
raise BadValueError(
raise RangeError(
'Strings are not allowed to embed a \\0! Use a Blob instead!')
return value
@@ -672,9 +677,9 @@ class StringType(DataType):
try:
if self.minchars < other.minchars or self.maxchars > other.maxchars or \
self.isUTF8 > other.isUTF8:
raise BadValueError('incompatible datatypes')
raise RangeError('incompatible datatypes')
except AttributeError:
raise BadValueError('incompatible datatypes') from None
raise WrongTypeError('incompatible datatypes') from None
# TextType is a special StringType intended for longer texts (i.e. embedding \n),
@@ -715,7 +720,7 @@ class BoolType(DataType):
return False
if value in [1, '1', 'True', 'true', 'yes', 'on', True]:
return True
raise BadValueError('%s is not a boolean value!' % shortrepr(value))
raise WrongTypeError('%s is not a boolean value!' % shortrepr(value))
def export_value(self, value):
"""returns a python object fit for serialisation"""
@@ -757,7 +762,7 @@ class ArrayOf(DataType):
def __init__(self, members, minlen=0, maxlen=None):
super().__init__()
if not isinstance(members, DataType):
raise BadValueError(
raise ProgrammingError(
'ArrayOf only works with a DataType as first argument!')
# one argument -> exactly that size
# argument default to 100
@@ -800,15 +805,15 @@ class ArrayOf(DataType):
try:
# check number of elements
if self.minlen is not None and len(value) < self.minlen:
raise BadValueError(
raise RangeError(
'array too small, needs at least %d elements!' %
self.minlen)
if self.maxlen is not None and len(value) > self.maxlen:
raise BadValueError(
raise RangeError(
'array too big, holds at most %d elements!' % self.maxlen)
except TypeError:
raise BadValueError('%s can not be converted to ArrayOf DataType!'
% type(value).__name__) from None
raise WrongTypeError('%s can not be converted to ArrayOf DataType!'
% type(value).__name__) from None
def __call__(self, value):
"""accepts any sequence, converts to tuple (immutable!)"""
@@ -816,7 +821,8 @@ class ArrayOf(DataType):
try:
return tuple(self.members(v) for v in value)
except Exception as e:
raise BadValueError('can not convert some array elements') from e
errcls = RangeError if isinstance(e, RangeError) else WrongTypeError
raise errcls('can not convert some array elements') from e
def validate(self, value, previous=None):
self.check_type(value)
@@ -825,7 +831,8 @@ class ArrayOf(DataType):
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
errcls = RangeError if isinstance(e, RangeError) else WrongTypeError
raise errcls('some array elements are invalid') from e
def export_value(self, value):
"""returns a python object fit for serialisation"""
@@ -852,10 +859,10 @@ class ArrayOf(DataType):
def compatible(self, other):
try:
if self.minlen < other.minlen or self.maxlen > other.maxlen:
raise BadValueError('incompatible datatypes')
raise RangeError('incompatible datatypes')
self.members.compatible(other.members)
except AttributeError:
raise BadValueError('incompatible datatypes') from None
raise WrongTypeError('incompatible datatypes') from None
def set_main_unit(self, unit):
self.members.set_main_unit(unit)
@@ -869,10 +876,10 @@ class TupleOf(DataType):
def __init__(self, *members):
super().__init__()
if not members:
raise BadValueError('Empty tuples are not allowed!')
raise ProgrammingError('Empty tuples are not allowed!')
for subtype in members:
if not isinstance(subtype, DataType):
raise BadValueError(
raise ProgrammingError(
'TupleOf only works with DataType objs as arguments!')
self.members = members
self.default = tuple(el.default for el in members)
@@ -890,11 +897,11 @@ class TupleOf(DataType):
def check_type(self, value):
try:
if len(value) != len(self.members):
raise BadValueError(
raise WrongTypeError(
'tuple needs %d elements' % len(self.members))
except TypeError:
raise BadValueError('%s can not be converted to TupleOf DataType!'
% type(value).__name__) from None
raise WrongTypeError('%s can not be converted to TupleOf DataType!'
% type(value).__name__) from None
def __call__(self, value):
"""accepts any sequence, converts to tuple"""
@@ -902,7 +909,8 @@ class TupleOf(DataType):
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
errcls = RangeError if isinstance(e, RangeError) else WrongTypeError
raise errcls('can not convert some tuple elements') from e
def validate(self, value, previous=None):
self.check_type(value)
@@ -911,7 +919,8 @@ class TupleOf(DataType):
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
errcls = RangeError if isinstance(e, RangeError) else WrongTypeError
raise errcls('some tuple elements are invalid') from e
def export_value(self, value):
"""returns a python object fit for serialisation"""
@@ -933,9 +942,9 @@ class TupleOf(DataType):
def compatible(self, other):
if not isinstance(other, TupleOf):
raise BadValueError('incompatible datatypes')
raise WrongTypeError('incompatible datatypes')
if len(self.members) != len(other.members):
raise BadValueError('incompatible datatypes')
raise WrongTypeError('incompatible datatypes')
for a, b in zip(self.members, other.members):
a.compatible(b)
@@ -962,7 +971,7 @@ class StructOf(DataType):
super().__init__()
self.members = members
if not members:
raise BadValueError('Empty structs are not allowed!')
raise ProgrammingError('Empty structs are not allowed!')
self.optional = list(members if optional is None else optional)
for name, subtype in list(members.items()):
if not isinstance(subtype, DataType):
@@ -994,27 +1003,28 @@ class StructOf(DataType):
"""accepts any mapping, returns an immutable dict"""
try:
if set(dict(value)) != set(self.members):
raise BadValueError('member names do not match') from None
raise WrongTypeError('member names do not match') from None
except TypeError:
raise BadValueError('%s can not be converted a StructOf'
% type(value).__name__) from None
raise WrongTypeError('%s can not be converted a StructOf'
% type(value).__name__) from None
try:
return ImmutableDict((str(k), self.members[k](v))
for k, v in list(value.items()))
except Exception as e:
raise BadValueError('can not convert some struct element') from e
errcls = RangeError if isinstance(e, RangeError) else WrongTypeError
raise errcls('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
raise WrongTypeError('%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))
raise WrongTypeError('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))
raise WrongTypeError('missing struct elements: %s' % ', '.join(missing))
try:
if previous is None:
return ImmutableDict((str(k), self.members[k].validate(v))
@@ -1023,7 +1033,8 @@ class StructOf(DataType):
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
errcls = RangeError if isinstance(e, RangeError) else WrongTypeError
raise errcls('some struct elements are invalid') from e
def export_value(self, value):
"""returns a python object fit for serialisation"""
@@ -1051,9 +1062,9 @@ class StructOf(DataType):
m.compatible(other.members[k])
mandatory.discard(k)
if mandatory:
raise BadValueError('incompatible datatypes')
raise WrongTypeError('incompatible datatypes')
except (AttributeError, TypeError, KeyError):
raise BadValueError('incompatible datatypes') from None
raise WrongTypeError('incompatible datatypes') from None
def set_main_unit(self, unit):
for member in self.members.values():
@@ -1071,10 +1082,10 @@ class CommandType(DataType):
super().__init__()
if argument is not None:
if not isinstance(argument, DataType):
raise BadValueError('CommandType: Argument type must be a DataType!')
raise ProgrammingError('CommandType: Argument type must be a DataType!')
if result is not None:
if not isinstance(result, DataType):
raise BadValueError('CommandType: Result type must be a DataType!')
raise ProgrammingError('CommandType: Result type must be a DataType!')
self.argument = argument
self.result = result
@@ -1118,7 +1129,7 @@ class CommandType(DataType):
if self.result != other.result: # not both are None
other.result.compatible(self.result)
except AttributeError:
raise BadValueError('incompatible datatypes') from None
raise WrongTypeError('incompatible datatypes') from None
# internally used datatypes (i.e. only for programming the SEC-node)
@@ -1204,7 +1215,7 @@ class OrType(DataType):
return t(value)
except Exception:
pass
raise BadValueError("Invalid Value, must conform to one of %s" % (', '.join((str(t) for t in self.types))))
raise WrongTypeError("Invalid Value, must conform to one of %s" % (', '.join((str(t) for t in self.types))))
Int8 = IntRange(-(1 << 7), (1 << 7) - 1)
@@ -1226,7 +1237,7 @@ class LimitsType(TupleOf):
"""accepts an ordered tuple of numeric member types"""
limits = TupleOf.validate(self, value)
if limits[1] < limits[0]:
raise BadValueError('Maximum Value %s must be greater than minimum value %s!' % (limits[1], limits[0]))
raise RangeError('Maximum Value %s must be greater than minimum value %s!' % (limits[1], limits[0]))
return limits
@@ -1294,9 +1305,9 @@ def get_datatype(json, pname=''):
kwargs = json.copy()
base = kwargs.pop('type')
except (TypeError, KeyError, AttributeError):
raise BadValueError('a data descriptor must be a dict containing a "type" key, not %r' % json) from None
raise WrongTypeError('a data descriptor must be a dict containing a "type" key, not %r' % json) from None
try:
return DATATYPES[base](pname=pname, **kwargs)
except Exception as e:
raise BadValueError('invalid data descriptor: %r (%s)' % (json, str(e))) from None
raise WrongTypeError('invalid data descriptor: %r (%s)' % (json, str(e))) from None