fix missing import in change message

Transported values in a change must be converted first.
As this is only relevant for the exotic "scaled" and "blob"
datatypes, this was not detected yet.

- add tests
- suppress warning PytestUnhandledThreadExceptionWarning in tests
+ change import_value methods to raise no other exceptions than
  WrongTypeError and RangeError
+ simplify Command.do: as import_value already raises the
  appropriate error, no more try/except is needed

Change-Id: I299e511468dc0fcecff4c20cf8a917da38b70786
Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/32743
Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de>
Reviewed-by: Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
Reviewed-by: Alexander Zaft <a.zaft@fz-juelich.de>
Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
This commit is contained in:
2023-12-11 15:02:16 +01:00
parent dbacac71f9
commit adabf5c4b1
6 changed files with 47 additions and 33 deletions

View File

@@ -105,7 +105,7 @@ class DataType(HasProperties):
note: for importing from gui/configfile/commandline use :meth:`from_string` note: for importing from gui/configfile/commandline use :meth:`from_string`
instead. instead.
""" """
return value return self(value)
def format_value(self, value, unit=None): def format_value(self, value, unit=None):
"""format a value of this type into a str string """format a value of this type into a str string
@@ -259,10 +259,6 @@ class FloatRange(HasUnit, DataType):
"""returns a python object fit for serialisation""" """returns a python object fit for serialisation"""
return float(value) return float(value)
def import_value(self, value):
"""returns a python object from serialisation"""
return float(value)
def from_string(self, text): def from_string(self, text):
value = float(text) value = float(text)
return self(value) return self(value)
@@ -341,10 +337,6 @@ class IntRange(DataType):
"""returns a python object fit for serialisation""" """returns a python object fit for serialisation"""
return int(value) return int(value)
def import_value(self, value):
"""returns a python object from serialisation"""
return int(value)
def from_string(self, text): def from_string(self, text):
value = int(text) value = int(text)
return self(value) return self(value)
@@ -461,7 +453,10 @@ class ScaledInteger(HasUnit, DataType):
def import_value(self, value): def import_value(self, value):
"""returns a python object from serialisation""" """returns a python object from serialisation"""
return self.scale * int(value) try:
return self.scale * int(value)
except Exception:
raise WrongTypeError(f'can not import {shortrepr(value)} to scaled') from None
def from_string(self, text): def from_string(self, text):
value = float(text) value = float(text)
@@ -513,10 +508,6 @@ class EnumType(DataType):
"""returns a python object fit for serialisation""" """returns a python object fit for serialisation"""
return int(self(value)) return int(self(value))
def import_value(self, value):
"""returns a python object from serialisation"""
return self(value)
def __call__(self, value): def __call__(self, value):
"""accepts integers and strings, converts to EnumMember (may be used like an int)""" """accepts integers and strings, converts to EnumMember (may be used like an int)"""
try: try:
@@ -588,7 +579,10 @@ class BLOBType(DataType):
def import_value(self, value): def import_value(self, value):
"""returns a python object from serialisation""" """returns a python object from serialisation"""
return b64decode(value) try:
return b64decode(value)
except Exception:
raise WrongTypeError(f'can not b64decode {shortrepr(value)}') from None
def from_string(self, text): def from_string(self, text):
value = text value = text
@@ -659,10 +653,6 @@ class StringType(DataType):
"""returns a python object fit for serialisation""" """returns a python object fit for serialisation"""
return f'{value}' return f'{value}'
def import_value(self, value):
"""returns a python object from serialisation"""
return str(value)
def from_string(self, text): def from_string(self, text):
value = str(text) value = str(text)
return self(value) return self(value)
@@ -723,10 +713,6 @@ class BoolType(DataType):
"""returns a python object fit for serialisation""" """returns a python object fit for serialisation"""
return self(value) return self(value)
def import_value(self, value):
"""returns a python object from serialisation"""
return self(value)
def from_string(self, text): def from_string(self, text):
value = text value = text
return self(value) return self(value)

View File

@@ -499,7 +499,7 @@ class Command(Accessible):
"""perform function call """perform function call
:param module_obj: the module on which the command is to be executed :param module_obj: the module on which the command is to be executed
:param argument: the argument from the do command :param argument: the argument from the do command (transported value!)
:returns: the return value converted to the result type :returns: the return value converted to the result type
- when the argument type is TupleOf, the function is called with multiple arguments - when the argument type is TupleOf, the function is called with multiple arguments
@@ -514,13 +514,9 @@ class Command(Accessible):
f'{module_obj.__class__.__name__}.{self.name} needs an' f'{module_obj.__class__.__name__}.{self.name} needs an'
f' argument of type {self.argument}!' f' argument of type {self.argument}!'
) )
try: # convert transported value to internal value
argument = self.argument.import_value(argument) argument = self.argument.import_value(argument)
except TypeError: # verify range
pass # validate will raise appropriate message
except ValueError:
pass # validate will raise appropriate message
self.argument.validate(argument) self.argument.validate(argument)
if isinstance(self.argument, TupleOf): if isinstance(self.argument, TupleOf):
res = func(*argument) res = func(*argument)

View File

@@ -162,7 +162,9 @@ class Dispatcher:
if pobj.readonly: if pobj.readonly:
raise ReadOnlyError(f"Parameter {modulename}:{pname} can not be changed remotely") raise ReadOnlyError(f"Parameter {modulename}:{pname} can not be changed remotely")
# validate! # convert transported value to internal value
value = pobj.datatype.import_value(value)
# verify range
value = pobj.datatype.validate(value, previous=pobj.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)

View File

