big rework to comply to current spec

- adapt to release(v2018-11-07)
- remove duplicate errors.py
- adapt tests

Change-Id: I383bb571f9808c72b37c12fbe55042011c4c0084
Reviewed-on: https://forge.frm2.tum.de/review/19397
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-11-21 17:10:11 +01:00
parent 87261382cf
commit 29a5b5c49e
25 changed files with 770 additions and 424 deletions

View File

@ -40,9 +40,9 @@ timeout=900
pollinterval.export=False pollinterval.export=False
# some parameter grouping # some parameter grouping
p.group='pid' p.group=pid
i.group='pid' i.group=pid
d.group='pid' d.group=pid
value.unit='K' value.unit=K

View File

@ -47,7 +47,7 @@ from secop.lib.parsing import parse_time, format_time
#from secop.protocol.encoding import ENCODERS #from secop.protocol.encoding import ENCODERS
#from secop.protocol.framing import FRAMERS #from secop.protocol.framing import FRAMERS
#from secop.protocol.messages import * #from secop.protocol.messages import *
from secop.protocol.errors import EXCEPTIONS from secop.errors import EXCEPTIONS
class TCPConnection(object): class TCPConnection(object):
@ -344,25 +344,18 @@ class Client(object):
def _getDescribingParameterData(self, module, parameter): def _getDescribingParameterData(self, module, parameter):
return self._getDescribingModuleData(module)['accessibles'][parameter] return self._getDescribingModuleData(module)['accessibles'][parameter]
def _decode_list_to_ordereddict(self, data):
# takes a list of 2*N <key>, <value> entries and
# return an orderedDict from it
result = OrderedDict()
while len(data) > 1:
key = data.pop(0)
value = data.pop(0)
result[key] = value
return result
def _decode_substruct(self, specialkeys=[], data={}): # pylint: disable=W0102 def _decode_substruct(self, specialkeys=[], data={}): # pylint: disable=W0102
# take a dict and move all keys which are not in specialkeys # take a dict and move all keys which are not in specialkeys
# into a 'properties' subdict # into a 'properties' subdict
# specialkeys entries are converted from list to ordereddict # specialkeys entries are converted from list to ordereddict
result = {} try:
for k in specialkeys: result = {}
result[k] = self._decode_list_to_ordereddict(data.pop(k, [])) for k in specialkeys:
result['properties'] = data result[k] = OrderedDict(data.pop(k, []))
return result result['properties'] = data
return result
except Exception as err:
raise RuntimeError('Error decoding substruct of descriptive data: %r\n%r' % (err, data))
def _issueDescribe(self): def _issueDescribe(self):
_, _, describing_data = self._communicate('describe') _, _, describing_data = self._communicate('describe')

View File

@ -33,7 +33,7 @@ except NameError:
from base64 import b64encode, b64decode from base64 import b64encode, b64decode
from secop.lib.enum import Enum from secop.lib.enum import Enum
from secop.errors import ProgrammingError, ParsingError from secop.errors import ProgrammingError, ProtocolError
from secop.parse import Parser from secop.parse import Parser
@ -92,10 +92,7 @@ class FloatRange(DataType):
self.max = None if maxval is None else float(maxval) self.max = None if maxval is None else float(maxval)
# note: as we may compare to Inf all comparisons would be false # note: as we may compare to Inf all comparisons would be false
if (self.min or float(u'-inf')) <= (self.max or float(u'+inf')): if (self.min or float(u'-inf')) <= (self.max or float(u'+inf')):
if minval is None and maxval is None: self.as_json = [u'double', minval, maxval]
self.as_json = [u'double']
else:
self.as_json = [u'double', minval, maxval]
else: else:
raise ValueError(u'Max must be larger then min!') raise ValueError(u'Max must be larger then min!')
@ -141,15 +138,14 @@ class FloatRange(DataType):
class IntRange(DataType): class IntRange(DataType):
"""Restricted int type""" """Restricted int type"""
def __init__(self, minval=None, maxval=None): def __init__(self, minval=-16777216, maxval=16777216):
self.min = int(minval) if minval is not None else minval self.min = int(minval)
self.max = int(maxval) if maxval is not None else maxval self.max = int(maxval)
if self.min is not None and self.max is not None and self.min > self.max: if self.min > self.max:
raise ValueError(u'Max must be larger then min!') raise ValueError(u'Max must be larger then min!')
if self.min is None and self.max is None: if None in (self.min, self.max):
self.as_json = [u'int'] raise ValueError(u'Limits can not be None!')
else: self.as_json = [u'int', self.min, self.max]
self.as_json = [u'int', self.min, self.max]
def validate(self, value): def validate(self, value):
try: try:
@ -193,7 +189,7 @@ class EnumType(DataType):
return [u'enum'] + [dict((m.name, m.value) for m in self._enum.members)] return [u'enum'] + [dict((m.name, m.value) for m in self._enum.members)]
def __repr__(self): def __repr__(self):
return "EnumType(%r, %s" % (self._enum.name, ', '.join('%s=%d' %(m.name, m.value) for m in self._enum.members)) return u"EnumType(%r, %s)" % (self._enum.name, ', '.join(u'%s=%d' %(m.name, m.value) for m in self._enum.members))
def export_value(self, value): def export_value(self, value):
"""returns a python object fit for serialisation""" """returns a python object fit for serialisation"""
@ -208,7 +204,7 @@ class EnumType(DataType):
try: try:
return self._enum[value] return self._enum[value]
except KeyError: except KeyError:
raise ValueError('%r is not a member of enum %r' % (value, self._enum)) raise ValueError(u'%r is not a member of enum %r' % (value, self._enum))
def from_string(self, text): def from_string(self, text):
return self.validate(text) return self.validate(text)
@ -217,26 +213,21 @@ class EnumType(DataType):
class BLOBType(DataType): class BLOBType(DataType):
minsize = None minsize = None
maxsize = None maxsize = None
def __init__(self, maxsize=None, minsize=0): def __init__(self, minsize=0, maxsize=None):
# if only one argument is given, use exactly that many bytes
# if nothing is given, default to 255
if maxsize is None: if maxsize is None:
raise ValueError(u'BLOBType needs a maximum number of Bytes count!') maxsize = minsize or 255
minsize, maxsize = min(minsize, maxsize), max(minsize, maxsize) minsize = maxsize
self.minsize = minsize minsize = int(minsize)
self.maxsize = maxsize maxsize = int(maxsize)
if minsize < 0: self.minsize, self.maxsize = min(minsize, maxsize), max(minsize, maxsize)
if self.minsize < 0:
raise ValueError(u'sizes must be bigger than or equal to 0!') raise ValueError(u'sizes must be bigger than or equal to 0!')
if minsize: self.as_json = [u'blob', minsize, maxsize]
self.as_json = [u'blob', maxsize, minsize]
else:
self.as_json = [u'blob', maxsize]
def __repr__(self): def __repr__(self):
if self.minsize: return u'BLOB(%s, %s)' % (unicode(self.minsize), unicode(self.maxsize))
return u'BLOB(%s, %s)' % (
unicode(self.minsize) if self.minsize else u'unspecified',
unicode(self.maxsize) if self.maxsize else u'unspecified')
return u'BLOB(%s)' % (unicode(self.minsize) if self.minsize else u'unspecified')
def validate(self, value): def validate(self, value):
"""return the validated (internal) value or raise""" """return the validated (internal) value or raise"""
@ -271,25 +262,19 @@ class StringType(DataType):
minsize = None minsize = None
maxsize = None maxsize = None
def __init__(self, maxsize=255, minsize=0): def __init__(self, minsize=0, maxsize=None):
if maxsize is None: if maxsize is None:
raise ValueError(u'StringType needs a maximum bytes count!') maxsize = minsize or 255
minsize, maxsize = min(minsize, maxsize), max(minsize, maxsize) minsize = 0
minsize = int(minsize)
if minsize < 0: maxsize = int(maxsize)
raise ValueError(u'sizes must be >= 0') self.minsize, self.maxsize = min(minsize, maxsize), max(minsize, maxsize)
if minsize: if self.minsize < 0:
self.as_json = [u'string', maxsize, minsize] raise ValueError(u'sizes must be bigger than or equal to 0!')
else: self.as_json = [u'string', minsize, maxsize]
self.as_json = [u'string', maxsize]
self.minsize = minsize
self.maxsize = maxsize
def __repr__(self): def __repr__(self):
if self.minsize: return u'StringType(%s, %s)' % (unicode(self.minsize), unicode(self.maxsize))
return u'StringType(%s, %s)' % (
unicode(self.minsize) or u'unspecified', unicode(self.maxsize) or u'unspecified')
return u'StringType(%s)' % unicode(self.maxsize)
def validate(self, value): def validate(self, value):
"""return the validated (internal) value or raise""" """return the validated (internal) value or raise"""
@ -358,24 +343,27 @@ class BoolType(DataType):
class ArrayOf(DataType): class ArrayOf(DataType):
minsize = None minsize = None
maxsize = None maxsize = None
def __init__(self, subtype, maxsize=None, minsize=0): subtype = None
self.subtype = subtype def __init__(self, subtype, minsize=0, maxsize=None):
# one argument -> exactly that size
# argument default to 10
if maxsize is None:
maxsize = minsize or 10
minsize = maxsize
if not isinstance(subtype, DataType): if not isinstance(subtype, DataType):
raise ValueError( raise ValueError(
u'ArrayOf only works with DataType objs as first argument!') u'ArrayOf only works with a DataType as first argument!')
self.subtype = subtype
if maxsize is None: minsize = int(minsize)
raise ValueError(u'ArrayOf needs a maximum size') maxsize = int(maxsize)
minsize, maxsize = min(minsize, maxsize), max(minsize, maxsize) minsize, maxsize = min(minsize, maxsize), max(minsize, maxsize)
if minsize < 0: if minsize < 0:
raise ValueError(u'sizes must be > 0') raise ValueError(u'sizes must be > 0')
if maxsize < 1: if maxsize < 1:
raise ValueError(u'Maximum size must be >= 1!') raise ValueError(u'Maximum size must be >= 1!')
# if only one arg is given, it is maxsize! # if only one arg is given, it is maxsize!
if minsize: self.as_json = [u'array', minsize, maxsize, subtype.as_json]
self.as_json = [u'array', subtype.as_json, maxsize, minsize]
else:
self.as_json = [u'array', subtype.as_json, maxsize]
self.minsize = minsize self.minsize = minsize
self.maxsize = maxsize self.maxsize = maxsize
@ -410,7 +398,7 @@ class ArrayOf(DataType):
def from_string(self, text): def from_string(self, text):
value, rem = Parser.parse(text) value, rem = Parser.parse(text)
if rem: if rem:
raise ParsingError(u'trailing garbage: %r' % rem) raise ProtocolError(u'trailing garbage: %r' % rem)
return self.validate(value) return self.validate(value)
@ -424,7 +412,7 @@ class TupleOf(DataType):
raise ValueError( raise ValueError(
u'TupleOf only works with DataType objs as arguments!') u'TupleOf only works with DataType objs as arguments!')
self.subtypes = subtypes self.subtypes = subtypes
self.as_json = [u'tuple', [subtype.as_json for subtype in subtypes]] self.as_json = [u'tuple'] + [subtype.as_json for subtype in subtypes]
def __repr__(self): def __repr__(self):
return u'TupleOf(%s)' % u', '.join([repr(st) for st in self.subtypes]) return u'TupleOf(%s)' % u', '.join([repr(st) for st in self.subtypes])
@ -454,7 +442,7 @@ class TupleOf(DataType):
def from_string(self, text): def from_string(self, text):
value, rem = Parser.parse(text) value, rem = Parser.parse(text)
if rem: if rem:
raise ParsingError(u'trailing garbage: %r' % rem) raise ProtocolError(u'trailing garbage: %r' % rem)
return self.validate(value) return self.validate(value)
@ -512,49 +500,40 @@ class StructOf(DataType):
def from_string(self, text): def from_string(self, text):
value, rem = Parser.parse(text) value, rem = Parser.parse(text)
if rem: if rem:
raise ParsingError(u'trailing garbage: %r' % rem) raise ProtocolError(u'trailing garbage: %r' % rem)
return self.validate(dict(value)) return self.validate(dict(value))
class CommandType(DataType): class CommandType(DataType):
IS_COMMAND = True IS_COMMAND = True
argtype = None
resulttype = None
def __init__(self, argtypes=tuple(), resulttype=None): def __init__(self, argtype=None, resulttype=None):
for arg in argtypes: if argtype is not None:
if not isinstance(arg, DataType): if not isinstance(argtype, DataType):
raise ValueError(u'CommandType: Argument types must be DataTypes!') raise ValueError(u'CommandType: Argument type must be a DataType!')
if resulttype is not None: if resulttype is not None:
if not isinstance(resulttype, DataType): if not isinstance(resulttype, DataType):
raise ValueError(u'CommandType: result type must be DataTypes!') raise ValueError(u'CommandType: Result type must be a DataType!')
self.argtypes = argtypes self.argtype = argtype
self.resulttype = resulttype self.resulttype = resulttype
if resulttype is not None: if argtype:
self.as_json = [u'command', argtype = argtype.as_json
[t.as_json for t in argtypes], if resulttype:
resulttype.as_json] resulttype = resulttype.as_json
else: self.as_json = [u'command', argtype, resulttype]
self.as_json = [u'command',
[t.as_json for t in argtypes],
None] # XXX: or NoneType ???
def __repr__(self): def __repr__(self):
argstr = u', '.join(repr(arg) for arg in self.argtypes) argstr = repr(self.argtype) if self.argtype else ''
if self.resulttype is None: if self.resulttype is None:
return u'CommandType(%s)' % argstr return u'CommandType(%s)' % argstr
return u'CommandType(%s)->%s' % (argstr, repr(self.resulttype)) return u'CommandType(%s)->%s' % (argstr, repr(self.resulttype))
def validate(self, value): def validate(self, value):
"""return the validated arguments value or raise""" """return the validated argument value or raise"""
try: return self.argtype.validate(value)
if len(value) != len(self.argtypes):
raise ValueError(
u'Illegal number of Arguments! Need %d arguments.' %
len(self.argtypes))
# validate elements and return
return [t.validate(v) for t, v in zip(self.argtypes, value)]
except Exception as exc:
raise ValueError(u'Can not validate %s: %s' % (repr(value), unicode(exc)))
def export_value(self, value): def export_value(self, value):
raise ProgrammingError(u'values of type command can not be transported!') raise ProgrammingError(u'values of type command can not be transported!')
@ -565,7 +544,7 @@ class CommandType(DataType):
def from_string(self, text): def from_string(self, text):
value, rem = Parser.parse(text) value, rem = Parser.parse(text)
if rem: if rem:
raise ParsingError(u'trailing garbage: %r' % rem) raise ProtocolError(u'trailing garbage: %r' % rem)
return self.validate(value) return self.validate(value)
@ -584,7 +563,7 @@ class LimitsType(StructOf):
class Status(TupleOf): class Status(TupleOf):
# shorten initialisation and allow acces to status enumMembers from status values # shorten initialisation and allow acces to status enumMembers from status values
def __init__(self, enum): def __init__(self, enum):
TupleOf.__init__(self, EnumType(enum), StringType(255)) TupleOf.__init__(self, EnumType(enum), StringType())
self.enum = enum self.enum = enum
def __getattr__(self, key): def __getattr__(self, key):
enum = TupleOf.__getattr__(self, 'enum') enum = TupleOf.__getattr__(self, 'enum')
@ -595,17 +574,17 @@ class Status(TupleOf):
# XXX: derive from above classes automagically! # XXX: derive from above classes automagically!
DATATYPES = dict( DATATYPES = dict(
bool=BoolType, bool =BoolType,
int=lambda _min=None, _max=None: IntRange(_min, _max), int =IntRange,
double=lambda _min=None, _max=None: FloatRange(_min, _max), double =FloatRange,
blob=lambda _max=None, _min=0: BLOBType(_max, _min), blob =BLOBType,
string=lambda _max=None, _min=0: StringType(_max, _min), string =StringType,
array=lambda subtype, _max=None, _min=0: ArrayOf(get_datatype(subtype), _max, _min), array =lambda _min, _max, subtype: ArrayOf(get_datatype(subtype), _min, _max),
tuple=lambda subtypes: TupleOf(*map(get_datatype, subtypes)), tuple =lambda *subtypes: TupleOf(*map(get_datatype, subtypes)),
enum=lambda kwds: EnumType('', **kwds), enum =lambda kwds: EnumType('', **kwds),
struct=lambda named_subtypes: StructOf( struct =lambda named_subtypes: StructOf(
**dict((n, get_datatype(t)) for n, t in list(named_subtypes.items()))), **dict((n, get_datatype(t)) for n, t in list(named_subtypes.items()))),
command=lambda args, res: CommandType(map(get_datatype, args), get_datatype(res)), command = lambda arg, res: CommandType(get_datatype(arg), get_datatype(res)),
) )
@ -623,14 +602,10 @@ def get_datatype(json):
if len(json) < 1: if len(json) < 1:
raise ValueError(u'can not validate %r' % json) raise ValueError(u'can not validate %r' % json)
base = json[0] base = json[0]
args = []
if len(json) > 1:
args = json[1:]
if base in DATATYPES: if base in DATATYPES:
if base in (u'enum', u'struct'):
if len(json) > 1:
args = json[1:]
else:
args = []
else:
args = json[1:]
try: try:
return DATATYPES[base](*args) return DATATYPES[base](*args)
except (TypeError, AttributeError): except (TypeError, AttributeError):

View File

@ -1,5 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
# ***************************************************************************** # *****************************************************************************
#
# This program is free software; you can redistribute it and/or modify it under # 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 # 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 # Foundation; either version 2 of the License, or (at your option) any later
@ -18,79 +19,117 @@
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de> # Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
# #
# ***************************************************************************** # *****************************************************************************
"""error class for our little framework""" """Define (internal) SECoP Errors"""
# base class
class SECoPServerError(Exception):
errorclass = 'InternalError'
# those errors should never be seen remotely! class SECoPError(RuntimeError):
# just in case they are, these are flagged as InternalError
class ConfigError(SECoPServerError): def __init__(self, *args, **kwds):
pass RuntimeError.__init__(self)
self.args = args
for k, v in list(kwds.items()):
setattr(self, k, v)
def __repr__(self):
args = ', '.join(map(repr, self.args))
kwds = ', '.join(['%s=%r' % i for i in list(self.__dict__.items())])
res = []
if args:
res.append(args)
if kwds:
res.append(kwds)
return '%s(%s)' % (self.name, ', '.join(res))
@property
def name(self):
return self.__class__.__name__[:-len('Error')]
class ProgrammingError(SECoPServerError): class SECoPServerError(SECoPError):
pass name = 'InternalError'
class ParsingError(SECoPServerError): class InternalError(SECoPError):
pass name = 'InternalError'
# to be exported for remote operation class ProgrammingError(SECoPError):
class SECoPError(SECoPServerError): name = 'InternalError'
pass
class ConfigError(SECoPError):
name = 'InternalError'
class ProtocolError(SECoPError):
name = 'ProtocolError'
class NoSuchModuleError(SECoPError): class NoSuchModuleError(SECoPError):
errorclass = 'NoSuchModule' name = 'NoSuchModule'
class NoSuchParameterError(SECoPError): class NoSuchParameterError(SECoPError):
errorclass = 'NoSuchParameter' pass
class NoSuchCommandError(SECoPError): class NoSuchCommandError(SECoPError):
errorclass = 'NoSuchCommand' pass
class CommandFailedError(SECoPError):
errorclass = 'CommandFailed'
class CommandRunningError(SECoPError):
errorclass = 'CommandRunning'
class ReadOnlyError(SECoPError): class ReadOnlyError(SECoPError):
errorclass = 'ReadOnly' pass
class BadValueError(SECoPError): class BadValueError(SECoPError):
errorclass = 'BadValue' pass
class CommunicationError(SECoPError): class CommandFailedError(SECoPError):
errorclass = 'CommunicationFailed' pass
class TimeoutError(SECoPError): class CommandRunningError(SECoPError):
errorclass = 'CommunicationFailed' # XXX: add to SECop messages pass
class HardwareError(SECoPError): class CommunicationFailedError(SECoPError):
errorclass = 'CommunicationFailed' # XXX: Add to SECoP messages pass
class IsBusyError(SECoPError): class IsBusyError(SECoPError):
errorclass = 'IsBusy' pass
class IsErrorError(SECoPError): class IsErrorError(SECoPError):
errorclass = 'IsError' pass
class DisabledError(SECoPError): class DisabledError(SECoPError):
errorclass = 'Disabled' pass
class HardwareError(SECoPError):
pass
EXCEPTIONS = dict(
NoSuchModule=NoSuchModuleError,
NoSuchParameter=NoSuchParameterError,
NoSuchCommand=NoSuchCommandError,
CommandFailed=CommandFailedError,
CommandRunning=CommandRunningError,
Readonly=ReadOnlyError,
BadValue=BadValueError,
CommunicationFailed=CommunicationFailedError,
HardwareError=HardwareError,
IsBusy=IsBusyError,
IsError=IsErrorError,
Disabled=DisabledError,
SyntaxError=ProtocolError,
InternalError=InternalError,
# internal short versions (candidates for spec)
Protocol=ProtocolError,
Internal=InternalError,
)

View File

@ -112,9 +112,9 @@ class HAS_Timeout(Feature):
class HAS_Pause(Feature): class HAS_Pause(Feature):
# just a proposal, can't agree on it.... # just a proposal, can't agree on it....
accessibles = { accessibles = {
'pause': Command('pauses movement', arguments=[], result=None), 'pause': Command('pauses movement', argument=None, result=None),
'go': Command('continues movement or start a new one if target was change since the last pause', 'go': Command('continues movement or start a new one if target was change since the last pause',
arguments=[], result=None), argument=None, result=None),
} }

430
secop/gui/miniplot.py Normal file
View File

@ -0,0 +1,430 @@
# -*- coding: utf-8 -*-
# *****************************************************************************
# Copyright (c) 2015-2016 by the authors, see LICENSE
#
# 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>
#
# *****************************************************************************
from os import path
from secop.gui.qt import Qt, QColor, QWidget, QSize, \
QBrush, QPainter, QPolygonF, QPointF, QPen, QRectF
_magenta = QBrush(QColor('#A12F86'))
_yellow = QBrush(QColor('yellow'))
_white = QBrush(QColor('white'))
_lightgrey = QBrush(QColor('lightgrey'))
_grey = QBrush(QColor('grey'))
_darkgrey = QBrush(QColor('#404040'))
_black = QBrush(QColor('black'))
_blue = QBrush(QColor('blue'))
_green = QBrush(QColor('green'))
_red = QBrush(QColor('red'))
_olive = QBrush(QColor('olive'))
_orange = QBrush(QColor('#ffa500'))
my_uipath = path.dirname(__file__)
class MiniPlotCurve(object):
# placeholder for data
linecolor = _black
linewidth = 0 # set to 0 to disable lines
symbolcolors = (_black, _white) # line, fill
symbolsize = 3 # both symbol linewidth and symbolsize, set to 0 to disable
errorbarcolor = _darkgrey
errorbarwidth = 3 # set to 0 to disable errorbar
def __init__(self):
self.data = [] # tripels of x, y, err (err may be None)
@property
def xvalues(self):
return [p[0] for p in self.data] if self.data else [0]
@property
def yvalues(self):
return [p[1] for p in self.data] if self.data else [0]
@property
def errvalues(self):
return [p[2] or 0.0 for p in self.data] if self.data else [0]
@property
def xmin(self):
return min(self.xvalues)
@property
def xmax(self):
return max(self.xvalues)
@property
def ymin(self):
return min(self.yvalues)
@property
def ymax(self):
return max(self.yvalues)
@property
def yemin(self):
return min(y-(e or 0) for _, y, e in self.data) if self.data else 0
@property
def yemax(self):
return max(y+(e or 0) for _, y, e in self.data) if self.data else 0
def paint(self, scale, painter):
# note: scale returns a screen-XY tuple for data XY
# draw errorbars, lines and symbols in that order
if self.errorbarwidth > 0:
pen = QPen()
pen.setBrush(self.errorbarcolor)
pen.setWidth(self.errorbarwidth)
painter.setPen(pen)
for _x,_y,_e in self.data:
if _e is None:
continue
x, y = scale(_x,_y)
e = scale(_x,_y + _e)[1] - y
painter.drawLine(x, y-e, x, y+e)
painter.fillRect(x - self.errorbarwidth / 2., y - e,
self.errorbarwidth, 2 * e, self.errorbarcolor)
points = [QPointF(*scale(p[0], p[1])) for p in self.data]
if self.linewidth > 0:
pen = QPen()
pen.setBrush(self.linecolor)
pen.setWidth(self.linewidth)
painter.setPen(pen)
painter.drawPolyline(QPolygonF(points))
if self.symbolsize > 0:
pen = QPen()
pen.setBrush(self.symbolcolors[0]) # linecolor
pen.setWidth(self.symbolsize) # linewidth
painter.setPen(pen)
painter.setBrush(self.symbolcolors[1]) # fill color
if self.symbolsize > 0:
for p in points:
painter.drawEllipse(p, 2*self.symbolsize, 2*self.symbolsize)
def preparepainting(self, scale, xmin, xmax):
pass # nothing to do
class MiniPlotFitCurve(MiniPlotCurve):
# do not influence scaling of plotting window
@property
def xmin(self):
return float('inf')
@property
def xmax(self):
return float('-inf')
@property
def ymin(self):
return float('inf')
@property
def ymax(self):
return float('-inf')
@property
def yemin(self):
return float('inf')
@property
def yemax(self):
return float('-inf')
def __init__(self, formula, params):
super(MiniPlotFitCurve, self).__init__()
self.formula = formula
self.params = params
linecolor = _blue
linewidth = 5 # set to 0 to disable lines
symbolsize = 0 # both symbol linewidth and symbolsize, set to 0 to disable
errorbarwidth = 0 # set to 0 to disable errorbar
def preparepainting(self, scale, xmin, xmax):
# recalculate data
points = int(scale(xmax) - scale(xmin))
self.data = []
for idx in range(points+1):
x = xmin + idx * (xmax-xmin) / points
y = self.formula(x, *self.params)
self.data.append((x,y,None))
class MiniPlot(QWidget):
ticklinecolors = (_grey, _lightgrey) # ticks, subticks
ticklinewidth = 1
bordercolor = _black
borderwidth = 1
labelcolor = _black
xlabel = 'x'
ylabel = 'y'
xfmt = '%.1f'
yfmt = '%g'
autotickx = True
autoticky = True
def __init__(self, parent=None):
QWidget.__init__(self, parent)
self.xmin = self.xmax = None
self.ymin = self.ymax = None
self.curves = []
self.plotx = 0 # left of this are labels
self.ploty = self.height() # below this are labels
def scaleX(self, x):
if not self.curves:
return x # XXX: !!!!
x = self.plotx + (self.width() - self.plotx) * (x - self.xmin) / (self.xmax - self.xmin)
# x = max(min(x, self.width()), self.plotx)
return x
def scaleY(self, y):
if not self.curves:
return y # XXX: !!!!
y = self.ploty * (self.ymax - y) / (self.ymax - self.ymin)
# y = max(min(y, self.ploty), 0)
return y
def scale(self, x, y):
# scales a plotting xx/y to a screen x/y to be used for painting...
return self.scaleX(x), self.scaleY(y)
def removeCurve(self, curve):
if curve in self.curves:
self.curves.remove(curve)
self.updatePlot()
def addCurve(self, curve):
if curve is not None and curve not in self.curves:
# new curve, recalculate all
self.curves.append(curve)
self.updatePlot()
def updatePlot(self):
xmin,xmax = -1,1
ymin,ymax = -1,1
# find limits of known curves
if self.curves:
xmin = min(c.xmin for c in self.curves)
xmax = max(c.xmax for c in self.curves)
ymin = min(c.yemin for c in self.curves)
ymax = max(c.yemax for c in self.curves)
# fallback values for no curve
while xmin >= xmax:
xmin, xmax = xmin - 1, xmax + 1
while ymin >= ymax:
ymin, ymax = ymin - 1, ymax + 1
# adjust limits a little
self.xmin = xmin - 0.05 * (xmax - xmin)
self.xmax = xmax + 0.05 * (xmax - xmin)
self.ymin = ymin - 0.05 * (ymax - ymin)
self.ymax = ymax + 0.05 * (ymax - ymin)
# (re-)generate x/yticks
if self.autotickx:
self.calc_xticks(xmin, xmax)
if self. autoticky:
self.calc_yticks(ymin, ymax)
# redraw
self.update()
def calc_xticks(self, xmin, xmax):
self.xticks = self.calc_ticks(xmin, xmax, self.xfmt)
def calc_yticks(self, ymin, ymax):
self.yticks = self.calc_ticks(ymin, ymax, self.yfmt)
def calc_ticks(self, _min, _max, fmt):
min_intervals = 2
diff = _max - _min
if diff <= 0:
return [0]
# find a 'good' step size
step = abs(diff / min_intervals)
# split into mantissa and exp.
expo = 0
while step >= 10:
step /= 10.
expo += 1
while step < 1:
step *= 10.
expo -= 1
# make step 'latch' into smalle bigger magic number
subs = 1
for n, subs in reversed([(1,5.), (1.5,3.), (2,4.), (3,3.), (5,5.), (10,2.)]):
if step >= n:
step = n
break
# convert back to normal number
while expo > 0:
step *= 10.
expo -= 1
while expo < 0:
step /= 10.
expo += 1
substep = step / subs
# round lower
rounded_min = step * int(_min / step)
# generate ticks list
ticks = []
x = rounded_min
while x + substep < _min:
x += substep
for _ in range(100):
if x < _max + substep:
break
# check if x is a tick or a subtick
x = substep * int(x / substep)
if abs(x - step * int(x / step)) <= substep / 2:
# tick
ticks.append((x, fmt % x))
else:
# subtick
ticks.append((x, ''))
x += substep
return ticks
def paintEvent(self, event):
painter = QPainter(self)
painter.setRenderHint(QPainter.Antialiasing)
# obtain a few properties we need for proper drawing
painter.setFont(self.font())
fm = painter.fontMetrics()
label_height = fm.height()
self.plotx = 3 + 2 * label_height
self.ploty = self.height() - 3 - 2 * label_height
# fill bg of plotting area
painter.fillRect(self.plotx ,0,self.width()-self.plotx, self.ploty,_white)
# paint ticklines
if self.curves and self.ticklinewidth > 0:
for e in self.xticks:
try:
_x = e[0] # pylint: disable=unsubscriptable-object
_l = e[1] # pylint: disable=unsubscriptable-object
except TypeError:
_x = e
_l = self.xfmt % _x
x = self.scaleX(_x)
pen = QPen()
pen.setBrush(self.ticklinecolors[0 if _l else 1])
pen.setWidth(self.ticklinewidth)
painter.setPen(pen)
painter.drawLine(x, 0, x, self.ploty)
for e in self.yticks:
try:
_y = e[0] # pylint: disable=unsubscriptable-object
_l = e[1] # pylint: disable=unsubscriptable-object
except TypeError:
_y = e
_l = self.xfmt % _x
y = self.scaleY(_y)
pen = QPen()
pen.setBrush(self.ticklinecolors[0 if _l else 1])
pen.setWidth(self.ticklinewidth)
painter.setPen(pen)
painter.drawLine(self.plotx, y, self.width(), y)
# paint curves
painter.setClipRect(QRectF(self.plotx, 0, self.width()-self.plotx, self.ploty))
for c in self.curves:
c.preparepainting(self.scaleX, self.xmin, self.xmax)
c.paint(self.scale, painter)
painter.setClipping(False)
# paint frame
pen = QPen()
pen.setBrush(self.bordercolor)
pen.setWidth(self.borderwidth)
painter.setPen(pen)
painter.drawPolyline(QPolygonF([
QPointF(self.plotx, 0),
QPointF(self.width()-1, 0),
QPointF(self.width()-1, self.ploty),
QPointF(self.plotx, self.ploty),
QPointF(self.plotx, 0),
]))
# draw labels
painter.setBrush(self.labelcolor)
h2 = (self.height()-self.ploty)/2.
# XXX: offset axis labels from axis a little
painter.drawText(self.plotx, self.ploty + h2,
self.width() - self.plotx, h2,
Qt.AlignCenter | Qt.AlignVCenter, self.xlabel)
# rotate ylabel?
painter.resetTransform()
painter.translate(0, self.ploty / 2.)
painter.rotate(-90)
w = fm.width(self.ylabel)
painter.drawText(-w, -fm.height() / 2., w * 2, self.plotx,
Qt.AlignCenter | Qt.AlignTop, self.ylabel)
painter.resetTransform()
if self.curves:
for e in self.xticks:
try:
_x = e[0] # pylint: disable=unsubscriptable-object
l = e[1] # pylint: disable=unsubscriptable-object
except TypeError:
_x = e
l = self.xfmt % _x
x = self.scaleX(_x)
w = fm.width(l)
painter.drawText(x - w, self.ploty + 2, 2 * w, h2,
Qt.AlignCenter | Qt.AlignVCenter, l)
for e in self.yticks:
try:
_y = e[0] # pylint: disable=unsubscriptable-object
l = e[1] # pylint: disable=unsubscriptable-object
except TypeError:
_y = e
l = self.yfmt % _y
y = self.scaleY(_y)
w = fm.width(l)
painter.resetTransform()
painter.translate(self.plotx - fm.height(), y + w)
painter.rotate(-90)
painter.drawText(0, -1,
2 * w, fm.height(),
Qt.AlignCenter | Qt.AlignBottom, l)
painter.resetTransform()
def sizeHint(self):
return QSize(320, 240)

