do not convert string to float

a read method should not reply on the automatic conversion
of the return value from string to a number.

- transitional solution with generalConfig.lazy_numer_validation
+ changing slighly generalInit mechanism: for above feature
  generalConfig.init is not required to be called (i.e. when
  used on the client side)

Change-Id: Ibecce1a45669273c105932acdc0908de55bfd1b9
Reviewed-on: https://forge.frm2.tum.de/review/c/sine2020/secop/playground/+/27516
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:
zolliker 2022-01-28 11:37:23 +01:00
parent 4f7083bc98
commit 26a0f2e078
5 changed files with 47 additions and 20 deletions

View File

@ -30,7 +30,7 @@ from base64 import b64decode, b64encode
from secop.errors import BadValueError, \ from secop.errors import BadValueError, \
ConfigError, ProgrammingError, ProtocolError ConfigError, ProgrammingError, ProtocolError
from secop.lib import clamp from secop.lib import clamp, generalConfig
from secop.lib.enum import Enum from secop.lib.enum import Enum
from secop.parse import Parser from secop.parse import Parser
from secop.properties import HasProperties, Property from secop.properties import HasProperties, Property
@ -49,6 +49,7 @@ __all__ = [
DEFAULT_MIN_INT = -16777216 DEFAULT_MIN_INT = -16777216
DEFAULT_MAX_INT = 16777216 DEFAULT_MAX_INT = 16777216
UNLIMITED = 1 << 64 # internal limit for integers, is probably high enough for any datatype size UNLIMITED = 1 << 64 # internal limit for integers, is probably high enough for any datatype size
generalConfig.defaults['lazy_number_validation'] = True # should be changed to False later
Parser = Parser() Parser = Parser()
@ -192,7 +193,10 @@ class FloatRange(DataType):
def __call__(self, value): def __call__(self, value):
try: try:
value = float(value) if generalConfig.lazy_number_validation:
value = float(value) # for legacy code
else:
value += 0.0 # do not accept strings here
except Exception: except Exception:
raise BadValueError('Can not convert %r to float' % value) from None raise BadValueError('Can not convert %r to float' % value) from None
# map +/-infty to +/-max possible number # map +/-infty to +/-max possible number
@ -265,7 +269,10 @@ class IntRange(DataType):
def __call__(self, value): def __call__(self, value):
try: try:
fvalue = float(value) if generalConfig.lazy_number_validation:
fvalue = float(value) # for legacy code
else:
fvalue = value + 0.0 # do not accept strings here
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 %r to int' % value) from None
@ -369,7 +376,10 @@ class ScaledInteger(DataType):
def __call__(self, value): def __call__(self, value):
try: try:
value = float(value) if generalConfig.lazy_number_validation:
value = float(value) # for legacy code
else:
value += 0.0 # do not accept strings here
except Exception: except Exception:
raise BadValueError('Can not convert %r to float' % value) from None raise BadValueError('Can not convert %r to float' % value) from None
prec = max(self.scale, abs(value * self.relative_resolution), prec = max(self.scale, abs(value * self.relative_resolution),

View File

@ -32,8 +32,10 @@ from os import environ, path
class GeneralConfig: class GeneralConfig:
def __init__(self): def __init__(self):
self._config = None self._config = None
self.defaults = {} #: default values. may be set before or after :meth:`init`
def init(self, configfile=None): def init(self, configfile=None):
cfg = {} cfg = {}
@ -82,7 +84,13 @@ class GeneralConfig:
def __getitem__(self, key): def __getitem__(self, key):
try: try:
return self._config[key] return self._config[key]
except KeyError:
return self.defaults[key]
except TypeError: except TypeError:
if key in self.defaults:
# accept retrieving defaults before init
# e.g. 'lazy_number_validation' in secop.datatypes
return self.defaults[key]
raise TypeError('generalConfig.init() has to be called first') from None raise TypeError('generalConfig.init() has to be called first') from None
def get(self, key, default=None): def get(self, key, default=None):
@ -101,18 +109,14 @@ class GeneralConfig:
"""goodie: use generalConfig.<key> instead of generalConfig.get('<key>')""" """goodie: use generalConfig.<key> instead of generalConfig.get('<key>')"""
return self.get(key) return self.get(key)
def __setattr__(self, key, value):
if key == '_config':
super().__setattr__(key, value)
return
if hasattr(type(self), key):
raise AttributeError('can not set generalConfig.%s' % key)
self._config[key] = value # for test purposes
@property @property
def initialized(self): def initialized(self):
return bool(self._config) return bool(self._config)
def testinit(self, **kwds):
"""for test purposes"""
self._config = kwds
generalConfig = GeneralConfig() generalConfig = GeneralConfig()

View File

@ -29,6 +29,7 @@ from secop.datatypes import ArrayOf, BLOBType, BoolType, \
CommandType, ConfigError, DataType, Enum, EnumType, FloatRange, \ CommandType, ConfigError, DataType, Enum, EnumType, FloatRange, \
IntRange, ProgrammingError, ScaledInteger, StatusType, \ IntRange, ProgrammingError, ScaledInteger, StatusType, \
StringType, StructOf, TextType, TupleOf, get_datatype StringType, StructOf, TextType, TupleOf, get_datatype
from secop.lib import generalConfig
def copytest(dt): def copytest(dt):
@ -36,6 +37,7 @@ def copytest(dt):
assert dt.export_datatype() == dt.copy().export_datatype() assert dt.export_datatype() == dt.copy().export_datatype()
assert dt != dt.copy() assert dt != dt.copy()
def test_DataType(): def test_DataType():
dt = DataType() dt = DataType()
with pytest.raises(NotImplementedError): with pytest.raises(NotImplementedError):
@ -116,7 +118,6 @@ def test_IntRange():
dt('1.3') dt('1.3')
dt(1) dt(1)
dt(0) dt(0)
dt('1')
with pytest.raises(ProgrammingError): with pytest.raises(ProgrammingError):
IntRange('xc', 'Yx') IntRange('xc', 'Yx')
@ -132,6 +133,7 @@ def test_IntRange():
with pytest.raises(ConfigError): with pytest.raises(ConfigError):
dt.checkProperties() dt.checkProperties()
def test_ScaledInteger(): def test_ScaledInteger():
dt = ScaledInteger(0.01, -3, 3) dt = ScaledInteger(0.01, -3, 3)
copytest(dt) copytest(dt)
@ -407,6 +409,7 @@ def test_ArrayOf():
dt = ArrayOf(EnumType('myenum', single=0), 5) dt = ArrayOf(EnumType('myenum', single=0), 5)
copytest(dt) copytest(dt)
def test_TupleOf(): def test_TupleOf():
# test constructor catching illegal arguments # test constructor catching illegal arguments
with pytest.raises(ValueError): with pytest.raises(ValueError):
@ -641,6 +644,7 @@ def test_oneway_compatible(dt, contained_in):
with pytest.raises(ValueError): with pytest.raises(ValueError):
contained_in.compatible(dt) contained_in.compatible(dt)
@pytest.mark.parametrize('dt1, dt2', [ @pytest.mark.parametrize('dt1, dt2', [
(FloatRange(-5.5, 5.5), ScaledInteger(10, -5.5, 5.5)), (FloatRange(-5.5, 5.5), ScaledInteger(10, -5.5, 5.5)),
(IntRange(0,1), BoolType()), (IntRange(0,1), BoolType()),
@ -650,6 +654,7 @@ def test_twoway_compatible(dt1, dt2):
dt1.compatible(dt1) dt1.compatible(dt1)
dt2.compatible(dt2) dt2.compatible(dt2)
@pytest.mark.parametrize('dt1, dt2', [ @pytest.mark.parametrize('dt1, dt2', [
(StringType(), FloatRange()), (StringType(), FloatRange()),
(IntRange(-10, 10), StringType()), (IntRange(-10, 10), StringType()),
@ -665,3 +670,12 @@ def test_incompatible(dt1, dt2):
dt1.compatible(dt2) dt1.compatible(dt2)
with pytest.raises(ValueError): with pytest.raises(ValueError):
dt2.compatible(dt1) dt2.compatible(dt1)
@pytest.mark.parametrize('dt', [FloatRange(), IntRange(), ScaledInteger(1)])
def test_lazy_validation(dt):
generalConfig.defaults['lazy_number_validation'] = True
dt('0')
generalConfig.defaults['lazy_number_validation'] = False
with pytest.raises(ValueError):
dt('0')

View File

@ -112,8 +112,7 @@ def init_(monkeypatch):
def communicate(self, request): def communicate(self, request):
self.comLog('> %s', request) self.comLog('> %s', request)
generalConfig.init() generalConfig.testinit(logger_root='frappy', comlog=comlog)
generalConfig.comlog = comlog
logger.init(console_level) logger.init(console_level)
self.srv = ServerStub() self.srv = ServerStub()
@ -139,7 +138,7 @@ def init_(monkeypatch):
yield Playground yield Playground
# revert settings # revert settings
generalConfig.__init__() generalConfig.testinit()
logger.__init__() logger.__init__()

View File

@ -38,10 +38,10 @@ V_test_Property = [
[Prop(StringType(), 'default', extname='extname', mandatory=False), [Prop(StringType(), 'default', extname='extname', mandatory=False),
dict(default='default', extname='extname', export=True, mandatory=False) dict(default='default', extname='extname', export=True, mandatory=False)
], ],
[Prop(IntRange(), '42', export=True, name='custom', mandatory=True), [Prop(IntRange(), 42, export=True, name='custom', mandatory=True),
dict(default=42, extname='_custom', export=True, mandatory=True), dict(default=42, extname='_custom', export=True, mandatory=True),
], ],
[Prop(IntRange(), '42', export=True, name='name'), [Prop(IntRange(), 42, export=True, name='name'),
dict(default=42, extname='_name', export=True, mandatory=False) dict(default=42, extname='_name', export=True, mandatory=False)
], ],
[Prop(IntRange(), 42, '_extname', mandatory=True), [Prop(IntRange(), 42, '_extname', mandatory=True),
@ -85,12 +85,12 @@ def test_Property_basic():
Property('') Property('')
with pytest.raises(ValueError): with pytest.raises(ValueError):
Property('', 1) Property('', 1)
Property('', IntRange(), '42', 'extname', False, False) Property('', IntRange(), 42, 'extname', False, False)
def test_Properties(): def test_Properties():
class Cls(HasProperties): class Cls(HasProperties):
aa = Property('', IntRange(0, 99), '42', export=True) aa = Property('', IntRange(0, 99), 42, export=True)
bb = Property('', IntRange(), 0, export=False) bb = Property('', IntRange(), 0, export=False)
assert Cls.aa.default == 42 assert Cls.aa.default == 42