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:
Enrico Faulhaber 2017-12-05 16:47:22 +01:00
parent 3e9733d8f3
commit 8c26ecf5cf
4 changed files with 142 additions and 75 deletions

View File

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

View File

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

View File

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

View File

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