Add value parser + use it for the gui

replaces eval which is used so far

Change-Id: Ie5ff8c82175786e233d52bc0faac4e72e3bc27e9
Reviewed-on: https://forge.frm2.tum.de/review/17271
Tested-by: JenkinsCodeReview <bjoern_pedersen@frm2.tum.de>
Reviewed-by: Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
This commit is contained in:
Enrico Faulhaber 2018-02-08 08:53:30 +01:00
parent c146f477aa
commit 66503e8975
6 changed files with 297 additions and 20 deletions

View File

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

View File

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

View File

@ -37,6 +37,10 @@ class ProgrammingError(SECoPServerError):
pass
class ParsingError(SECoPServerError):
pass
# to be exported for remote operation
class SECoPError(SECoPServerError):
pass

View File

@ -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, '<not supported>')))
self.update_status(self._get('status', (999, '<not supported>')), {})
# 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:

167
secop/parse.py Normal file
View File

@ -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 <enrico.faulhaber@frm2.tum.de>
#
# *****************************************************************************
"""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

96
test/test_parse.py Normal file
View File

@ -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 <enrico.faulhaber@frm2.tum.de>
#
# *****************************************************************************
"""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<World!') == ('Hello', '<World!')
assert parser.parse_string('"Hello World!\'') == (None, '"Hello World!\'')
assert parser.parse_string('"Hello World!\\"') == (
None, '"Hello World!\\"')
assert parser.parse_string('"Hello World!"X') == ("Hello World!", 'X')
def test_Parser_parse_tuple(parser):
assert parser.parse_tuple('(1,2.3)') == ((1, 2.3), '')
assert parser.parse_tuple('[1,a]') == ((1, 'a'), '')
assert parser.parse_tuple('[1,a>]') == (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<World!') == ('Hello', '<World!')
assert parser.parse('"Hello World!\'') == (None, '"Hello World!\'')
assert parser.parse('"Hello World!\\"') == (None, '"Hello World!\\"')
assert parser.parse('"Hello World!"X') == ("Hello World!", 'X')
assert parser.parse('(1,2.3)') == ((1, 2.3), '')
assert parser.parse('[1,a]') == ((1, 'a'), '')
assert parser.parse('[1,a>]') == (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', '')