@@ -45,7 +45,6 @@ def test_DataType():
with pytest.raises(NotImplementedError): with pytest.raises(NotImplementedError):
dt('') dt('')
dt.export_value('') dt.export_value('')
dt.import_value('')
def test_FloatRange(): def test_FloatRange():
@@ -61,8 +60,12 @@ def test_FloatRange():
dt(-9) # convert, but do not check limits dt(-9) # convert, but do not check limits
with pytest.raises(WrongTypeError): with pytest.raises(WrongTypeError):
dt('XX') dt('XX')
with pytest.raises(WrongTypeError):
dt.import_value('XX')
with pytest.raises(WrongTypeError): with pytest.raises(WrongTypeError):
dt([19, 'X']) dt([19, 'X'])
with pytest.raises(WrongTypeError):
dt.import_value([19, 'X'])
dt(1) dt(1)
dt(0) dt(0)
dt(13.14 - 10) # raises an error, if resolution is not handled correctly dt(13.14 - 10) # raises an error, if resolution is not handled correctly
@@ -114,12 +117,20 @@ def test_IntRange():
dt(-9) # convert, but do not check limits dt(-9) # convert, but do not check limits
with pytest.raises(WrongTypeError): with pytest.raises(WrongTypeError):
dt('XX') dt('XX')
with pytest.raises(WrongTypeError):
dt.import_value('XX')
with pytest.raises(WrongTypeError): with pytest.raises(WrongTypeError):
dt([19, 'X']) dt([19, 'X'])
with pytest.raises(WrongTypeError):
dt.import_value([19, 'X'])
with pytest.raises(WrongTypeError): with pytest.raises(WrongTypeError):
dt(1.3) dt(1.3)
with pytest.raises(WrongTypeError):
dt.import_value(1.3)
with pytest.raises(WrongTypeError): with pytest.raises(WrongTypeError):
dt('1.3') dt('1.3')
with pytest.raises(WrongTypeError):
dt.import_value('1.3')
dt(1) dt(1)
dt(0) dt(0)
with pytest.raises(ProgrammingError): with pytest.raises(ProgrammingError):
@@ -152,8 +163,12 @@ def test_ScaledInteger():
dt(-9) # convert, but do not check limits dt(-9) # convert, but do not check limits
with pytest.raises(WrongTypeError): with pytest.raises(WrongTypeError):
dt('XX') dt('XX')
with pytest.raises(WrongTypeError):
dt.import_value('XX')
with pytest.raises(WrongTypeError): with pytest.raises(WrongTypeError):
dt([19, 'X']) dt([19, 'X'])
with pytest.raises(WrongTypeError):
dt.import_value([19, 'X'])
dt(1) dt(1)
dt(0) dt(0)
with pytest.raises(ProgrammingError): with pytest.raises(ProgrammingError):
@@ -217,12 +232,20 @@ def test_EnumType():
with pytest.raises(RangeError): with pytest.raises(RangeError):
dt(9) dt(9)
with pytest.raises(RangeError):
dt.import_value(9)
with pytest.raises(RangeError): with pytest.raises(RangeError):
dt(-9) dt(-9)
with pytest.raises(RangeError):
dt.import_value(-9)
with pytest.raises(RangeError): with pytest.raises(RangeError):
dt('XX') dt('XX')
with pytest.raises(RangeError):
dt.import_value('XX')
with pytest.raises(WrongTypeError): with pytest.raises(WrongTypeError):
dt([19, 'X']) dt([19, 'X'])
with pytest.raises(WrongTypeError):
dt.import_value([19, 'X'])
assert dt('a') == 3 assert dt('a') == 3
assert dt('stuff') == 1 assert dt('stuff') == 1
@@ -265,6 +288,10 @@ def test_BLOBType():
dt(b'abcdefghijklmno') dt(b'abcdefghijklmno')
with pytest.raises(WrongTypeError): with pytest.raises(WrongTypeError):
dt('abcd') dt('abcd')
with pytest.raises(WrongTypeError):
dt.import_value('ab')
with pytest.raises(WrongTypeError):
dt.import_value(b'xxx')
assert dt(b'abcd') == b'abcd' assert dt(b'abcd') == b'abcd'
dt.setProperty('minbytes', 1) dt.setProperty('minbytes', 1)

View File

@@ -76,6 +76,7 @@ class DummyMultiEvent(threading.Event):
return trigger return trigger
@pytest.mark.filterwarnings('ignore') # ignore PytestUnhandledThreadExceptionWarning
def test_Communicator(): def test_Communicator():
o = Communicator('communicator', LoggerStub(), {'description': ''}, ServerStub({})) o = Communicator('communicator', LoggerStub(), {'description': ''}, ServerStub({}))
o.earlyInit() o.earlyInit()
@@ -86,6 +87,7 @@ def test_Communicator():
assert event.wait(timeout=0.1) assert event.wait(timeout=0.1)
@pytest.mark.filterwarnings('ignore') # ignore PytestUnhandledThreadExceptionWarning
def test_ModuleMagic(): def test_ModuleMagic():
class Newclass1(Drivable): class Newclass1(Drivable):
param1 = Parameter('param1', datatype=BoolType(), default=False) param1 = Parameter('param1', datatype=BoolType(), default=False)

View File

@@ -121,6 +121,7 @@ class Mod1(Base, Readable):
return 0 return 0
@pytest.mark.filterwarnings('ignore') # ignore PytestUnhandledThreadExceptionWarning
@pytest.mark.parametrize( @pytest.mark.parametrize(
'ncycles, pollinterval, slowinterval, mspan, pspan', 'ncycles, pollinterval, slowinterval, mspan, pspan',
[ # normal case: [ # normal case: