extend datatypes
also make interface more explicit Change-Id: Ib104e2c050d3e98e9d434d502951e33619784e2e missing: test cases for *.from_string(input) methods Reviewed-on: https://forge.frm2.tum.de/review/16893 Tested-by: JenkinsCodeReview <bjoern_pedersen@frm2.tum.de> Reviewed-by: Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
This commit is contained in:
parent
3e9733d8f3
commit
8c26ecf5cf
@ -21,6 +21,8 @@
|
||||
# *****************************************************************************
|
||||
"""Define validated data types."""
|
||||
|
||||
from ast import literal_eval
|
||||
from base64 import b64encode, b64decode
|
||||
|
||||
from .errors import ProgrammingError
|
||||
|
||||
@ -42,21 +44,31 @@ class DataType(object):
|
||||
IS_COMMAND = False
|
||||
|
||||
def validate(self, value):
|
||||
"""validate a external representation and return an internal one"""
|
||||
raise NotImplementedError
|
||||
"""check if given value (a python obj) is valid for this datatype
|
||||
|
||||
def export(self, value):
|
||||
"""returns a python object fit for external serialisation or logging"""
|
||||
returns the value or raises an appropriate exception"""
|
||||
raise NotImplementedError
|
||||
|
||||
def from_string(self, text):
|
||||
"""interprets a given string and returns a validated (internal) value"""
|
||||
# to evaluate values from configfiles, etc...
|
||||
# to evaluate values from configfiles, ui, etc...
|
||||
raise NotImplementedError
|
||||
|
||||
# goodie: if called, validate
|
||||
def __call__(self, value):
|
||||
return self.validate(value)
|
||||
def export_datatype(self):
|
||||
"""return a python object which after jsonifying identifies this datatype"""
|
||||
return self.as_json
|
||||
|
||||
def export_value(self, value):
|
||||
"""if needed, reformat value for transport"""
|
||||
return value
|
||||
|
||||
def import_value(self, value):
|
||||
"""opposite of export_value, reformat from transport to internal repr
|
||||
|
||||
note: for importing from gui/configfile/commandline use :meth:`from_string`
|
||||
instead.
|
||||
"""
|
||||
return value
|
||||
|
||||
|
||||
class FloatRange(DataType):
|
||||
@ -100,10 +112,14 @@ class FloatRange(DataType):
|
||||
return "FloatRange(%r)" % self.min
|
||||
return "FloatRange()"
|
||||
|
||||
def export(self, value):
|
||||
def export_value(self, value):
|
||||
"""returns a python object fit for serialisation"""
|
||||
return float(value)
|
||||
|
||||
def import_value(self, value):
|
||||
"""returns a python object from serialisation"""
|
||||
return float(value)
|
||||
|
||||
def from_string(self, text):
|
||||
value = float(text)
|
||||
return self.validate(value)
|
||||
@ -142,10 +158,14 @@ class IntRange(DataType):
|
||||
return "IntRange(%d)" % self.min
|
||||
return "IntRange()"
|
||||
|
||||
def export(self, value):
|
||||
def export_value(self, value):
|
||||
"""returns a python object fit for serialisation"""
|
||||
return int(value)
|
||||
|
||||
def import_value(self, value):
|
||||
"""returns a python object from serialisation"""
|
||||
return int(value)
|
||||
|
||||
def from_string(self, text):
|
||||
value = int(text)
|
||||
return self.validate(value)
|
||||
@ -155,7 +175,7 @@ class EnumType(DataType):
|
||||
as_json = ['enum']
|
||||
|
||||
def __init__(self, *args, **kwds):
|
||||
# enum keys are ints! check
|
||||
# enum keys are ints! remember mapping from intvalue to 'name'
|
||||
self.entries = {}
|
||||
num = 0
|
||||
for arg in args:
|
||||
@ -174,6 +194,7 @@ class EnumType(DataType):
|
||||
self.entries[v] = k
|
||||
# if len(self.entries) == 0:
|
||||
# raise ValueError('Empty enums ae not allowed!')
|
||||
# also keep a mapping from name strings to numbers
|
||||
self.reversed = {}
|
||||
for k, v in self.entries.items():
|
||||
if v in self.reversed:
|
||||
@ -185,7 +206,7 @@ class EnumType(DataType):
|
||||
return "EnumType(%s)" % ', '.join(
|
||||
['%s=%d' % (v, k) for k, v in self.entries.items()])
|
||||
|
||||
def export(self, value):
|
||||
def export_value(self, value):
|
||||
"""returns a python object fit for serialisation"""
|
||||
if value in self.reversed:
|
||||
return self.reversed[value]
|
||||
@ -194,6 +215,11 @@ class EnumType(DataType):
|
||||
raise ValueError('%r is not one of %s', str(
|
||||
value), ', '.join(self.reversed.keys()))
|
||||
|
||||
def import_value(self, value):
|
||||
"""returns a python object from serialisation"""
|
||||
# internally we store the key (which is a string)
|
||||
return self.entries[int(value)]
|
||||
|
||||
def validate(self, value):
|
||||
"""return the validated (internal) value or raise"""
|
||||
if value in self.reversed:
|
||||
@ -246,12 +272,17 @@ class BLOBType(DataType):
|
||||
'%r must be at most %d bytes long!', value, self.maxsize)
|
||||
return value
|
||||
|
||||
def export(self, value):
|
||||
def export_value(self, value):
|
||||
"""returns a python object fit for serialisation"""
|
||||
return b'%s' % value
|
||||
return b64encode(value)
|
||||
|
||||
def import_value(self, value):
|
||||
"""returns a python object from serialisation"""
|
||||
return b64decode(value)
|
||||
|
||||
def from_string(self, text):
|
||||
value = text
|
||||
# XXX:
|
||||
return self.validate(value)
|
||||
|
||||
|
||||
@ -297,12 +328,17 @@ class StringType(DataType):
|
||||
'Strings are not allowed to embed a \\0! Use a Blob instead!')
|
||||
return value
|
||||
|
||||
def export(self, value):
|
||||
def export_value(self, value):
|
||||
"""returns a python object fit for serialisation"""
|
||||
return '%s' % value
|
||||
|
||||
def import_value(self, value):
|
||||
"""returns a python object from serialisation"""
|
||||
# XXX: do we keep it as unicode str, or convert it to something else? (UTF-8 maybe?)
|
||||
return str(value)
|
||||
|
||||
def from_string(self, text):
|
||||
value = text
|
||||
value = str(text)
|
||||
return self.validate(value)
|
||||
|
||||
# Bool is a special enum
|
||||
@ -322,10 +358,14 @@ class BoolType(DataType):
|
||||
return True
|
||||
raise ValueError('%r is not a boolean value!', value)
|
||||
|
||||
def export(self, value):
|
||||
def export_value(self, value):
|
||||
"""returns a python object fit for serialisation"""
|
||||
return True if self.validate(value) else False
|
||||
|
||||
def import_value(self, value):
|
||||
"""returns a python object from serialisation"""
|
||||
return self.validate(value)
|
||||
|
||||
def from_string(self, text):
|
||||
value = text
|
||||
return self.validate(value)
|
||||
@ -379,9 +419,13 @@ class ArrayOf(DataType):
|
||||
raise ValueError(
|
||||
'Can not convert %s to ArrayOf DataType!', repr(value))
|
||||
|
||||
def export(self, value):
|
||||
def export_value(self, value):
|
||||
"""returns a python object fit for serialisation"""
|
||||
return [self.subtype.export(elem) for elem in value]
|
||||
return [self.subtype.export_value(elem) for elem in value]
|
||||
|
||||
def import_value(self, value):
|
||||
"""returns a python object from serialisation"""
|
||||
return [self.subtype.import_value(elem) for elem in value]
|
||||
|
||||
def from_string(self, text):
|
||||
# XXX: parse differntly than using eval!
|
||||
@ -418,13 +462,16 @@ class TupleOf(DataType):
|
||||
except Exception as exc:
|
||||
raise ValueError('Can not validate:', str(exc))
|
||||
|
||||
def export(self, value):
|
||||
def export_value(self, value):
|
||||
"""returns a python object fit for serialisation"""
|
||||
return [sub.export(elem) for sub, elem in zip(self.subtypes, value)]
|
||||
return [sub.export_value(elem) for sub, elem in zip(self.subtypes, value)]
|
||||
|
||||
def import_value(self, value):
|
||||
"""returns a python object from serialisation"""
|
||||
return [sub.import_value(elem) for sub, elem in zip(self.subtypes, value)]
|
||||
|
||||
def from_string(self, text):
|
||||
# XXX: parse differntly than using eval!
|
||||
value = eval(text) # pylint: disable=W0123
|
||||
value = literal_eval(text)
|
||||
return self.validate(tuple(value))
|
||||
|
||||
|
||||
@ -461,18 +508,26 @@ class StructOf(DataType):
|
||||
except Exception as exc:
|
||||
raise ValueError('Can not validate %s: %s', repr(value), str(exc))
|
||||
|
||||
def export(self, value):
|
||||
def export_value(self, value):
|
||||
"""returns a python object fit for serialisation"""
|
||||
if len(value.keys()) != len(self.named_subtypes.keys()):
|
||||
raise ValueError(
|
||||
'Illegal number of Arguments! Need %d arguments.', len(
|
||||
self.namd_subtypes.keys()))
|
||||
return dict((str(k), self.named_subtypes[k].export(v))
|
||||
return dict((str(k), self.named_subtypes[k].export_value(v))
|
||||
for k, v in value.items())
|
||||
|
||||
def import_value(self, value):
|
||||
"""returns a python object from serialisation"""
|
||||
if len(value.keys()) != len(self.named_subtypes.keys()):
|
||||
raise ValueError(
|
||||
'Illegal number of Arguments! Need %d arguments.', len(
|
||||
self.namd_subtypes.keys()))
|
||||
return dict((str(k), self.named_subtypes[k].import_value(v))
|
||||
for k, v in value.items())
|
||||
|
||||
def from_string(self, text):
|
||||
# XXX: parse differntly than using eval!
|
||||
value = eval(text) # pylint: disable=W0123
|
||||
value = literal_eval(text)
|
||||
return self.validate(dict(value))
|
||||
|
||||
|
||||
@ -517,17 +572,14 @@ class Command(DataType):
|
||||
except Exception as exc:
|
||||
raise ValueError('Can not validate %s: %s', repr(value), str(exc))
|
||||
|
||||
def export(self, value):
|
||||
"""returns a python object fit for serialisation"""
|
||||
if len(value) != len(self.argtypes):
|
||||
raise ValueError(
|
||||
'Illegal number of Arguments! Need %d arguments.' % len(
|
||||
self.argtypes))
|
||||
# return [t.export(v) for t,v in zip(self.argtypes, value)]
|
||||
def export_value(self, value):
|
||||
raise ProgrammingError('values of type command can not be transported!')
|
||||
|
||||
def import_value(self, value):
|
||||
raise ProgrammingError('values of type command can not be transported!')
|
||||
|
||||
def from_string(self, text):
|
||||
import ast
|
||||
value = ast.literal_eval(text)
|
||||
value = literal_eval(text)
|
||||
return self.validate(value)
|
||||
|
||||
|
||||
@ -547,16 +599,12 @@ DATATYPES = dict(
|
||||
)
|
||||
|
||||
|
||||
# probably not needed...
|
||||
def export_datatype(datatype):
|
||||
if datatype is None:
|
||||
return datatype
|
||||
return datatype.as_json
|
||||
|
||||
# important for getting the right datatype from formerly jsonified descr.
|
||||
|
||||
|
||||
def get_datatype(json):
|
||||
"""returns a DataType object from description
|
||||
|
||||
inverse of <DataType>.export_datatype()
|
||||
"""
|
||||
if json is None:
|
||||
return json
|
||||
if not isinstance(json, list):
|
||||
|
@ -35,7 +35,7 @@ from secop.lib import formatExtendedStack, mkthread
|
||||
from secop.lib.parsing import format_time
|
||||
from secop.errors import ConfigError, ProgrammingError
|
||||
from secop.protocol import status
|
||||
from secop.datatypes import DataType, EnumType, TupleOf, StringType, FloatRange, export_datatype, get_datatype
|
||||
from secop.datatypes import DataType, EnumType, TupleOf, StringType, FloatRange, get_datatype
|
||||
|
||||
|
||||
EVENT_ONLY_ON_CHANGED_VALUES = False
|
||||
@ -101,7 +101,7 @@ class PARAM(object):
|
||||
res = dict(
|
||||
description=self.description,
|
||||
readonly=self.readonly,
|
||||
datatype=export_datatype(self.datatype),
|
||||
datatype=self.datatype.export_datatype(),
|
||||
)
|
||||
if self.unit:
|
||||
res['unit'] = self.unit
|
||||
@ -113,9 +113,8 @@ class PARAM(object):
|
||||
res['timestamp'] = format_time(self.timestamp)
|
||||
return res
|
||||
|
||||
@property
|
||||
def export_value(self):
|
||||
return self.datatype.export(self.value)
|
||||
return self.datatype.export_value(self.value)
|
||||
|
||||
|
||||
class OVERRIDE(object):
|
||||
@ -158,8 +157,9 @@ class CMD(object):
|
||||
# used for serialisation only
|
||||
return dict(
|
||||
description=self.description,
|
||||
arguments=map(export_datatype, self.arguments),
|
||||
resulttype=export_datatype(self.resulttype), )
|
||||
arguments=[arg.export_datatype() for arg in self.arguments],
|
||||
resulttype=self.resulttype.export_datatype() if self.resulttype else None,
|
||||
)
|
||||
|
||||
|
||||
# Meta class
|
||||
|
@ -40,8 +40,11 @@ Interface to the modules:
|
||||
import time
|
||||
import threading
|
||||
|
||||
from messages import *
|
||||
from errors import *
|
||||
from secop.protocol.messages import Value, CommandReply, HelpMessage, \
|
||||
DeactivateReply, IdentifyReply, DescribeReply, HeartbeatReply, \
|
||||
ActivateReply, WriteReply
|
||||
from secop.protocol.errors import SECOPError, NoSuchModuleError, \
|
||||
NoSuchCommandError, NoSuchParamError, BadValueError, ReadonlyError
|
||||
from secop.lib.parsing import format_time
|
||||
from secop.lib import formatExtendedStack, formatException
|
||||
|
||||
@ -130,7 +133,7 @@ class Dispatcher(object):
|
||||
msg = Value(
|
||||
moduleobj.name,
|
||||
parameter=pname,
|
||||
value=pobj.export_value,
|
||||
value=pobj.export_value(),
|
||||
t=pobj.timestamp)
|
||||
self.broadcast_event(msg)
|
||||
|
||||
@ -328,10 +331,10 @@ class Dispatcher(object):
|
||||
res = Value(
|
||||
modulename,
|
||||
parameter=pname,
|
||||
value=pobj.export_value,
|
||||
value=pobj.export_value(),
|
||||
t=pobj.timestamp)
|
||||
else:
|
||||
res = Value(modulename, parameter=pname, value=pobj.export_value)
|
||||
res = Value(modulename, parameter=pname, value=pobj.export_value())
|
||||
return res
|
||||
|
||||
# now the (defined) handlers for the different requests
|
||||
@ -409,7 +412,7 @@ class Dispatcher(object):
|
||||
res = Value(
|
||||
module=modulename,
|
||||
parameter=pname,
|
||||
value=pobj.export_value,
|
||||
value=pobj.export_value(),
|
||||
t=pobj.timestamp,
|
||||
unit=pobj.unit)
|
||||
if res.value != Ellipsis: # means we do not have a value at all so skip this
|
||||
|
@ -40,7 +40,8 @@ def test_DataType():
|
||||
with pytest.raises(NotImplementedError):
|
||||
dt = DataType()
|
||||
dt.validate('')
|
||||
dt.export('')
|
||||
dt.export_value('')
|
||||
dt.import_value('')
|
||||
|
||||
|
||||
def test_FloatRange():
|
||||
@ -57,7 +58,8 @@ def test_FloatRange():
|
||||
dt.validate([19, 'X'])
|
||||
dt.validate(1)
|
||||
dt.validate(0)
|
||||
assert dt.export(-2.718) == -2.718
|
||||
assert dt.export_value(-2.718) == -2.718
|
||||
assert dt.import_value(-2.718) == -2.718
|
||||
with pytest.raises(ValueError):
|
||||
FloatRange('x', 'Y')
|
||||
|
||||
@ -111,11 +113,16 @@ def test_EnumType():
|
||||
with pytest.raises(ValueError):
|
||||
dt.validate(2)
|
||||
|
||||
assert dt.export('c') == 7
|
||||
assert dt.export('stuff') == 1
|
||||
assert dt.export(1) == 1
|
||||
assert dt.export_value('c') == 7
|
||||
assert dt.export_value('stuff') == 1
|
||||
assert dt.export_value(1) == 1
|
||||
assert dt.import_value(7) == 'c'
|
||||
assert dt.import_value(3) == 'a'
|
||||
assert dt.import_value(1) == 'stuff'
|
||||
with pytest.raises(ValueError):
|
||||
dt.export(2)
|
||||
dt.export_value(2)
|
||||
with pytest.raises(ValueError):
|
||||
dt.import_value('A')
|
||||
|
||||
|
||||
def test_BLOBType():
|
||||
@ -138,9 +145,10 @@ def test_BLOBType():
|
||||
assert dt.validate(b'abcd') == b'abcd'
|
||||
assert dt.validate(u'abcd') == b'abcd'
|
||||
|
||||
assert dt.export('abcd') == b'abcd'
|
||||
assert dt.export(b'abcd') == b'abcd'
|
||||
assert dt.export(u'abcd') == b'abcd'
|
||||
assert dt.export_value('abcd') == 'YWJjZA=='
|
||||
assert dt.export_value(b'abcd') == 'YWJjZA=='
|
||||
assert dt.export_value(u'abcd') == 'YWJjZA=='
|
||||
assert dt.import_value('YWJjZA==') == 'abcd'
|
||||
|
||||
|
||||
def test_StringType():
|
||||
@ -164,9 +172,10 @@ def test_StringType():
|
||||
assert dt.validate(b'abcd') == b'abcd'
|
||||
assert dt.validate(u'abcd') == b'abcd'
|
||||
|
||||
assert dt.export('abcd') == b'abcd'
|
||||
assert dt.export(b'abcd') == b'abcd'
|
||||
assert dt.export(u'abcd') == b'abcd'
|
||||
assert dt.export_value('abcd') == b'abcd'
|
||||
assert dt.export_value(b'abcd') == b'abcd'
|
||||
assert dt.export_value(u'abcd') == b'abcd'
|
||||
assert dt.import_value(u'abcd') == 'abcd'
|
||||
|
||||
|
||||
def test_BoolType():
|
||||
@ -183,9 +192,14 @@ def test_BoolType():
|
||||
assert dt.validate('off') is False
|
||||
assert dt.validate(1) is True
|
||||
|
||||
assert dt.export('false') is False
|
||||
assert dt.export(0) is False
|
||||
assert dt.export('on') is True
|
||||
assert dt.export_value('false') is False
|
||||
assert dt.export_value(0) is False
|
||||
assert dt.export_value('on') is True
|
||||
|
||||
assert dt.import_value(False) is False
|
||||
assert dt.import_value(True) is True
|
||||
with pytest.raises(ValueError):
|
||||
dt.import_value('av')
|
||||
|
||||
|
||||
def test_ArrayOf():
|
||||
@ -206,7 +220,8 @@ def test_ArrayOf():
|
||||
|
||||
assert dt.validate([1, 2, 3]) == [1, 2, 3]
|
||||
|
||||
assert dt.export([1, 2, 3]) == [1, 2, 3]
|
||||
assert dt.export_value([1, 2, 3]) == [1, 2, 3]
|
||||
assert dt.import_value([1, 2, 3]) == [1, 2, 3]
|
||||
|
||||
|
||||
def test_TupleOf():
|
||||
@ -224,7 +239,8 @@ def test_TupleOf():
|
||||
|
||||
assert dt.validate([1, True]) == [1, True]
|
||||
|
||||
assert dt.export([1, True]) == [1, True]
|
||||
assert dt.export_value([1, True]) == [1, True]
|
||||
assert dt.import_value([1, True]) == [1, True]
|
||||
|
||||
|
||||
def test_StructOf():
|
||||
@ -248,8 +264,8 @@ def test_StructOf():
|
||||
|
||||
assert dt.validate(dict(a_string='XXX', an_int=8)) == {'a_string': 'XXX',
|
||||
'an_int': 8}
|
||||
assert dt.export({'an_int': 13, 'a_string': 'WFEC'}) == {'a_string': 'WFEC',
|
||||
'an_int': 13}
|
||||
assert dt.export_value({'an_int': 13, 'a_string': 'WFEC'}) == {'a_string': 'WFEC', 'an_int': 13}
|
||||
assert dt.import_value({'an_int': 13, 'a_string': 'WFEC'}) == {'a_string': 'WFEC', 'an_int': 13}
|
||||
|
||||
|
||||
def test_get_datatype():
|
||||
|
Loading…
x
Reference in New Issue
Block a user