View File

@ -43,30 +43,31 @@ from secop.gui.valuewidgets import get_widget
class CommandDialog(QDialog): class CommandDialog(QDialog):
def __init__(self, cmdname, arglist, parent=None): def __init__(self, cmdname, argument, parent=None):
super(CommandDialog, self).__init__(parent) super(CommandDialog, self).__init__(parent)
loadUi(self, 'cmddialog.ui') loadUi(self, 'cmddialog.ui')
self.setWindowTitle('Arguments for %s' % cmdname) self.setWindowTitle('Arguments for %s' % cmdname)
row = 0 #row = 0
self._labels = [] self._labels = []
self.widgets = [] self.widgets = []
for row, dtype in enumerate(arglist): # improve! recursive?
l = QLabel(repr(dtype)) dtype = argument
l.setWordWrap(True) l = QLabel(repr(dtype))
w = get_widget(dtype, readonly=False) l.setWordWrap(True)
self.gridLayout.addWidget(l, row, 0) w = get_widget(dtype, readonly=False)
self.gridLayout.addWidget(w, row, 1) self.gridLayout.addWidget(l, 0, 0)
self._labels.append(l) self.gridLayout.addWidget(w, 0, 1)
self.widgets.append(w) self._labels.append(l)
self.widgets.append(w)
self.gridLayout.setRowStretch(len(arglist), 1) self.gridLayout.setRowStretch(1, 1)
self.setModal(True) self.setModal(True)
self.resize(self.sizeHint()) self.resize(self.sizeHint())
def get_value(self): def get_value(self):
return [w.get_value() for w in self.widgets] return True, self.widgets[0].get_value()
def exec_(self): def exec_(self):
if super(CommandDialog, self).exec_(): if super(CommandDialog, self).exec_():
@ -127,7 +128,7 @@ class CommandButton(QPushButton):
super(CommandButton, self).__init__(parent) super(CommandButton, self).__init__(parent)
self._cmdname = cmdname self._cmdname = cmdname
self._argintypes = cmdinfo['datatype'].argtypes # list of datatypes self._argintype = cmdinfo['datatype'].argtype # single datatype
self.resulttype = cmdinfo['datatype'].resulttype self.resulttype = cmdinfo['datatype'].resulttype
self._cb = cb # callback function for exection self._cb = cb # callback function for exection
@ -138,11 +139,11 @@ class CommandButton(QPushButton):
def on_pushButton_pressed(self): def on_pushButton_pressed(self):
self.setEnabled(False) self.setEnabled(False)
if self._argintypes: if self._argintype:
dlg = CommandDialog(self._cmdname, self._argintypes) dlg = CommandDialog(self._cmdname, self._argintype)
args = dlg.exec_() args = dlg.exec_()
if args: # not 'Cancel' clicked if args: # not 'Cancel' clicked
self._cb(self._cmdname, args) self._cb(self._cmdname, args[1])
else: else:
# no need for arguments # no need for arguments
self._cb(self._cmdname, None) self._cb(self._cmdname, None)
@ -172,8 +173,13 @@ class ModuleCtrl(QWidget):
def _execCommand(self, command, args=None): def _execCommand(self, command, args=None):
if not args: if not args:
args = tuple() args = tuple()
result, qualifiers = self._node.execCommand( try:
self._module, command, args) result, qualifiers = self._node.execCommand(
self._module, command, args)
except TypeError:
result = None
qualifiers = {}
# XXX: flag missing data report as error
showCommandResultDialog(command, args, result, qualifiers) showCommandResultDialog(command, args, result, qualifiers)
def _initModuleWidgets(self): def _initModuleWidgets(self):
@ -182,7 +188,7 @@ class ModuleCtrl(QWidget):
# ignore groupings for commands (for now) # ignore groupings for commands (for now)
commands = self._node.getCommands(self._module) commands = self._node.getCommands(self._module)
# keep a reference or the widgets are detroyed to soon. # keep a reference or the widgets are destroyed to soon.
self.cmdWidgets = cmdWidgets = {} self.cmdWidgets = cmdWidgets = {}
# create and insert widgets into our QGridLayout # create and insert widgets into our QGridLayout
for command in sorted(commands): for command in sorted(commands):
@ -191,6 +197,7 @@ class ModuleCtrl(QWidget):
cmdWidgets[command] = w cmdWidgets[command] = w
self.commandGroupBox.layout().addWidget(w, 0, row) self.commandGroupBox.layout().addWidget(w, 0, row)
row += 1 row += 1
row = 0 row = 0
# collect grouping information # collect grouping information
paramsByGroup = {} # groupname -> [paramnames] paramsByGroup = {} # groupname -> [paramnames]
@ -198,8 +205,8 @@ class ModuleCtrl(QWidget):
params = self._node.getParameters(self._module) params = self._node.getParameters(self._module)
for param in params: for param in params:
props = self._node.getProperties(self._module, param) props = self._node.getProperties(self._module, param)
group = props.get('group', None) group = props.get('group', '')
if group is not None: if group:
allGroups.add(group) allGroups.add(group)
paramsByGroup.setdefault(group, []).append(param) paramsByGroup.setdefault(group, []).append(param)
# enforce reading initial value if not already in cache # enforce reading initial value if not already in cache
@ -210,6 +217,7 @@ class ModuleCtrl(QWidget):
self._groupWidgets = groupWidgets = {} self._groupWidgets = groupWidgets = {}
# create and insert widgets into our QGridLayout # create and insert widgets into our QGridLayout
# iterate over a union of all groups and all params
for param in sorted(allGroups.union(set(params))): for param in sorted(allGroups.union(set(params))):
labelstr = param + ':' labelstr = param + ':'
if param in paramsByGroup: if param in paramsByGroup:
@ -226,7 +234,7 @@ class ModuleCtrl(QWidget):
'datatype', None) 'datatype', None)
# yes: create a widget for this as well # yes: create a widget for this as well
labelstr, buttons = self._makeEntry( labelstr, buttons = self._makeEntry(
param, initValues[param].value, datatype=datatype, nolabel=True, checkbox=checkbox, invert=True) group, initValues[param].value, datatype=datatype, nolabel=True, checkbox=checkbox, invert=True)
checkbox.setText(labelstr) checkbox.setText(labelstr)
# add to Layout (yes: ignore the label!) # add to Layout (yes: ignore the label!)
@ -249,7 +257,7 @@ class ModuleCtrl(QWidget):
self._module, param_).get( self._module, param_).get(
'datatype', None) 'datatype', None)
label, buttons = self._makeEntry( label, buttons = self._makeEntry(
param_, initval, checkbox=checkbox, invert=False) param_, initval, datatype=datatype, checkbox=checkbox, invert=False)
# add to Layout # add to Layout
self.paramGroupBox.layout().addWidget(label, row, 0) self.paramGroupBox.layout().addWidget(label, row, 0)
@ -260,7 +268,7 @@ class ModuleCtrl(QWidget):
# param is a 'normal' param: create a widget if it has no group # param is a 'normal' param: create a widget if it has no group
# or is named after a group (otherwise its created above) # or is named after a group (otherwise its created above)
props = self._node.getProperties(self._module, param) props = self._node.getProperties(self._module, param)
if props.get('group', param) == param: if (props.get('group', '') or param) == param:
datatype = self._node.getProperties( datatype = self._node.getProperties(
self._module, param).get( self._module, param).get(
'datatype', None) 'datatype', None)

