diff --git a/secop/datatypes.py b/secop/datatypes.py index 634f4af..85ae644 100644 --- a/secop/datatypes.py +++ b/secop/datatypes.py @@ -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 .export_datatype() + """ if json is None: return json if not isinstance(json, list): diff --git a/secop/modules.py b/secop/modules.py index 87d0a82..447aa03 100644 --- a/secop/modules.py +++ b/secop/modules.py @@ -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 diff --git a/secop/protocol/dispatcher.py b/secop/protocol/dispatcher.py index 9c53656..0728a2a 100644 --- a/secop/protocol/dispatcher.py +++ b/secop/protocol/dispatcher.py @@ -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 diff --git a/test/test_datatypes.py b/test/test_datatypes.py index b8c9143..5bd677d 100644 --- a/test/test_datatypes.py +++ b/test/test_datatypes.py @@ -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():