diff --git a/secop/client/baseclient.py b/secop/client/baseclient.py index 7e8ea74..98a9113 100644 --- a/secop/client/baseclient.py +++ b/secop/client/baseclient.py @@ -150,7 +150,6 @@ class Value(object): r = [] if self.t is not None: r.append("timestamp=%r" % format_time(self.t)) - print("Check 3") if self.u is not None: r.append('unit=%r' % self.u) if self.e is not None: @@ -523,7 +522,8 @@ class Client(object): datatype = self._getDescribingParameterData(module, parameter)['datatype'] - value = datatype.export_value(datatype.validate(value)) + value = datatype.from_string(value) + value = datatype.export_value(value) self.communicate('change', '%s:%s' % (module, parameter), value) @property diff --git a/secop/datatypes.py b/secop/datatypes.py index 8a36bbd..146004f 100644 --- a/secop/datatypes.py +++ b/secop/datatypes.py @@ -21,10 +21,12 @@ # ***************************************************************************** """Define validated data types.""" -from ast import literal_eval from base64 import b64encode, b64decode -from .errors import ProgrammingError +from .errors import ProgrammingError, ParsingError +from .parse import Parser + +Parser = Parser() # Only export these classes for 'from secop.datatypes import *' __all__ = [ @@ -426,8 +428,9 @@ class ArrayOf(DataType): return [self.subtype.import_value(elem) for elem in value] def from_string(self, text): - # XXX: parse differntly than using eval! - value = eval(text) # pylint: disable=W0123 + value, rem = Parser.parse(text) + if rem: + raise ParsingError('trailing garbage: %r' % rem) return self.validate(value) @@ -469,8 +472,10 @@ class TupleOf(DataType): return [sub.import_value(elem) for sub, elem in zip(self.subtypes, value)] def from_string(self, text): - value = literal_eval(text) - return self.validate(tuple(value)) + value, rem = Parser.parse(text) + if rem: + raise ParsingError('trailing garbage: %r' % rem) + return self.validate(value) class StructOf(DataType): @@ -525,7 +530,9 @@ class StructOf(DataType): for k, v in value.items()) def from_string(self, text): - value = literal_eval(text) + value, rem = Parser.parse(text) + if rem: + raise ParsingError('trailing garbage: %r' % rem) return self.validate(dict(value)) @@ -577,7 +584,9 @@ class Command(DataType): raise ProgrammingError('values of type command can not be transported!') def from_string(self, text): - value = literal_eval(text) + value, rem = Parser.parse(text) + if rem: + raise ParsingError('trailing garbage: %r' % rem) return self.validate(value) diff --git a/secop/errors.py b/secop/errors.py index 7eb2cb6..6ed42d0 100644 --- a/secop/errors.py +++ b/secop/errors.py @@ -37,6 +37,10 @@ class ProgrammingError(SECoPServerError): pass +class ParsingError(SECoPServerError): + pass + + # to be exported for remote operation class SECoPError(SECoPServerError): pass diff --git a/secop/gui/nodectrl.py b/secop/gui/nodectrl.py index 1beb771..ed9f07a 100644 --- a/secop/gui/nodectrl.py +++ b/secop/gui/nodectrl.py @@ -24,8 +24,8 @@ import pprint import json -from PyQt4.QtGui import QWidget, QTextCursor, QFont, QFontMetrics, QLabel, QPushButton, QLineEdit, QMessageBox, QCheckBox, QSizePolicy -from PyQt4.QtCore import pyqtSignature as qtsig, Qt, pyqtSignal +from PyQt4.QtGui import QWidget, QTextCursor, QFont, QFontMetrics, QLabel, QMessageBox +from PyQt4.QtCore import pyqtSignature as qtsig, Qt from secop.gui.util import loadUi from secop.protocol.errors import SECOPError @@ -221,10 +221,10 @@ class ReadableWidget(QWidget): raise def _init_status_widgets(self): - self.update_status(self._get('status', (999, ''))) + self.update_status(self._get('status', (999, '')), {}) # XXX: also connect update_status signal to LineEdit ?? - def update_status(self, status, qualifiers={}): + def update_status(self, status, qualifiers): display_string = self._status_type.subtypes[0].entries.get(status[0]) if status[1]: display_string += ':' + status[1] @@ -232,9 +232,9 @@ class ReadableWidget(QWidget): # may change meaning of cmdPushButton def _init_current_widgets(self): - self.update_current(self._get('value', '')) + self.update_current(self._get('value', ''), {}) - def update_current(self, value, qualifiers={}): + def update_current(self, value, qualifiers): self.currentLineEdit.setText(str(value)) def _init_target_widgets(self): @@ -243,13 +243,15 @@ class ReadableWidget(QWidget): self.targetComboBox.setHidden(True) self.cmdPushButton.setHidden(True) - def update_target(self, target, qualifiers={}): + def update_target(self, target, qualifiers): pass def target_go(self, target): + print self, target try: self._node.setParameter(self._module, 'target', target) except Exception as e: + self.log.exception(e) QMessageBox.warning(self.parent(), 'Operation failed', str(e)) def _updateValue(self, module, parameter, value): @@ -266,7 +268,6 @@ class ReadableWidget(QWidget): class DrivableWidget(ReadableWidget): def _init_target_widgets(self): - params = self._node.getProperties(self._module, 'target') if self._is_enum: # EnumType: disable Linedit self.targetLineEdit.setHidden(True) @@ -280,13 +281,13 @@ class DrivableWidget(ReadableWidget): else: self.update_target(target) - def update_current(self, value, qualifiers={}): + def update_current(self, value, qualifiers): if self._is_enum: self.currentLineEdit.setText(self._map[self._revmap[value]][0]) else: self.currentLineEdit.setText(str(value)) - def update_target(self, target, qualifiers={}): + def update_target(self, target, qualifiers): if self._is_enum: # update selected item if target in self._revmap: diff --git a/secop/parse.py b/secop/parse.py new file mode 100644 index 0000000..699018e --- /dev/null +++ b/secop/parse.py @@ -0,0 +1,167 @@ +# -*- coding: utf-8 -*- +# ***************************************************************************** +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., +# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Module authors: +# Enrico Faulhaber +# +# ***************************************************************************** +"""parser. used for config files and the gui + +can't use ast.literal_eval as it can't handle enums :( + +parsing rules: +(...) -> tuple +[...] -> tuple +{text:...} -> dict +{text=...} -> dict +..., ... -> tuple +digits -> float or int (try int, if it fails: take float) +text -> string +'text' -> string +"text" -> string + +further convertions are done by the validator of the datatype.... +""" + +from collections import OrderedDict + + +class Parser(object): + # all parsing methods return (parsed value, remaining string) + # or (None, remaining_text) if parsing error + + def parse_number(self, text): + text = text.strip() + l = 1 + number = None + while l <= len(text): + try: + number = float(text[:l]) + length = l + l += 1 + except ValueError: + if text[l - 1] in u'eE+-': + l += 1 + continue + if number is None: + return None, text + try: + return int(text[:length]), text[length:] + except ValueError: + return number, text[length:] + return number, '' + + def parse_string(self, orgtext): + # handle quoted and unquoted strings correctly + text = orgtext.strip() + if text[0] in (u'"', u"'"): + # quoted string + quote = text[0] + idx = 0 + + while True: + idx = text.find(quote, idx + 1) + if idx == -1: + return None, orgtext + # check escapes! + if text[idx - 1] == u'\\': + continue + return text[1:idx], text[idx + 1:] + + # unquoted strings are terminated by comma or whitespace + idx = 0 + while idx < len(text): + if text[idx] in u'\x09 ,.;:()[]{}<>-+*/\\!"§$%&=?#~+*\'´`^°|-': + break + idx += 1 + return text[:idx] or None, text[idx:] + + def parse_tuple(self, orgtext): + text = orgtext.strip() + bra = text[0] + if bra not in u'([<': + return None, orgtext + # convert to closing bracket + bra = u')]>'[u'([<'.index(bra)] + reslist = [] + # search for cosing bracket, collecting results + text = text[1:] + while text: + if bra not in text: + return None, text + res, rem = self.parse_sub(text) + if res is None: + return None, text + reslist.append(res) + if rem[0] == bra: + return tuple(reslist), rem[1:] + # eat separator + if rem[0] in u',;': + text = rem[1:] + else: + return None, rem + return None, orgtext + + def parse_dict(self, orgtext): + text = orgtext.strip() + if text[0] != u'{': + return None, orgtext + # keep ordering + result = OrderedDict() + # search for cosing bracket, collecting results + # watch for key=value or key:value pairs, separated by , + text = text[1:] + while u'}' in text: + # first part is always a string + key, rem = self.parse_string(text) + if not key: + return None, orgtext + if rem[0] not in u':=': + return None, rem + # eat separator + text = rem[1:] + value, rem = self.parse_sub(text) + if not value: + return None, orgtext + result[key] = value + if rem[0] == u'}': + return result, rem[1:] + + if rem[0] not in u',;': + return None, rem + # eat separator + text = rem[1:] + return None, text + + def parse_sub(self, orgtext): + text = orgtext.strip() + if not text: + return None, orgtext + if text[0] in u'+-.0123456789': + return self.parse_number(orgtext) + elif text[0] == u'{': + return self.parse_dict(orgtext) + elif text[0] in u'([<': + return self.parse_tuple(orgtext) + return self.parse_string(orgtext) + + def parse(self, orgtext): + print "parsing %r" % orgtext + res, rem = self.parse_sub(orgtext) + if rem and rem[0] in u',;': + return self.parse_sub(u'[%s]' % orgtext) + return res, rem diff --git a/test/test_parse.py b/test/test_parse.py new file mode 100644 index 0000000..702b835 --- /dev/null +++ b/test/test_parse.py @@ -0,0 +1,96 @@ +# -*- coding: utf-8 -*- +# ***************************************************************************** +# +# This program is free software; you can redistribute it and/or modify it under +# the terms of the GNU General Public License as published by the Free Software +# Foundation; either version 2 of the License, or (at your option) any later +# version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more +# details. +# +# You should have received a copy of the GNU General Public License along with +# this program; if not, write to the Free Software Foundation, Inc., +# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# Module authors: +# Enrico Faulhaber +# +# ***************************************************************************** +"""test data types.""" + +import sys +sys.path.insert(0, sys.path[0] + '/..') + +from collections import OrderedDict + +import pytest + +from secop.parse import Parser + +# pylint: disable=redefined-outer-name +@pytest.fixture(scope="module") +def parser(): + return Parser() + + +def test_Parser_parse_number(parser): + assert parser.parse_number('1') == (1, '') + assert parser.parse_number('') == (None, '') + assert parser.parse_number('a') == (None, 'a') + assert parser.parse_number('1,2,3') == (1, ',2,3') + assert parser.parse_number('1.23e-04:9') == (1.23e-4, ':9') + + +def test_Parser_parse_string(parser): + assert parser.parse_string('%') == (None, '%') + assert parser.parse_string('a') == ('a', '') + assert parser.parse_string('Hello World!') == ('Hello', ' World!') + assert parser.parse_string('Hello]') == (None, '>]') + assert parser.parse_tuple('x') == (None, 'x') + assert parser.parse_tuple('2') == (None, '2') + + +def test_Parser_parse_dict(parser): + assert (parser.parse_dict('{a:1,b=2,"X y":\'a:9=3\'}') == + (OrderedDict([('a', 1), ('b', 2), ('X y', 'a:9=3')]), '')) + + +def test_Parser_parse(parser): + assert parser.parse('1') == (1, '') + assert parser.parse('') == (None, '') + assert parser.parse('a') == ('a', '') + assert parser.parse('1,2,3') == ((1, 2, 3), '') + assert parser.parse('1.23e-04:9') == (1.23e-4, ':9') + + assert parser.parse('%') == (None, '%') + assert parser.parse('Hello World!') == ('Hello', ' World!') + assert parser.parse('Hello]') == (None, '>]') + assert parser.parse('x') == ('x', '') + assert parser.parse('2') == (2, '') + + assert (parser.parse('{a:1,b=2,"X y":\'a:9=3\'}') == + (OrderedDict([('a', 1), ('b', 2), ('X y', 'a:9=3')]), '')) + + assert parser.parse('1, 2,a,c') == ((1, 2, 'a', 'c'), '') + + assert parser.parse('"\x09 \r"') == ('\t \r', '')