View File

@ -37,7 +37,7 @@ from secop.gui.qt import QWidget, QTextCursor, QFont, QFontMetrics, QLabel, \
QMessageBox, pyqtSlot, toHtmlEscaped QMessageBox, pyqtSlot, toHtmlEscaped
from secop.gui.util import loadUi from secop.gui.util import loadUi
from secop.protocol.errors import SECOPError from secop.errors import SECoPError
from secop.datatypes import StringType, EnumType from secop.datatypes import StringType, EnumType
@ -80,7 +80,7 @@ class NodeCtrl(QWidget):
self._addLogEntry(reply, newline=True, pretty=False) self._addLogEntry(reply, newline=True, pretty=False)
else: else:
self._addLogEntry(reply, newline=True, pretty=True) self._addLogEntry(reply, newline=True, pretty=True)
except SECOPError as e: except SECoPError as e:
self._addLogEntry( self._addLogEntry(
'error %s %s' % (e.name, json.dumps(e.args)), 'error %s %s' % (e.name, json.dumps(e.args)),
newline=True, newline=True,
@ -145,8 +145,8 @@ class NodeCtrl(QWidget):
if 'interface_class' in modprops: if 'interface_class' in modprops:
interfaces = modprops['interface_class'] interfaces = modprops['interface_class']
else: else:
interfaces = modprops['interfaces'] interfaces = modprops.get('interfaces', '')
description = modprops['description'] description = modprops.get('description', '!!! missing description !!!')
# fallback: allow (now) invalid 'Driveable' # fallback: allow (now) invalid 'Driveable'
unit = '' unit = ''

View File

@ -134,7 +134,7 @@ class GenericCmdWidget(ParameterWidget):
loadUi(self, 'cmdbuttons.ui') loadUi(self, 'cmdbuttons.ui')
self.cmdLineEdit.setText('') self.cmdLineEdit.setText('')
self.cmdLineEdit.setEnabled(self.datatype.argtypes is not None) self.cmdLineEdit.setEnabled(self.datatype.argtype is not None)
self.cmdLineEdit.returnPressed.connect( self.cmdLineEdit.returnPressed.connect(
self.on_cmdPushButton_clicked) self.on_cmdPushButton_clicked)
@ -164,7 +164,6 @@ def ParameterView(module,
parent=None): parent=None):
# depending on datatype returns an initialized widget fit for display and # depending on datatype returns an initialized widget fit for display and
# interaction # interaction
if datatype is not None: if datatype is not None:
if datatype.IS_COMMAND: if datatype.IS_COMMAND:
return GenericCmdWidget( return GenericCmdWidget(

View File

@ -24,25 +24,34 @@
# pylint: disable=unused-import # pylint: disable=unused-import
from __future__ import print_function from __future__ import print_function
import sys
try: try:
# Do not abort on exceptions in signal handlers.
# pylint: disable=unnecessary-lambda
sys.excepthook = lambda *args: sys.__excepthook__(*args)
from PyQt5 import uic from PyQt5 import uic
from PyQt5.QtCore import Qt, QObject, pyqtSignal, pyqtSlot from PyQt5.QtCore import Qt, QObject, pyqtSignal, pyqtSlot, QSize, QPointF, \
from PyQt5.QtGui import QFont, QTextCursor, QFontMetrics QRectF
from PyQt5.QtWidgets import QLabel, QWidget, QDialog, QLineEdit, QCheckBox, QPushButton, \ from PyQt5.QtGui import QFont, QTextCursor, QFontMetrics, QColor, QBrush, \
QSizePolicy, QMainWindow, QMessageBox, QInputDialog, QTreeWidgetItem, QApplication, \ QPainter, QPolygonF, QPen
QGroupBox, QSpinBox, QDoubleSpinBox, QComboBox, QRadioButton, QVBoxLayout, QHBoxLayout, \ from PyQt5.QtWidgets import QLabel, QWidget, QDialog, QLineEdit, QCheckBox, \
QGridLayout, QScrollArea, QFrame QPushButton, QSizePolicy, QMainWindow, QMessageBox, QInputDialog, \
QTreeWidgetItem, QApplication, QGroupBox, QSpinBox, QDoubleSpinBox, \
QComboBox, QRadioButton, QVBoxLayout, QHBoxLayout, QGridLayout, \
QScrollArea, QFrame
from xml.sax.saxutils import escape as toHtmlEscaped from xml.sax.saxutils import escape as toHtmlEscaped
except ImportError: except ImportError:
from PyQt4 import uic from PyQt4 import uic
from PyQt4.QtCore import Qt, QObject, pyqtSignal, pyqtSlot from PyQt4.QtCore import Qt, QObject, pyqtSignal, pyqtSlot, QSize, QPointF, QRectF
from PyQt4.QtGui import QFont, QTextCursor, QFontMetrics, \ from PyQt4.QtGui import QFont, QTextCursor, QFontMetrics, \
QLabel, QWidget, QDialog, QLineEdit, QCheckBox, QPushButton, \ QLabel, QWidget, QDialog, QLineEdit, QCheckBox, QPushButton, \
QSizePolicy, QMainWindow, QMessageBox, QInputDialog, QTreeWidgetItem, QApplication, \ QSizePolicy, QMainWindow, QMessageBox, QInputDialog, QTreeWidgetItem, QApplication, \
QGroupBox, QSpinBox, QDoubleSpinBox, QComboBox, QRadioButton, QVBoxLayout, QHBoxLayout, \ QGroupBox, QSpinBox, QDoubleSpinBox, QComboBox, QRadioButton, QVBoxLayout, QHBoxLayout, \
QGridLayout, QScrollArea, QFrame QGridLayout, QScrollArea, QFrame, QColor, QBrush, QPainter, QPolygonF, QPen
def toHtmlEscaped(s): def toHtmlEscaped(s):
return Qt.escape(s) return Qt.escape(s)

View File

@ -6,10 +6,16 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>730</width> <width>464</width>
<height>33</height> <height>33</height>
</rect> </rect>
</property> </property>
<property name="minimumSize">
<size>
<width>0</width>
<height>33</height>
</size>
</property>
<property name="windowTitle"> <property name="windowTitle">
<string>Form</string> <string>Form</string>
</property> </property>
@ -37,7 +43,7 @@
</property> </property>
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>256</width> <width>128</width>
<height>0</height> <height>0</height>
</size> </size>
</property> </property>
@ -57,7 +63,7 @@
<widget class="QLineEdit" name="setLineEdit"> <widget class="QLineEdit" name="setLineEdit">
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
<width>256</width> <width>128</width>
<height>0</height> <height>0</height>
</size> </size>
</property> </property>

View File

@ -6,8 +6,8 @@
<rect> <rect>
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>238</width> <width>100</width>
<height>121</height> <height>100</height>
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">

View File

@ -224,7 +224,8 @@ class Readable(Module):
WARN = 200, WARN = 200,
UNSTABLE = 250, UNSTABLE = 250,
ERROR = 400, ERROR = 400,
UNKNOWN = 900, DISABLED = 500,
UNKNOWN = 0,
) )
parameters = { parameters = {
'value': Parameter('current value of the Module', readonly=True, 'value': Parameter('current value of the Module', readonly=True,
@ -312,7 +313,7 @@ class Drivable(Writable):
commands = { commands = {
'stop': Command( 'stop': Command(
'cease driving, go to IDLE state', 'cease driving, go to IDLE state',
arguments=[], argument=None,
result=None result=None
), ),
} }
@ -360,7 +361,7 @@ class Communicator(Module):
commands = { commands = {
"communicate": Command("provides the simplest mean to communication", "communicate": Command("provides the simplest mean to communication",
arguments=[StringType()], argument=StringType(),
result=StringType() result=StringType()
), ),
} }

View File

@ -155,15 +155,14 @@ class Override(CountedObj):
class Command(CountedObj): class Command(CountedObj):
"""storage for Commands settings (description + call signature...) """storage for Commands settings (description + call signature...)
""" """
def __init__(self, description, arguments=None, result=None, export=True, optional=False, datatype=None, ctr=None): def __init__(self, description, argument=None, result=None, export=True, optional=False, datatype=None, ctr=None):
super(Command, self).__init__() super(Command, self).__init__()
# descriptive text for humans # descriptive text for humans
self.description = description self.description = description
# list of datatypes for arguments # datatypes for argument/result
self.arguments = arguments or [] self.argument = argument
self.datatype = CommandType(arguments, result)
self.arguments = arguments
self.result = result self.result = result
self.datatype = CommandType(argument, result)
# whether implementation is optional # whether implementation is optional
self.optional = optional self.optional = optional
self.export = export self.export = export

View File

@ -45,9 +45,9 @@ from secop.protocol.messages import EVENTREPLY, IDENTREQUEST, IDENTREPLY, \
ENABLEEVENTSREPLY, DESCRIPTIONREPLY, WRITEREPLY, COMMANDREPLY, \ ENABLEEVENTSREPLY, DESCRIPTIONREPLY, WRITEREPLY, COMMANDREPLY, \
DISABLEEVENTSREPLY, HEARTBEATREPLY DISABLEEVENTSREPLY, HEARTBEATREPLY
from secop.protocol.errors import InternalError, NoSuchModuleError, \ from secop.errors import NoSuchModuleError, NoSuchCommandError, \
NoSuchCommandError, NoSuchParameterError, BadValueError, ReadonlyError, \ NoSuchParameterError, BadValueError, ReadOnlyError, \
ProtocolError ProtocolError, SECoPServerError as InternalError
from secop.params import Parameter from secop.params import Parameter
@ -63,9 +63,9 @@ class Dispatcher(object):
def __init__(self, name, logger, options, srv): def __init__(self, name, logger, options, srv):
# to avoid errors, we want to eat all options here # to avoid errors, we want to eat all options here
self.equipment_id = name self.equipment_id = name
self.nodeopts = {} self.nodeprops = {}
for k in list(options): for k in list(options):
self.nodeopts[k] = options.pop(k) self.nodeprops[k] = options.pop(k)
self.log = logger self.log = logger
# map ALL modulename -> moduleobj # map ALL modulename -> moduleobj
@ -169,7 +169,7 @@ class Dispatcher(object):
res = [] res = []
for aname, aobj in self.get_module(modulename).accessibles.items(): for aname, aobj in self.get_module(modulename).accessibles.items():
if aobj.export: if aobj.export:
res.extend([aname, aobj.for_export()]) res.append([aname, aobj.for_export()])
self.log.debug(u'list accessibles for module %s -> %r' % self.log.debug(u'list accessibles for module %s -> %r' %
(modulename, res)) (modulename, res))
return res return res
@ -183,21 +183,22 @@ class Dispatcher(object):
result = {u'modules': []} result = {u'modules': []}
for modulename in self._export: for modulename in self._export:
module = self.get_module(modulename) module = self.get_module(modulename)
if not module.properties.get('export', False):
continue
# some of these need rework ! # some of these need rework !
mod_desc = {u'accessibles': self.export_accessibles(modulename)} mod_desc = {u'accessibles': self.export_accessibles(modulename)}
for propname, prop in list(module.properties.items()): for propname, prop in list(module.properties.items()):
if propname == 'export':
continue
mod_desc[propname] = prop mod_desc[propname] = prop
result[u'modules'].extend([modulename, mod_desc]) result[u'modules'].append([modulename, mod_desc])
result[u'equipment_id'] = self.equipment_id result[u'equipment_id'] = self.equipment_id
result[u'firmware'] = u'FRAPPY - The Python Framework for SECoP' result[u'firmware'] = u'FRAPPY - The Python Framework for SECoP'
result[u'version'] = u'2018.09' result[u'version'] = u'2018.09'
result.update(self.nodeopts) result.update(self.nodeprops)
return result return result
def _execute_command(self, modulename, command, arguments=None): def _execute_command(self, modulename, command, argument=None):
if arguments is None:
arguments = []
moduleobj = self.get_module(modulename) moduleobj = self.get_module(modulename)
if moduleobj is None: if moduleobj is None:
raise NoSuchModuleError('Module does not exist on this SEC-Node!') raise NoSuchModuleError('Module does not exist on this SEC-Node!')
@ -205,14 +206,16 @@ class Dispatcher(object):
cmdspec = moduleobj.accessibles.get(command, None) cmdspec = moduleobj.accessibles.get(command, None)
if cmdspec is None: if cmdspec is None:
raise NoSuchCommandError('Module has no such command!') raise NoSuchCommandError('Module has no such command!')
num_args_required = len(cmdspec.datatype.argtypes) if argument is None and cmdspec.datatype.argtype is not None:
if num_args_required != len(arguments): raise BadValueError(u'Command needs an argument!')
raise BadValueError(u'Wrong number of arguments (need %d, got %d)!' % (num_args_required, len(arguments)))
if argument is not None and cmdspec.datatype.argtype is None:
raise BadValueError(u'Command takes no argument!')
# now call func and wrap result as value # now call func and wrap result as value
# note: exceptions are handled in handle_request, not here! # note: exceptions are handled in handle_request, not here!
func = getattr(moduleobj, u'do_' + command) func = getattr(moduleobj, u'do_' + command)
res = func(*arguments) res = func(argument) if argument else func()
# XXX: pipe through cmdspec.datatype.result ? # XXX: pipe through cmdspec.datatype.result ?
return res, dict(t=currenttime()) return res, dict(t=currenttime())
@ -225,7 +228,7 @@ class Dispatcher(object):
if pobj is None or not isinstance(pobj, Parameter): if pobj is None or not isinstance(pobj, Parameter):
raise NoSuchParameterError('Module has no such parameter on this SEC-Node!') raise NoSuchParameterError('Module has no such parameter on this SEC-Node!')
if pobj.readonly: if pobj.readonly:
raise ReadonlyError('This parameter can not be changed remotely.') raise ReadOnlyError('This parameter can not be changed remotely.')
writefunc = getattr(moduleobj, u'write_%s' % pname, None) writefunc = getattr(moduleobj, u'write_%s' % pname, None)
# note: exceptions are handled in handle_request, not here! # note: exceptions are handled in handle_request, not here!
@ -272,7 +275,7 @@ class Dispatcher(object):
action, specifier, data = msg action, specifier, data = msg
# special case for *IDN? # special case for *IDN?
if action == IDENTREQUEST: if action == IDENTREQUEST:
action, specifier, data = 'ident', None, None action, specifier, data = '_ident', None, None
self.log.debug(u'Looking for handle_%s' % action) self.log.debug(u'Looking for handle_%s' % action)
handler = getattr(self, u'handle_%s' % action, None) handler = getattr(self, u'handle_%s' % action, None)
@ -286,7 +289,7 @@ class Dispatcher(object):
def handle_help(self, conn, specifier, data): def handle_help(self, conn, specifier, data):
self.log.error('should have been handled in the interface!') self.log.error('should have been handled in the interface!')
def handle_ident(self, conn, specifier, data): def handle__ident(self, conn, specifier, data):
return (IDENTREPLY, None, None) return (IDENTREPLY, None, None)
def handle_describe(self, conn, specifier, data): def handle_describe(self, conn, specifier, data):

View File

@ -1,118 +0,0 @@
# -*- 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>
#
# *****************************************************************************
"""Define (internal) SECoP Errors"""
class SECOPError(RuntimeError):
def __init__(self, *args, **kwds):
RuntimeError.__init__(self)
self.args = args
for k, v in list(kwds.items()):
setattr(self, k, v)
def __repr__(self):
args = ', '.join(map(repr, self.args))
kwds = ', '.join(['%s=%r' % i for i in list(self.__dict__.items())])
res = []
if args:
res.append(args)
if kwds:
res.append(kwds)
return '%s(%s)' % (self.name, ', '.join(res))
@property
def name(self):
return self.__class__.__name__[:-len('Error')]
class InternalError(SECOPError):
name = 'InternalError'
class ProtocolError(SECOPError):
name = 'SyntaxError'
class NoSuchModuleError(SECOPError):
pass
class NoSuchParameterError(SECOPError):
pass
class NoSuchCommandError(SECOPError):
pass
class ReadonlyError(SECOPError):
pass
class BadValueError(SECOPError):
pass
class CommandFailedError(SECOPError):
pass
class CommandRunningError(SECOPError):
pass
class CommunicationFailedError(SECOPError):
pass
class IsBusyError(SECOPError):
pass
class IsErrorError(SECOPError):
pass
class DisabledError(SECOPError):
pass
EXCEPTIONS = dict(
NoSuchModule=NoSuchModuleError,
NoSuchParameter=NoSuchParameterError,
NoSuchCommand=NoSuchCommandError,
CommandFailed=CommandFailedError,
CommandRunning=CommandRunningError,
Readonly=ReadonlyError,
BadValue=BadValueError,
CommunicationFailed=CommunicationFailedError,
IsBusy=IsBusyError,
IsError=IsErrorError,
Disabled=DisabledError,
SyntaxError=ProtocolError,
InternalError=InternalError,
# internal short versions (candidates for spec)
Protocol=ProtocolError,
Internal=InternalError,
)

View File

@ -21,6 +21,7 @@
"""provides tcp interface to the SECoP Server""" """provides tcp interface to the SECoP Server"""
from __future__ import print_function from __future__ import print_function
import sys
import socket import socket
import collections import collections
@ -29,7 +30,7 @@ try:
except ImportError: except ImportError:
import SocketServer as socketserver # py2 import SocketServer as socketserver # py2
from secop.lib import formatExtendedStack, formatException from secop.lib import formatExtendedStack, formatException, formatExtendedTraceback
from secop.protocol.messages import HELPREQUEST, HELPREPLY, HelpMessage from secop.protocol.messages import HELPREQUEST, HELPREPLY, HelpMessage
from secop.errors import SECoPError from secop.errors import SECoPError
from secop.protocol.interface import encode_msg_frame, get_msg, decode_msg from secop.protocol.interface import encode_msg_frame, get_msg, decode_msg
@ -136,7 +137,7 @@ class TCPRequestHandler(socketserver.BaseRequestHandler):
print('--------------------') print('--------------------')
print(formatException()) print(formatException())
print('--------------------') print('--------------------')
print(formatExtendedStack()) print(formatExtendedTraceback(sys.exc_info()))
print('====================') print('====================')
if not result: if not result:

View File

@ -26,7 +26,7 @@ from __future__ import print_function
IDENTREQUEST = u'*IDN?' # literal IDENTREQUEST = u'*IDN?' # literal
# literal! first part is fixed! # literal! first part is fixed!
IDENTREPLY = u'SINE2020&ISSE,SECoP,V2018-06-16,rc1' IDENTREPLY = u'SINE2020&ISSE,SECoP,V2018-11-07,v1.0\\beta'
DESCRIPTIONREQUEST = u'describe' # literal DESCRIPTIONREQUEST = u'describe' # literal
DESCRIPTIONREPLY = u'describing' # +<id> +json DESCRIPTIONREPLY = u'describing' # +<id> +json
@ -41,11 +41,16 @@ COMMANDREQUEST = u'do' # +module:command +json args (if needed)
# +module:command +json args (if needed) # send after the command finished ! # +module:command +json args (if needed) # send after the command finished !
COMMANDREPLY = u'done' COMMANDREPLY = u'done'
# +module[:parameter] +json_value -> NO direct reply, calls POLL internally # +module[:parameter] +json_value
WRITEREQUEST = u'change' WRITEREQUEST = u'change'
# +module[:parameter] +json_value # send with the read back value # +module[:parameter] +json_value # send with the read back value
WRITEREPLY = u'changed' WRITEREPLY = u'changed'
# +module[:parameter] +json_value
BUFFERREQUEST = u'buffer'
# +module[:parameter] +json_value # send with the read back value
BUFFERREPLY = u'buffered'
# +module[:parameter] -> NO direct reply, calls POLL internally! # +module[:parameter] -> NO direct reply, calls POLL internally!
POLLREQUEST = u'read' POLLREQUEST = u'read'
EVENTREPLY = u'update' # +module[:parameter] +json_value (value, qualifiers_as_dict) EVENTREPLY = u'update' # +module[:parameter] +json_value (value, qualifiers_as_dict)
@ -66,6 +71,7 @@ REQUEST2REPLY = {
DISABLEEVENTSREQUEST: DISABLEEVENTSREPLY, DISABLEEVENTSREQUEST: DISABLEEVENTSREPLY,
COMMANDREQUEST: COMMANDREPLY, COMMANDREQUEST: COMMANDREPLY,
WRITEREQUEST: WRITEREPLY, WRITEREQUEST: WRITEREPLY,
BUFFERREQUEST: BUFFERREPLY,
POLLREQUEST: EVENTREPLY, POLLREQUEST: EVENTREPLY,
HEARTBEATREQUEST: HEARTBEATREPLY, HEARTBEATREQUEST: HEARTBEATREPLY,
HELPREQUEST: HELPREPLY, HELPREQUEST: HELPREPLY,

View File

@ -125,7 +125,7 @@ class Cryostat(CryoBase):
commands = dict( commands = dict(
stop=Command( stop=Command(
"Stop ramping the setpoint\n\nby setting the current setpoint as new target", "Stop ramping the setpoint\n\nby setting the current setpoint as new target",
[], None,
None), None),
) )

View File

@ -314,6 +314,6 @@ class DatatypesTest(Readable):
class ArrayTest(Readable): class ArrayTest(Readable):
parameters = { parameters = {
"x": Parameter('value', datatype=ArrayOf(FloatRange(), 100000, 100000), "x": Parameter('value', datatype=ArrayOf(FloatRange(), 0, 100000),
default = 100000 * [0]), default = 100000 * [0]),
} }

View File

@ -39,7 +39,7 @@ from secop.lib import lazy_property
#from secop.parse import Parser #from secop.parse import Parser
from secop.datatypes import IntRange, FloatRange, StringType, TupleOf, \ from secop.datatypes import IntRange, FloatRange, StringType, TupleOf, \
ArrayOf, EnumType ArrayOf, EnumType
from secop.errors import ConfigError, ProgrammingError, CommunicationError, \ from secop.errors import ConfigError, ProgrammingError, CommunicationFailedError, \
HardwareError HardwareError
from secop.modules import Parameter, Command, Override, Module, Readable, Drivable from secop.modules import Parameter, Command, Override, Module, Readable, Drivable
@ -57,7 +57,7 @@ __all__ = [
] ]
EXC_MAPPING = { EXC_MAPPING = {
PyTango.CommunicationFailed: CommunicationError, PyTango.CommunicationFailedError: CommunicationFailedError,
PyTango.WrongNameSyntax: ConfigError, PyTango.WrongNameSyntax: ConfigError,
PyTango.DevFailed: HardwareError, PyTango.DevFailed: HardwareError,
} }
@ -65,7 +65,7 @@ EXC_MAPPING = {
REASON_MAPPING = { REASON_MAPPING = {
'Entangle_ConfigurationError': ConfigError, 'Entangle_ConfigurationError': ConfigError,
'Entangle_WrongAPICall': ProgrammingError, 'Entangle_WrongAPICall': ProgrammingError,
'Entangle_CommunicationFailure': CommunicationError, 'Entangle_CommunicationFailure': CommunicationFailedError,
'Entangle_InvalidValue': ValueError, 'Entangle_InvalidValue': ValueError,
'Entangle_ProgrammingError': ProgrammingError, 'Entangle_ProgrammingError': ProgrammingError,
'Entangle_HardwareFailure': HardwareError, 'Entangle_HardwareFailure': HardwareError,
@ -174,7 +174,7 @@ class PyTangoDevice(Module):
} }
commands = { commands = {
'reset': Command('Tango reset command', arguments=[], result=None), 'reset': Command('Tango reset command', argument=None, result=None),
} }
tango_status_mapping = { tango_status_mapping = {
@ -255,7 +255,7 @@ class PyTangoDevice(Module):
try: try:
device.State device.State
except AttributeError: except AttributeError:
raise CommunicationError( raise CommunicationFailedError(
self, 'connection to Tango server failed, ' self, 'connection to Tango server failed, '
'is the server running?') 'is the server running?')
return self._applyGuardsToPyTangoDevice(device) return self._applyGuardsToPyTangoDevice(device)
@ -351,11 +351,11 @@ class PyTangoDevice(Module):
"""Process the exception raised either by communication or _com_return. """Process the exception raised either by communication or _com_return.
Should raise a NICOS exception. Default is to raise Should raise a NICOS exception. Default is to raise
CommunicationError. CommunicationFailedError.
""" """
reason = self._tango_exc_reason(err) reason = self._tango_exc_reason(err)
exclass = REASON_MAPPING.get( exclass = REASON_MAPPING.get(
reason, EXC_MAPPING.get(type(err), CommunicationError)) reason, EXC_MAPPING.get(type(err), CommunicationFailedError))
fulldesc = self._tango_exc_desc(err) fulldesc = self._tango_exc_desc(err)
self.log.debug('PyTango error: %s', fulldesc) self.log.debug('PyTango error: %s', fulldesc)
raise exclass(self, fulldesc) raise exclass(self, fulldesc)
@ -405,7 +405,7 @@ class Sensor(AnalogInput):
commands = { commands = {
'setposition': Command('Set the position to the given value.', 'setposition': Command('Set the position to the given value.',
arguments=[FloatRange()], result=None, argument=FloatRange(), result=None,
), ),
} }
@ -450,7 +450,7 @@ class AnalogOutput(PyTangoDevice, Drivable):
), ),
} }
commands = { commands = {
'stop': Command('Stops current movement.', arguments=[], result=None), 'stop': Command('Stops current movement.', argument=None, result=None),
} }
_history = () _history = ()
_timeout = None _timeout = None
@ -615,7 +615,7 @@ class Actuator(AnalogOutput):
commands = { commands = {
'setposition': Command('Set the position to the given value.', 'setposition': Command('Set the position to the given value.',
arguments=[FloatRange()], result=None, argument=FloatRange(), result=None,
), ),
} }
@ -656,7 +656,7 @@ class Motor(Actuator):
} }
commands = { commands = {
'reference': Command('Do a reference run', arguments=[], result=None), 'reference': Command('Do a reference run', argument=None, result=None),
} }
def read_refpos(self, maxage=0): def read_refpos(self, maxage=0):
@ -952,25 +952,25 @@ class StringIO(PyTangoDevice, Module):
commands = { commands = {
'communicate': Command('Send a string and return the reply', 'communicate': Command('Send a string and return the reply',
arguments=[StringType()], argument=StringType(),
result=StringType()), result=StringType()),
'flush': Command('Flush output buffer', 'flush': Command('Flush output buffer',
arguments=[], result=None), argument=None, result=None),
'read': Command('read some characters from input buffer', 'read': Command('read some characters from input buffer',
arguments=[IntRange()], result=StringType()), argument=IntRange(0), result=StringType()),
'write': Command('write some chars to output', 'write': Command('write some chars to output',
arguments=[StringType()], result=None), argument=StringType(), result=None),
'readLine': Command('Read sol - a whole line - eol', 'readLine': Command('Read sol - a whole line - eol',
arguments=[], result=StringType()), argument=None, result=StringType()),
'writeLine': Command('write sol + a whole line + eol', 'writeLine': Command('write sol + a whole line + eol',
arguments=[StringType()], result=None), argument=StringType(), result=None),
'availableChars': Command('return number of chars in input buffer', 'availableChars': Command('return number of chars in input buffer',
arguments=[], result=IntRange(0)), argument=None, result=IntRange(0)),
'availableLines': Command('return number of lines in input buffer', 'availableLines': Command('return number of lines in input buffer',
arguments=[], result=IntRange(0)), argument=None, result=IntRange(0)),
'multiCommunicate': Command('perform a sequence of communications', 'multiCommunicate': Command('perform a sequence of communications',
arguments=[ArrayOf( argument=ArrayOf(
TupleOf(StringType(), IntRange()), 100)], TupleOf(StringType(), IntRange()), 100),
result=ArrayOf(StringType(), 100)), result=ArrayOf(StringType(), 100)),
} }

View File

@ -57,30 +57,28 @@ def clientobj(request):
# pylint: disable=redefined-outer-name # pylint: disable=redefined-outer-name
def test_describing_data_decode(clientobj): def test_describing_data_decode(clientobj):
assert OrderedDict( assert {'modules': OrderedDict(), 'properties': {}
[('a', 1)]) == clientobj._decode_list_to_ordereddict(['a', 1])
assert {'modules': {}, 'properties': {}
} == clientobj._decode_substruct(['modules'], {}) } == clientobj._decode_substruct(['modules'], {})
describing_data = {'equipment_id': 'eid', describing_data = {'equipment_id': 'eid',
'modules': ['LN2', {'commands': [], 'modules': [['LN2', {'commands': [],
'interfaces': ['Readable', 'Module'], 'interfaces': ['Readable', 'Module'],
'parameters': ['value', {'datatype': ['double'], 'parameters': [['value', {'datatype': ['double'],
'description': 'current value', 'description': 'current value',
'readonly': True, 'readonly': True,
} }
] ]]
} }
] ]]
} }
decoded_data = {'modules': {'LN2': {'commands': {}, decoded_data = {'modules': OrderedDict([('LN2', {'commands': OrderedDict(),
'parameters': {'value': {'datatype': ['double'], 'parameters': OrderedDict([('value', {'datatype': ['double'],
'description': 'current value', 'description': 'current value',
'readonly': True, 'readonly': True,
} }
}, )]),
'properties': {'interfaces': ['Readable', 'Module']} 'properties': {'interfaces': ['Readable', 'Module']}
} }
}, )]),
'properties': {'equipment_id': 'eid', 'properties': {'equipment_id': 'eid',
} }
} }

View File

@ -65,7 +65,7 @@ def test_FloatRange():
FloatRange('x', 'Y') FloatRange('x', 'Y')
dt = FloatRange() dt = FloatRange()
assert dt.as_json == ['double'] assert dt.as_json == ['double', None, None]
def test_IntRange(): def test_IntRange():
@ -86,7 +86,8 @@ def test_IntRange():
IntRange('xc', 'Yx') IntRange('xc', 'Yx')
dt = IntRange() dt = IntRange()
assert dt.as_json == ['int'] assert dt.as_json[0] == 'int'
assert dt.as_json[1] < 0 < dt.as_json[2]
def test_EnumType(): def test_EnumType():
@ -128,13 +129,13 @@ def test_EnumType():
def test_BLOBType(): def test_BLOBType():
# test constructor catching illegal arguments # test constructor catching illegal arguments
with pytest.raises(ValueError): dt = BLOBType()
dt = BLOBType() assert dt.as_json == ['blob', 255, 255]
dt = BLOBType(10) dt = BLOBType(10)
assert dt.as_json == ['blob', 10] assert dt.as_json == ['blob', 10, 10]
dt = BLOBType(3, 10) dt = BLOBType(3, 10)
assert dt.as_json == ['blob', 10, 3] assert dt.as_json == ['blob', 3, 10]
with pytest.raises(ValueError): with pytest.raises(ValueError):
dt.validate(9) dt.validate(9)
@ -156,10 +157,10 @@ def test_StringType():
# test constructor catching illegal arguments # test constructor catching illegal arguments
dt = StringType() dt = StringType()
dt = StringType(12) dt = StringType(12)
assert dt.as_json == ['string', 12] assert dt.as_json == ['string', 0, 12]
dt = StringType(4, 11) dt = StringType(4, 11)
assert dt.as_json == ['string', 11, 4] assert dt.as_json == ['string', 4, 11]
with pytest.raises(ValueError): with pytest.raises(ValueError):
dt.validate(9) dt.validate(9)
@ -208,12 +209,12 @@ def test_ArrayOf():
with pytest.raises(ValueError): with pytest.raises(ValueError):
ArrayOf(int) ArrayOf(int)
with pytest.raises(ValueError): with pytest.raises(ValueError):
ArrayOf(IntRange(-10,10)) ArrayOf(-3, IntRange(-10,10))
dt = ArrayOf(IntRange(-10, 10), 5) dt = ArrayOf(IntRange(-10, 10), 5)
assert dt.as_json == ['array', ['int', -10, 10], 5] assert dt.as_json == ['array', 5, 5, ['int', -10, 10]]
dt = ArrayOf(IntRange(-10, 10), 1, 3) dt = ArrayOf(IntRange(-10, 10), 1, 3)
assert dt.as_json == ['array', ['int', -10, 10], 3, 1] assert dt.as_json == ['array', 1, 3, ['int', -10, 10]]
with pytest.raises(ValueError): with pytest.raises(ValueError):
dt.validate(9) dt.validate(9)
with pytest.raises(ValueError): with pytest.raises(ValueError):
@ -231,7 +232,7 @@ def test_TupleOf():
TupleOf(2) TupleOf(2)
dt = TupleOf(IntRange(-10, 10), BoolType()) dt = TupleOf(IntRange(-10, 10), BoolType())
assert dt.as_json == ['tuple', [['int', -10, 10], ['bool']]] assert dt.as_json == ['tuple', ['int', -10, 10], ['bool']]
with pytest.raises(ValueError): with pytest.raises(ValueError):
dt.validate(9) dt.validate(9)
@ -252,8 +253,8 @@ def test_StructOf():
StructOf(IntRange=1) StructOf(IntRange=1)
dt = StructOf(a_string=StringType(55), an_int=IntRange(0, 999)) dt = StructOf(a_string=StringType(55), an_int=IntRange(0, 999))
assert dt.as_json == ['struct', {'a_string': ['string', 55], assert dt.as_json == [u'struct', {u'a_string': [u'string', 0, 55],
'an_int': ['int', 0, 999], u'an_int': [u'int', 0, 999],
}] }]
with pytest.raises(ValueError): with pytest.raises(ValueError):
@ -285,7 +286,6 @@ def test_get_datatype():
assert isinstance(get_datatype(['int']), IntRange) assert isinstance(get_datatype(['int']), IntRange)
assert isinstance(get_datatype(['int', -10]), IntRange) assert isinstance(get_datatype(['int', -10]), IntRange)
assert isinstance(get_datatype(['int', None, 10]), IntRange)
assert isinstance(get_datatype(['int', -10, 10]), IntRange) assert isinstance(get_datatype(['int', -10, 10]), IntRange)
with pytest.raises(ValueError): with pytest.raises(ValueError):
@ -320,8 +320,7 @@ def test_get_datatype():
with pytest.raises(ValueError): with pytest.raises(ValueError):
get_datatype(['blob', 10, -10, 1]) get_datatype(['blob', 10, -10, 1])
with pytest.raises(ValueError): get_datatype(['string'])
get_datatype(['string'])
assert isinstance(get_datatype(['string', 1]), StringType) assert isinstance(get_datatype(['string', 1]), StringType)
assert isinstance(get_datatype(['string', 10, 1]), StringType) assert isinstance(get_datatype(['string', 10, 1]), StringType)
@ -336,15 +335,15 @@ def test_get_datatype():
get_datatype(['array', 1]) get_datatype(['array', 1])
with pytest.raises(ValueError): with pytest.raises(ValueError):
get_datatype(['array', [1], 2, 3]) get_datatype(['array', [1], 2, 3])
assert isinstance(get_datatype(['array', ['blob', 1], 1]), ArrayOf) assert isinstance(get_datatype(['array', 1, 1, ['blob', 1]]), ArrayOf)
assert isinstance(get_datatype(['array', ['blob', 1], 1]).subtype, BLOBType) assert isinstance(get_datatype(['array', 1, 1, ['blob', 1]]).subtype, BLOBType)
with pytest.raises(ValueError): with pytest.raises(ValueError):
get_datatype(['array', ['blob', 1], -10]) get_datatype(['array', ['blob', 1], -10])
with pytest.raises(ValueError): with pytest.raises(ValueError):
get_datatype(['array', ['blob', 1], 10, -10]) get_datatype(['array', ['blob', 1], 10, -10])
assert isinstance(get_datatype(['array', ['blob', 1], 10, 1]), ArrayOf) assert isinstance(get_datatype(['array', 1, 10, ['blob', 1]]), ArrayOf)
with pytest.raises(ValueError): with pytest.raises(ValueError):
get_datatype(['tuple']) get_datatype(['tuple'])
@ -352,16 +351,15 @@ def test_get_datatype():
get_datatype(['tuple', 1]) get_datatype(['tuple', 1])
with pytest.raises(ValueError): with pytest.raises(ValueError):
get_datatype(['tuple', [1], 2, 3]) get_datatype(['tuple', [1], 2, 3])
assert isinstance(get_datatype(['tuple', [['blob', 1]]]), TupleOf) assert isinstance(get_datatype(['tuple', ['blob', 1]]), TupleOf)
assert isinstance(get_datatype( assert isinstance(get_datatype(['tuple', ['blob', 1]]).subtypes[0], BLOBType)
['tuple', [['blob', 1]]]).subtypes[0], BLOBType)
with pytest.raises(ValueError): with pytest.raises(ValueError):
get_datatype(['tuple', [['blob', 1]], -10]) get_datatype(['tuple', ['blob', 1], -10])
with pytest.raises(ValueError): with pytest.raises(ValueError):
get_datatype(['tuple', [['blob', 1]], 10, -10]) get_datatype(['tuple', ['blob', 1], 10, -10])
assert isinstance(get_datatype(['tuple', [['blob', 1], ['int']]]), TupleOf) assert isinstance(get_datatype(['tuple', ['blob', 1], ['int']]), TupleOf)
with pytest.raises(ValueError): with pytest.raises(ValueError):
get_datatype(['struct']) get_datatype(['struct'])
@ -370,13 +368,12 @@ def test_get_datatype():
with pytest.raises(ValueError): with pytest.raises(ValueError):
get_datatype(['struct', [1], 2, 3]) get_datatype(['struct', [1], 2, 3])
assert isinstance(get_datatype(['struct', {'blob': ['blob', 1]}]), StructOf) assert isinstance(get_datatype(['struct', {'blob': ['blob', 1]}]), StructOf)
assert isinstance(get_datatype( assert isinstance(get_datatype(['struct', {'blob': ['blob', 1]}]).named_subtypes['blob'], BLOBType)
['struct', {'blob': ['blob', 1]}]).named_subtypes['blob'], BLOBType)
with pytest.raises(ValueError): with pytest.raises(ValueError):
get_datatype(['struct', [['blob', 1]], -10]) get_datatype(['struct', ['blob', 1], -10])
with pytest.raises(ValueError): with pytest.raises(ValueError):
get_datatype(['struct', [['blob', 1]], 10, -10]) get_datatype(['struct', ['blob', 1], 10, -10])
assert isinstance(get_datatype( assert isinstance(get_datatype(
['struct', {'blob': ['blob', 1], 'int':['int']}]), StructOf) ['struct', {'blob': ['blob', 1], 'int':['int']}]), StructOf)

View File

@ -69,13 +69,13 @@ def test_ModuleMeta():
'param2': Parameter('param2', datatype=BoolType(), default=True), 'param2': Parameter('param2', datatype=BoolType(), default=True),
}, },
"commands": { "commands": {
"cmd": Command('stuff',[BoolType()], BoolType()) "cmd": Command('stuff',BoolType(), BoolType())
}, },
"accessibles": { "accessibles": {
'a1': Parameter('a1', datatype=BoolType(), default=False), 'a1': Parameter('a1', datatype=BoolType(), default=False),
'a2': Parameter('a2', datatype=BoolType(), default=True), 'a2': Parameter('a2', datatype=BoolType(), default=True),
'value':Override(datatype=BoolType(), default = True), 'value':Override(datatype=BoolType(), default = True),
'cmd2': Command('another stuff', [BoolType()], BoolType()), 'cmd2': Command('another stuff', BoolType(), BoolType()),
}, },
"do_cmd": lambda self, arg: not arg, "do_cmd": lambda self, arg: not arg,
"do_cmd2": lambda self, arg: not arg, "do_cmd2": lambda self, arg: not arg,

View File

@ -35,10 +35,10 @@ from secop.params import Command, Parameter, Override
def test_Command(): def test_Command():
cmd = Command('do_something', [], None) cmd = Command('do_something')
assert cmd.description assert cmd.description
assert cmd.ctr assert cmd.ctr
assert cmd.arguments == [] assert cmd.argument is None
assert cmd.result is None assert cmd.result is None