Bug hunting and polishing

Change-Id: I0f05730dd4e01e926ab0c4870c27ed5754f3ccfd
This commit is contained in:
Enrico Faulhaber
2017-01-20 18:21:27 +01:00
parent 8e3d0da5dd
commit d5e935788f
18 changed files with 552 additions and 202 deletions

View File

@ -38,7 +38,10 @@ def main(argv=None):
if argv is None: if argv is None:
argv = sys.argv argv = sys.argv
if '-d' in argv:
loggers.initLogging('gui', 'debug') loggers.initLogging('gui', 'debug')
else:
loggers.initLogging('gui', 'info')
app = QApplication(argv) app = QApplication(argv)

View File

@ -1,8 +1,12 @@
[server] [equipment]
id=demonstration
[interface testing]
interface=tcp
bindto=0.0.0.0 bindto=0.0.0.0
bindport=10767 bindport=10767
interface = tcp # protocol to use for this interface
framing=demo framing=eol
encoding=demo encoding=demo
[device heatswitch] [device heatswitch]

View File

@ -35,6 +35,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
class TCPConnection(object): class TCPConnection(object):
@ -115,9 +116,9 @@ class Value(object):
def __init__(self, value, qualifiers={}): def __init__(self, value, qualifiers={}):
self.value = value self.value = value
if 't' in qualifiers:
self.t = parse_time(qualifiers.pop('t'))
self.__dict__.update(qualifiers) self.__dict__.update(qualifiers)
if 't' in qualifiers:
self.t = parse_time(qualifiers['t'])
def __repr__(self): def __repr__(self):
r = [] r = []
@ -153,9 +154,12 @@ class Client(object):
port = int(opts.pop('port', 10767)) port = int(opts.pop('port', 10767))
self.contactPoint = "tcp://%s:%d" % (host, port) self.contactPoint = "tcp://%s:%d" % (host, port)
self.connection = TCPConnection(host, port) self.connection = TCPConnection(host, port)
# maps an expected reply to an list containing a single Event() # maps an expected reply to a list containing a single Event()
# upon rcv of that reply, the event is set and the listitem 0 is # upon rcv of that reply, entry is appended with False and
# appended with the reply-tuple # the data of the reply.
# if an error is received, the entry is appended with True and an
# appropriate Exception.
# Then the Event is set.
self.expected_replies = {} self.expected_replies = {}
# maps spec to a set of callback functions (or single_shot callbacks) # maps spec to a set of callback functions (or single_shot callbacks)
@ -193,47 +197,60 @@ class Client(object):
self.log.info('connected to: ' + line.strip()) self.log.info('connected to: ' + line.strip())
self.secop_id = line self.secop_id = line
continue continue
msgtype, spec, data = self._decode_message(line) msgtype, spec, data = self.decode_message(line)
if msgtype in ('update', 'changed'): if msgtype in ('update', 'changed'):
# handle async stuff # handle async stuff
self._handle_event(spec, data) self._handle_event(spec, data)
if msgtype != 'update':
# handle sync stuff # handle sync stuff
if msgtype in self.expected_replies: self._handle_sync_reply(msgtype, spec, data)
entry = self.expected_replies[msgtype]
entry.extend([msgtype, spec, data])
# wake up calling process
entry[0].set()
elif msgtype == "error":
# XXX: hack!
if len(self.expected_replies) == 1:
entry = self.expected_replies.values()[0]
entry.extend([msgtype, spec, data])
# wake up calling process
entry[0].set()
else: # try to find the right request....
print data[0] # should be the origin request
# XXX: make an assignment of ERROR to an expected reply.
self.log.error('TODO: handle ERROR replies!')
else:
self.log.error('ignoring unexpected reply %r' % line)
def _encode_message(self, requesttype, spec='', data=Ellipsis): def _handle_sync_reply(self, msgtype, spec, data):
# handle sync stuff
if msgtype == "error":
# find originating msgtype and map to expected_reply_type
# errormessages carry to offending request as the first
# result in the resultist
_msgtype, _spec, _data = self.decode_message(data[0])
_reply = self._get_reply_from_request(_msgtype)
entry = self.expected_replies.get((_reply, _spec), None)
if entry:
self.log.error("request %r resulted in Error %r" %
(data[0], spec))
entry.extend([True, EXCEPTIONS[spec](data)])
entry[0].set()
return
self.log.error("got an unexpected error %s %r" %
(spec, data[0]))
return
if msgtype == "describing":
data = [spec, data]
spec = ''
entry = self.expected_replies.get((msgtype, spec), None)
if entry:
self.log.debug("got expected reply '%s %s'" %
(msgtype, spec) if spec else
"got expected reply '%s'" % msgtype)
entry.extend([False, data])
entry[0].set()
return
def encode_message(self, requesttype, spec='', data=None):
"""encodes the given message to a string """encodes the given message to a string
""" """
req = [str(requesttype)] req = [str(requesttype)]
if spec: if spec:
req.append(str(spec)) req.append(str(spec))
if data is not Ellipsis: if data is not None:
req.append(json.dumps(data)) req.append(json.dumps(data))
req = ' '.join(req) req = ' '.join(req)
return req return req
def _decode_message(self, msg): def decode_message(self, msg):
"""return a decoded message tripel""" """return a decoded message tripel"""
msg = msg.strip() msg = msg.strip()
if ' ' not in msg: if ' ' not in msg:
return msg, None, None return msg, '', None
msgtype, spec = msg.split(' ', 1) msgtype, spec = msg.split(' ', 1)
data = None data = None
if ' ' in spec: if ' ' in spec:
@ -277,8 +294,7 @@ class Client(object):
return self._getDescribingModuleData(module)['parameters'][parameter] return self._getDescribingModuleData(module)['parameters'][parameter]
def _issueDescribe(self): def _issueDescribe(self):
_, self.equipment_id, self.describing_data = self.communicate( self.equipment_id, self.describing_data = self.communicate('describe')
'describe')
for module, moduleData in self.describing_data['modules'].items(): for module, moduleData in self.describing_data['modules'].items():
for parameter, parameterData in moduleData['parameters'].items(): for parameter, parameterData in moduleData['parameters'].items():
@ -299,53 +315,71 @@ class Client(object):
self.callbacks.setdefault('%s:%s' % self.callbacks.setdefault('%s:%s' %
(module, parameter), set()).discard(cb) (module, parameter), set()).discard(cb)
def communicate(self, msgtype, spec='', data=Ellipsis): def _get_reply_from_request(self, requesttype):
# maps each (sync) request to the corresponding reply # maps each (sync) request to the corresponding reply
# XXX: should go to the encoder! and be imported here (or make a # XXX: should go to the encoder! and be imported here
# translating method)
REPLYMAP = { REPLYMAP = {
"describe": "describing", "describe": "describing",
"do": "done", "do": "done",
"change": "changed", "change": "changed",
"activate": "active", "activate": "active",
"deactivate": "inactive", "deactivate": "inactive",
"*IDN?": "SECoP,", "read": "update",
#"*IDN?": "SECoP,", # XXX: !!!
"ping": "pong", "ping": "pong",
} }
return REPLYMAP.get(requesttype, requesttype)
def communicate(self, msgtype, spec='', data=None):
self.log.debug('communicate: %r %r %r' % (msgtype, spec, data))
if self.stopflag: if self.stopflag:
raise RuntimeError('alreading stopping!') raise RuntimeError('alreading stopping!')
if msgtype == 'read': if msgtype == "*IDN?":
# send a poll request and then check incoming events return self.secop_id
if ':' not in spec:
spec = spec + ':value'
event = threading.Event()
result = ['update', spec]
self.single_shots.setdefault(spec, set()).add(
lambda d: (result.append(d), event.set()))
self.connection.writeline(
self._encode_message(
msgtype, spec, data))
if event.wait(10):
return tuple(result)
raise RuntimeError("timeout upon waiting for reply!")
rply = REPLYMAP[msgtype] if msgtype not in ('*IDN?', 'describe', 'activate',
if rply in self.expected_replies: 'deactivate', 'do', 'change', 'read', 'ping', 'help'):
raise EXCEPTIONS['Protocol'](errorclass='Protocol',
errorinfo='%r: No Such Messagetype defined!' %
msgtype,
origin=self.encode_message(msgtype, spec, data))
# sanitize input + handle syntactic sugar
msgtype = str(msgtype)
spec = str(spec)
if msgtype == 'change' and ':' not in spec:
spec = spec + ':target'
if msgtype == 'read' and ':' not in spec:
spec = spec + ':value'
# check if a such a request is already out
rply = self._get_reply_from_request(msgtype)
if (rply, spec) in self.expected_replies:
raise RuntimeError( raise RuntimeError(
"can not have more than one requests of the same type at the same time!") "can not have more than one requests of the same type at the same time!")
# prepare sending request
event = threading.Event() event = threading.Event()
self.expected_replies[rply] = [event] self.expected_replies[(rply, spec)] = [event]
self.log.debug('prepared reception of %r msg' % rply) self.log.debug('prepared reception of %r msg' % rply)
self.connection.writeline(self._encode_message(msgtype, spec, data))
self.log.debug('sent %r msg' % msgtype) # send request
if event.wait(10): # wait 10s for reply msg = self.encode_message(msgtype, spec, data)
self.connection.writeline(msg)
self.log.debug('sent msg %r' % msg)
# wait for reply. timeout after 10s
if event.wait(10):
self.log.debug('checking reply') self.log.debug('checking reply')
result = self.expected_replies[rply][1:4] event, is_error, result = self.expected_replies.pop((rply, spec))
del self.expected_replies[rply] if is_error:
# if result[0] == "ERROR": # if error, result contains the rigth Exception to raise
# raise RuntimeError('Got %s! %r' % (str(result[1]), repr(result[2]))) raise result
return result return result
del self.expected_replies[rply]
# timed out
del self.expected_replies[(rply, spec)]
# XXX: raise a TimedOut ?
raise RuntimeError("timeout upon waiting for reply to %r!" % msgtype) raise RuntimeError("timeout upon waiting for reply to %r!" % msgtype)
def quit(self): def quit(self):
@ -379,6 +413,9 @@ class Client(object):
return result return result
def getParameter(self, module, parameter):
return self.communicate('read', '%s:%s' % (module, parameter))
def setParameter(self, module, parameter, value): def setParameter(self, module, parameter, value):
validator = self._getDescribingParameterData(module, validator = self._getDescribingParameterData(module,
parameter)['validator'] parameter)['validator']
@ -417,7 +454,11 @@ class Client(object):
def getProperties(self, module, parameter): def getProperties(self, module, parameter):
return self.describing_data['modules'][ return self.describing_data['modules'][
module]['parameters'][parameter].items() module]['parameters'][parameter]
def syncCommunicate(self, *msg): def syncCommunicate(self, *msg):
return self.communicate(*msg) return self.communicate(*msg)
def ping(self, pingctr=[0]):
pingctr[0] = pingctr[0] + 1
self.communicate("ping", pingctr[0])

View File

@ -35,7 +35,7 @@ import threading
from secop.lib.parsing import format_time from secop.lib.parsing import format_time
from secop.errors import ConfigError, ProgrammingError from secop.errors import ConfigError, ProgrammingError
from secop.protocol import status from secop.protocol import status
from secop.validators import enum, vector, floatrange from secop.validators import enum, vector, floatrange, validator_to_str
EVENT_ONLY_ON_CHANGED_VALUES = False EVENT_ONLY_ON_CHANGED_VALUES = False
@ -74,9 +74,9 @@ class PARAM(object):
unit=self.unit, unit=self.unit,
readonly=self.readonly, readonly=self.readonly,
value=self.value, value=self.value,
timestamp=format_time(self.timestamp) if self.timestamp else None, timestamp=format_time(
validator=str(self.validator) if not isinstance( self.timestamp) if self.timestamp else None,
self.validator, type) else self.validator.__name__ validator=validator_to_str(self.validator),
) )
@ -260,7 +260,7 @@ class Device(object):
# only check if validator given # only check if validator given
try: try:
v = validator(v) v = validator(v)
except ValueError as e: except (ValueError, TypeError) as e:
raise ConfigError('Device %s: config parameter %r:\n%r' raise ConfigError('Device %s: config parameter %r:\n%r'
% (self.name, k, e)) % (self.name, k, e))
setattr(self, k, v) setattr(self, k, v)
@ -285,15 +285,15 @@ class Readable(Device):
default="Readable", validator=str), default="Readable", validator=str),
'value': PARAM('current value of the device', readonly=True, default=0.), 'value': PARAM('current value of the device', readonly=True, default=0.),
'pollinterval': PARAM('sleeptime between polls', readonly=False, default=5, validator=floatrange(1, 120),), 'pollinterval': PARAM('sleeptime between polls', readonly=False, default=5, validator=floatrange(1, 120),),
'status': PARAM('current status of the device', default=status.OK, # 'status': PARAM('current status of the device', default=status.OK,
validator=enum(**{'idle': status.OK, # validator=enum(**{'idle': status.OK,
'BUSY': status.BUSY, # 'BUSY': status.BUSY,
'WARN': status.WARN, # 'WARN': status.WARN,
'UNSTABLE': status.UNSTABLE, # 'UNSTABLE': status.UNSTABLE,
'ERROR': status.ERROR, # 'ERROR': status.ERROR,
'UNKNOWN': status.UNKNOWN}), # 'UNKNOWN': status.UNKNOWN}),
readonly=True), # readonly=True),
'status2': PARAM('current status of the device', default=(status.OK, ''), 'status': PARAM('current status of the device', default=(status.OK, ''),
validator=vector(enum(**{'idle': status.OK, validator=vector(enum(**{'idle': status.OK,
'BUSY': status.BUSY, 'BUSY': status.BUSY,
'WARN': status.WARN, 'WARN': status.WARN,

View File

@ -68,21 +68,27 @@ class Switch(Driveable):
def read_status(self, maxage=0): def read_status(self, maxage=0):
self.log.info("read status") self.log.info("read status")
self._update() info = self._update()
if self.target == self.value: if self.target == self.value:
return status.OK return status.OK, ''
return status.BUSY return status.BUSY, info
def _update(self): def _update(self):
started = self.PARAMS['target'].timestamp started = self.PARAMS['target'].timestamp
info = ''
if self.target > self.value: if self.target > self.value:
info = 'waiting for ON'
if time.time() > started + self.switch_on_time: if time.time() > started + self.switch_on_time:
self.log.debug('is switched ON') info = 'is switched ON'
self.value = self.target self.value = self.target
elif self.target < self.value: elif self.target < self.value:
info = 'waiting for OFF'
if time.time() > started + self.switch_off_time: if time.time() > started + self.switch_off_time:
self.log.debug('is switched OFF') info = 'is switched OFF'
self.value = self.target self.value = self.target
if info:
self.log.debug(info)
return info
class MagneticField(Driveable): class MagneticField(Driveable):
@ -101,7 +107,7 @@ class MagneticField(Driveable):
def init(self): def init(self):
self._state = 'idle' self._state = 'idle'
self._heatswitch = self.DISPATCHER.get_device(self.heatswitch) self._heatswitch = self.DISPATCHER.get_module(self.heatswitch)
_thread = threading.Thread(target=self._thread) _thread = threading.Thread(target=self._thread)
_thread.daemon = True _thread.daemon = True
_thread.start() _thread.start()
@ -116,7 +122,8 @@ class MagneticField(Driveable):
# note: we may also return the read-back value from the hw here # note: we may also return the read-back value from the hw here
def read_status(self, maxage=0): def read_status(self, maxage=0):
return status.OK if self._state == 'idle' else status.BUSY return (status.OK, '') if self._state == 'idle' else (
status.BUSY, self._state)
def _thread(self): def _thread(self):
loopdelay = 1 loopdelay = 1
@ -202,9 +209,9 @@ class SampleTemp(Driveable):
ts = time.time() ts = time.time()
if self.value == self.target: if self.value == self.target:
if self.status != status.OK: if self.status != status.OK:
self.status = status.OK self.status = status.OK, ''
else: else:
self.status = status.BUSY self.status = status.BUSY, 'ramping'
step = self.ramp * loopdelay / 60. step = self.ramp * loopdelay / 60.
step = max(min(self.target - self.value, step), -step) step = max(min(self.target - self.value, step), -step)
self.value += step self.value += step
@ -230,14 +237,14 @@ class Label(Readable):
def read_value(self, maxage=0): def read_value(self, maxage=0):
strings = [self.system] strings = [self.system]
dev_ts = self.DISPATCHER.get_device(self.subdev_ts) dev_ts = self.DISPATCHER.get_module(self.subdev_ts)
if dev_ts: if dev_ts:
strings.append('at %.3f %s' % strings.append('at %.3f %s' %
(dev_ts.read_value(), dev_ts.PARAMS['value'].unit)) (dev_ts.read_value(), dev_ts.PARAMS['value'].unit))
else: else:
strings.append('No connection to sample temp!') strings.append('No connection to sample temp!')
dev_mf = self.DISPATCHER.get_device(self.subdev_mf) dev_mf = self.DISPATCHER.get_module(self.subdev_mf)
if dev_mf: if dev_mf:
mf_stat = dev_mf.read_status() mf_stat = dev_mf.read_status()
mf_mode = dev_mf.mode mf_mode = dev_mf.mode
@ -262,8 +269,8 @@ class ValidatorTest(Readable):
'enum': PARAM('enum', validator=enum('boo', 'faar', z=9), readonly=False, default=1), 'enum': PARAM('enum', validator=enum('boo', 'faar', z=9), readonly=False, default=1),
'vector': PARAM('vector of int, float and str', validator=vector(int, float, str), readonly=False, default=(1, 2.3, 'a')), 'vector': PARAM('vector of int, float and str', validator=vector(int, float, str), readonly=False, default=(1, 2.3, 'a')),
'array': PARAM('array: 2..3 time oneof(0,1)', validator=array(oneof(2, 3), oneof(0, 1)), readonly=False, default=[1, 0, 1]), 'array': PARAM('array: 2..3 time oneof(0,1)', validator=array(oneof(2, 3), oneof(0, 1)), readonly=False, default=[1, 0, 1]),
'nonnegative': PARAM('nonnegative', validator=nonnegative(), readonly=False, default=0), 'nonnegative': PARAM('nonnegative', validator=nonnegative, readonly=False, default=0),
'positive': PARAM('positive', validator=positive(), readonly=False, default=1), 'positive': PARAM('positive', validator=positive, readonly=False, default=1),
'intrange': PARAM('intrange', validator=intrange(2, 9), readonly=False, default=4), 'intrange': PARAM('intrange', validator=intrange(2, 9), readonly=False, default=4),
'floatrange': PARAM('floatrange', validator=floatrange(-1, 1), readonly=False, default=0,), 'floatrange': PARAM('floatrange', validator=floatrange(-1, 1), readonly=False, default=0,),
} }

View File

@ -24,7 +24,7 @@
import random import random
from secop.devices.core import Readable, Driveable, PARAM from secop.devices.core import Readable, Driveable, PARAM
from secop.validators import floatrange from secop.validators import floatrange, positive
class LN2(Readable): class LN2(Readable):
@ -65,6 +65,8 @@ class Temp(Driveable):
PARAMS = { PARAMS = {
'sensor': PARAM("Sensor number or calibration id", 'sensor': PARAM("Sensor number or calibration id",
validator=str, readonly=True), validator=str, readonly=True),
'target': PARAM("Target temperature", default=300.0,
validator=positive, readonly=False, unit='K'),
} }
def read_value(self, maxage=0): def read_value(self, maxage=0):

View File

@ -27,6 +27,7 @@ from PyQt4.QtCore import pyqtSignature as qtsig, QObject, pyqtSignal
from secop.gui.util import loadUi from secop.gui.util import loadUi
from secop.gui.nodectrl import NodeCtrl from secop.gui.nodectrl import NodeCtrl
from secop.gui.modulectrl import ModuleCtrl from secop.gui.modulectrl import ModuleCtrl
from secop.gui.paramview import ParameterView
from secop.client.baseclient import Client as SECNode from secop.client.baseclient import Client as SECNode
ITEM_TYPE_NODE = QTreeWidgetItem.UserType + 1 ITEM_TYPE_NODE = QTreeWidgetItem.UserType + 1
@ -91,6 +92,10 @@ class MainWindow(QMainWindow):
self._displayNode(current.text(0)) self._displayNode(current.text(0))
elif current.type() == ITEM_TYPE_MODULE: elif current.type() == ITEM_TYPE_MODULE:
self._displayModule(current.parent().text(0), current.text(0)) self._displayModule(current.parent().text(0), current.text(0))
elif current.type() == ITEM_TYPE_PARAMETER:
self._displayParameter(current.parent().parent().text(0),
current.parent().text(0),
current.text(0))
def _addNode(self, host): def _addNode(self, host):
@ -99,9 +104,10 @@ class MainWindow(QMainWindow):
if ':' in host: if ':' in host:
host, port = host.split(':', 1) host, port = host.split(':', 1)
port = int(port) port = int(port)
node = QSECNode({'connectto':host, 'port':port}, parent=self) node = QSECNode({'connectto': host, 'port': port}, parent=self)
host = '%s:%d' % (host, port) host = '%s:%d' % (host, port)
host = '%s (%s)' % (node.equipment_id, host)
self._nodes[host] = node self._nodes[host] = node
# fill tree # fill tree
@ -127,6 +133,13 @@ class MainWindow(QMainWindow):
def _displayModule(self, node, module): def _displayModule(self, node, module):
self._replaceCtrlWidget(ModuleCtrl(self._nodes[node], module)) self._replaceCtrlWidget(ModuleCtrl(self._nodes[node], module))
def _displayParameter(self, node, module, parameter):
self._replaceCtrlWidget(
ParameterView(
self._nodes[node],
module,
parameter))
def _replaceCtrlWidget(self, new): def _replaceCtrlWidget(self, new):
old = self.splitter.widget(1).layout().takeAt(0) old = self.splitter.widget(1).layout().takeAt(0)
if old: if old:

View File

@ -26,10 +26,12 @@ from PyQt4.QtCore import pyqtSignature as qtsig, Qt, pyqtSignal
from secop.gui.util import loadUi from secop.gui.util import loadUi
class ParameterButtons(QWidget):
setRequested = pyqtSignal(str, str, str) # module, parameter, setpoint
def __init__(self, module, parameter, initval='', parent=None): class ParameterButtons(QWidget):
setRequested = pyqtSignal(str, str, str) # module, parameter, target
def __init__(self, module, parameter, initval='',
readonly=True, parent=None):
super(ParameterButtons, self).__init__(parent) super(ParameterButtons, self).__init__(parent)
loadUi(self, 'parambuttons.ui') loadUi(self, 'parambuttons.ui')
@ -37,6 +39,9 @@ class ParameterButtons(QWidget):
self._parameter = parameter self._parameter = parameter
self.currentLineEdit.setText(str(initval)) self.currentLineEdit.setText(str(initval))
if readonly:
self.setPushButton.setEnabled(False)
self.setLineEdit.setEnabled(False)
def on_setPushButton_clicked(self): def on_setPushButton_clicked(self):
self.setRequested.emit(self._module, self._parameter, self.setRequested.emit(self._module, self._parameter,
@ -44,6 +49,7 @@ class ParameterButtons(QWidget):
class ModuleCtrl(QWidget): class ModuleCtrl(QWidget):
def __init__(self, node, module, parent=None): def __init__(self, node, module, parent=None):
super(ModuleCtrl, self).__init__(parent) super(ModuleCtrl, self).__init__(parent)
loadUi(self, 'modulectrl.ui') loadUi(self, 'modulectrl.ui')
@ -68,8 +74,12 @@ class ModuleCtrl(QWidget):
label = QLabel(param + ':') label = QLabel(param + ':')
label.setFont(font) label.setFont(font)
props = self._node.getProperties(self._module, param)
buttons = ParameterButtons(self._module, param, buttons = ParameterButtons(self._module, param,
initValues[param].value) initValues[param].value,
props['readonly'])
buttons.setRequested.connect(self._node.setParameter) buttons.setRequested.connect(self._node.setParameter)
self.paramGroupBox.layout().addWidget(label, row, 0) self.paramGroupBox.layout().addWidget(label, row, 0)

View File

@ -27,8 +27,11 @@ from PyQt4.QtGui import QWidget, QTextCursor, QFont, QFontMetrics
from PyQt4.QtCore import pyqtSignature as qtsig, Qt from PyQt4.QtCore import pyqtSignature as qtsig, Qt
from secop.gui.util import loadUi from secop.gui.util import loadUi
from secop.protocol.errors import SECOPError
class NodeCtrl(QWidget): class NodeCtrl(QWidget):
def __init__(self, node, parent=None): def __init__(self, node, parent=None):
super(NodeCtrl, self).__init__(parent) super(NodeCtrl, self).__init__(parent)
loadUi(self, 'nodectrl.ui') loadUi(self, 'nodectrl.ui')
@ -49,9 +52,12 @@ class NodeCtrl(QWidget):
self._addLogEntry('<span style="font-weight:bold">Request:</span> ' self._addLogEntry('<span style="font-weight:bold">Request:</span> '
'%s:' % msg, raw=True) '%s:' % msg, raw=True)
msg = msg.split(' ', 2) # msg = msg.split(' ', 2)
reply = self._node.syncCommunicate(*msg) try:
reply = self._node.syncCommunicate(*self._node.decode_message(msg))
self._addLogEntry(reply, newline=True, pretty=True) self._addLogEntry(reply, newline=True, pretty=True)
except SECOPError as e:
self._addLogEntry(e, newline=True, pretty=True, error=True)
@qtsig('') @qtsig('')
def on_clearPushButton_clicked(self): def on_clearPushButton_clicked(self):
@ -64,13 +70,19 @@ class NodeCtrl(QWidget):
self._addLogEntry('=========================') self._addLogEntry('=========================')
self._addLogEntry('', newline=True) self._addLogEntry('', newline=True)
def _addLogEntry(self, msg, newline=False, pretty=False, raw=False): def _addLogEntry(self, msg, newline=False,
pretty=False, raw=False, error=False):
if pretty: if pretty:
msg = pprint.pformat(msg, width=self._getLogWidth()) msg = pprint.pformat(msg, width=self._getLogWidth())
if not raw: if not raw:
msg = '<pre>%s</pre>' % Qt.escape(str(msg)).replace('\n', '<br />') if error:
msg = '<div style="color:#FF0000"><b><pre>%s</pre></b></div>' % Qt.escape(
str(msg)).replace('\n', '<br />')
else:
msg = '<pre>%s</pre>' % Qt.escape(str(msg)
).replace('\n', '<br />')
content = '' content = ''
if self.logTextBrowser.toPlainText(): if self.logTextBrowser.toPlainText():
@ -89,4 +101,3 @@ class NodeCtrl(QWidget):
# due to monospace) # due to monospace)
result = self.logTextBrowser.width() / fontMetrics.width('a') result = self.logTextBrowser.width() / fontMetrics.width('a')
return result return result

81
secop/gui/paramview.py Normal file
View File

@ -0,0 +1,81 @@
# -*- coding: utf-8 -*-
# *****************************************************************************
# Copyright (c) 2015-2017 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 PyQt4.QtGui import QWidget, QLabel, QSizePolicy
from PyQt4.QtCore import pyqtSignature as qtsig, Qt, pyqtSignal
from secop.gui.util import loadUi
from secop.validators import validator_to_str
class ParameterView(QWidget):
def __init__(self, node, module, parameter, parent=None):
super(ParameterView, self).__init__(parent)
loadUi(self, 'paramview.ui')
self._node = node
self._module = module
self._parameter = parameter
self._propWidgets = {} # widget cache do avoid garbage collection
self.paramNameLabel.setText("%s:%s" % (module, parameter))
self._initParamWidgets()
# self._node.newData.connect(self._updateValue)
def _initParamWidgets(self):
# initValues = self._node.queryCache(self._module) #? mix live data?
row = 0
font = self.font()
font.setBold(True)
props = self._node._getDescribingParameterData(
self._module, self._parameter)
for prop in sorted(props):
label = QLabel(prop + ':')
label.setFont(font)
label.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
# make 'display' label
if prop == 'validator':
view = QLabel(validator_to_str(props[prop]))
else:
view = QLabel(str(props[prop]))
view.setFont(self.font())
view.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
view.setWordWrap(True)
self.propertyGroupBox.layout().addWidget(label, row, 0)
self.propertyGroupBox.layout().addWidget(view, row, 1)
self._propWidgets[prop] = (label, view)
row += 1
def _updateValue(self, module, parameter, value):
if module != self._module:
return
self._paramWidgets[parameter][1].currentLineEdit.setText(str(value[0]))

View File

@ -7,7 +7,7 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>730</width> <width>730</width>
<height>31</height> <height>33</height>
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
@ -33,7 +33,7 @@
<item row="0" column="1"> <item row="0" column="1">
<widget class="QLineEdit" name="currentLineEdit"> <widget class="QLineEdit" name="currentLineEdit">
<property name="enabled"> <property name="enabled">
<bool>false</bool> <bool>true</bool>
</property> </property>
<property name="minimumSize"> <property name="minimumSize">
<size> <size>
@ -41,6 +41,9 @@
<height>0</height> <height>0</height>
</size> </size>
</property> </property>
<property name="readOnly">
<bool>true</bool>
</property>
</widget> </widget>
</item> </item>
<item row="0" column="2"> <item row="0" column="2">

99
secop/gui/ui/paramview.ui Normal file
View File

@ -0,0 +1,99 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>Form</class>
<widget class="QWidget" name="Form">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>230</width>
<height>121</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QGridLayout" name="gridLayout">
<item row="3" column="0">
<spacer name="verticalSpacer">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>40</height>
</size>
</property>
</spacer>
</item>
<item row="0" column="0">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QLabel" name="label">
<property name="font">
<font>
<weight>75</weight>
<italic>false</italic>
<bold>true</bold>
</font>
</property>
<property name="text">
<string>Parameter name:</string>
</property>
</widget>
</item>
<item>
<widget class="QLabel" name="paramNameLabel">
<property name="text">
<string>TextLabel</string>
</property>
</widget>
</item>
</layout>
</item>
<item row="2" column="0">
<widget class="QGroupBox" name="propertyGroupBox">
<property name="title">
<string>Parameters:</string>
</property>
<layout class="QGridLayout" name="gridLayout_2">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>6</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>6</number>
</property>
<property name="spacing">
<number>6</number>
</property>
</layout>
</widget>
</item>
<item row="1" column="0">
<spacer name="verticalSpacer_2">
<property name="orientation">
<enum>Qt::Vertical</enum>
</property>
<property name="sizeType">
<enum>QSizePolicy::Fixed</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>20</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -45,6 +45,7 @@ from messages import *
from errors import * from errors import *
from secop.lib.parsing import format_time from secop.lib.parsing import format_time
class Dispatcher(object): class Dispatcher(object):
def __init__(self, logger, options): def __init__(self, logger, options):
@ -205,7 +206,7 @@ class Dispatcher(object):
def get_descriptive_data(self): def get_descriptive_data(self):
# XXX: be lazy and cache this? # XXX: be lazy and cache this?
result = {'modules':{}} result = {'modules': {}}
for modulename in self._export: for modulename in self._export:
module = self.get_module(modulename) module = self.get_module(modulename)
# some of these need rework ! # some of these need rework !
@ -335,9 +336,9 @@ class Dispatcher(object):
res = self._setParamValue(msg.module, 'target', msg.value) res = self._setParamValue(msg.module, 'target', msg.value)
res.parameter = 'target' res.parameter = 'target'
# self.broadcast_event(res) # self.broadcast_event(res)
if conn in self._active_connections: # if conn in self._active_connections:
return None # already send to myself # return None # already send to myself
return res # send reply to inactive conns return res
def handle_Command(self, conn, msg): def handle_Command(self, conn, msg):
# notify all by sending CommandReply # notify all by sending CommandReply

View File

@ -27,7 +27,7 @@
from secop.protocol.encoding import MessageEncoder from secop.protocol.encoding import MessageEncoder
from secop.protocol.messages import * from secop.protocol.messages import *
from secop.protocol.errors import ProtocollError from secop.protocol.errors import ProtocolError
import ast import ast
import re import re
@ -389,7 +389,7 @@ class DemoEncoder_MZ(MessageEncoder):
# errors # errors
ErrorReply: lambda msg: "", ErrorReply: lambda msg: "",
InternalError: lambda msg: "", InternalError: lambda msg: "",
ProtocollError: lambda msg: "", ProtocolError: lambda msg: "",
CommandFailedError: lambda msg: "error CommandError %s:%s %s" % (msg.device, msg.param, msg.error), CommandFailedError: lambda msg: "error CommandError %s:%s %s" % (msg.device, msg.param, msg.error),
NoSuchCommandError: lambda msg: "error NoSuchCommand %s:%s" % (msg.device, msg.param, msg.error), NoSuchCommandError: lambda msg: "error NoSuchCommand %s:%s" % (msg.device, msg.param, msg.error),
NoSuchDeviceError: lambda msg: "error NoSuchModule %s" % msg.device, NoSuchDeviceError: lambda msg: "error NoSuchModule %s" % msg.device,

View File

@ -28,7 +28,7 @@
from secop.lib.parsing import format_time from secop.lib.parsing import format_time
from secop.protocol.encoding import MessageEncoder from secop.protocol.encoding import MessageEncoder
from secop.protocol.messages import * from secop.protocol.messages import *
from secop.protocol.errors import ProtocollError #from secop.protocol.errors import ProtocolError
import ast import ast
import re import re
@ -71,7 +71,7 @@ HELPREQUEST = 'help' # literal
HELPREPLY = 'helping' # +line number +json_text HELPREPLY = 'helping' # +line number +json_text
ERRORCLASSES = ['NoSuchDevice', 'NoSuchParameter', 'NoSuchCommand', ERRORCLASSES = ['NoSuchDevice', 'NoSuchParameter', 'NoSuchCommand',
'CommandFailed', 'ReadOnly', 'BadValue', 'CommunicationFailed', 'CommandFailed', 'ReadOnly', 'BadValue', 'CommunicationFailed',
'IsBusy', 'IsError', 'SyntaxError', 'InternalError', 'IsBusy', 'IsError', 'ProtocolError', 'InternalError',
'CommandRunning', 'Disabled', ] 'CommandRunning', 'Disabled', ]
# note: above strings need to be unique in the sense, that none is/or # note: above strings need to be unique in the sense, that none is/or
# starts with another # starts with another
@ -83,15 +83,18 @@ def encode_cmd_result(msgobj):
q['t'] = format_time(q['t']) q['t'] = format_time(q['t'])
return msgobj.result, q return msgobj.result, q
def encode_value_data(vobj): def encode_value_data(vobj):
q = vobj.qualifiers.copy() q = vobj.qualifiers.copy()
if 't' in q: if 't' in q:
q['t'] = format_time(q['t']) q['t'] = format_time(q['t'])
return vobj.value, q return vobj.value, q
def encode_error_msg(emsg): def encode_error_msg(emsg):
# note: result is JSON-ified.... # note: result is JSON-ified....
return [emsg.origin, dict( (k,getattr(emsg, k)) for k in emsg.ARGS if k != 'origin')] return [emsg.origin, dict((k, getattr(emsg, k))
for k in emsg.ARGS if k != 'origin')]
class DemoEncoder(MessageEncoder): class DemoEncoder(MessageEncoder):
@ -163,6 +166,14 @@ class DemoEncoder(MessageEncoder):
ENABLEEVENTSREQUEST, DISABLEEVENTSREQUEST) ENABLEEVENTSREQUEST, DISABLEEVENTSREQUEST)
return '\n'.join('%s %d %s' % (HELPREPLY, i + 1, l.strip()) return '\n'.join('%s %d %s' % (HELPREPLY, i + 1, l.strip())
for i, l in enumerate(text.split('\n')[:-1])) for i, l in enumerate(text.split('\n')[:-1]))
if isinstance(msg, HeartbeatRequest):
if msg.nonce:
return 'ping %s' % msg.nonce
return 'ping'
if isinstance(msg, HeartbeatReply):
if msg.nonce:
return 'pong %s' % msg.nonce
return 'pong'
for msgcls, parts in self.ENCODEMAP.items(): for msgcls, parts in self.ENCODEMAP.items():
if isinstance(msg, msgcls): if isinstance(msg, msgcls):
# resolve lambdas # resolve lambdas
@ -183,12 +194,12 @@ class DemoEncoder(MessageEncoder):
return IdentifyReply(version_string=encoded) return IdentifyReply(version_string=encoded)
return HelpMessage() return HelpMessage()
return ErrorMessage(errorclass='SyntaxError', # return ErrorMessage(errorclass='Protocol',
errorinfo='Regex did not match!', # errorinfo='Regex did not match!',
is_request=True) # is_request=True)
msgtype, msgspec, data = match.groups() msgtype, msgspec, data = match.groups()
if msgspec is None and data: if msgspec is None and data:
return ErrorMessage(errorclass='InternalError', return ErrorMessage(errorclass='Internal',
errorinfo='Regex matched json, but not spec!', errorinfo='Regex matched json, but not spec!',
is_request=True, is_request=True,
origin=encoded) origin=encoded)
@ -206,10 +217,10 @@ class DemoEncoder(MessageEncoder):
errorinfo=[repr(err), str(encoded)], errorinfo=[repr(err), str(encoded)],
origin=encoded) origin=encoded)
msg = self.DECODEMAP[msgtype](msgspec, data) msg = self.DECODEMAP[msgtype](msgspec, data)
msg.setvalue("origin",encoded) msg.setvalue("origin", encoded)
return msg return msg
return ErrorMessage( return ErrorMessage(
errorclass='SyntaxError', errorclass='Protocol',
errorinfo='%r: No Such Messagetype defined!' % errorinfo='%r: No Such Messagetype defined!' %
encoded, encoded,
is_request=True, is_request=True,

View File

@ -30,12 +30,26 @@ class SECOPError(RuntimeError):
for k, v in kwds.items(): for k, v in kwds.items():
setattr(self, k, v) setattr(self, k, v)
def __repr__(self):
args = ', '.join(map(repr, self.args))
kwds = ', '.join(['%s=%r' % i for i in 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): class InternalError(SECOPError):
pass pass
class ProtocollError(SECOPError): class ProtocolError(SECOPError):
pass pass
@ -56,6 +70,10 @@ class ReadonlyError(SECOPError):
pass pass
class BadValueError(SECOPError):
pass
class CommandFailedError(SECOPError): class CommandFailedError(SECOPError):
pass pass
@ -64,6 +82,18 @@ class InvalidParamValueError(SECOPError):
pass pass
EXCEPTIONS = dict(
Internal=InternalError,
Protocol=ProtocolError,
NoSuchModule=NoSuchModuleError,
NoSuchParam=NoSuchParamError,
NoSuchCommand=NoSuchCommandError,
BadValue=BadValueError,
Readonly=ReadonlyError,
CommandFailed=CommandFailedError,
InvalidParam=InvalidParamValueError,
)
if __name__ == '__main__': if __name__ == '__main__':
print("Minimal testing of errors....") print("Minimal testing of errors....")

View File

@ -69,11 +69,12 @@ class Value(object):
devspec = '%s:%s()' % (devspec, self.command) devspec = '%s:%s()' % (devspec, self.command)
return '%s:Value(%s)' % (devspec, ', '.join( return '%s:Value(%s)' % (devspec, ', '.join(
[repr(self.value)] + [repr(self.value)] +
['%s=%s' % (k, format_time(v) if k=="timestamp" else repr(v)) for k, v in self.qualifiers.items()])) ['%s=%s' % (k, format_time(v) if k == "timestamp" else repr(v)) for k, v in self.qualifiers.items()]))
class Request(Message): class Request(Message):
is_request = True is_request = True
def get_reply(self): def get_reply(self):
"""returns a Reply object prefilled with the attributes from this request.""" """returns a Reply object prefilled with the attributes from this request."""
m = Message() m = Message()
@ -190,6 +191,4 @@ class ErrorMessage(Message):
class HelpMessage(Request): class HelpMessage(Request):
is_reply = True #!sic! is_reply = True # !sic!

View File

@ -30,15 +30,14 @@
# if a validator does a mapping, it normally maps to the external representation (used for print/log/protocol/...) # if a validator does a mapping, it normally maps to the external representation (used for print/log/protocol/...)
# to get the internal representation (for the code), call method convert # to get the internal representation (for the code), call method convert
from errors import ProgrammingError
class ProgrammingError(Exception):
pass
class Validator(object): class Validator(object):
# list of tuples: (name, converter) # list of tuples: (name, converter)
params = [] params = []
valuetype = float valuetype = float
argstr = ''
def __init__(self, *args, **kwds): def __init__(self, *args, **kwds):
plist = self.params[:] plist = self.params[:]
@ -49,7 +48,7 @@ class Validator(object):
for pval in args: for pval in args:
pname, pconv = plist.pop(0) pname, pconv = plist.pop(0)
if pname in kwds: if pname in kwds:
raise ProgrammingError('%s: positional parameter %s als given ' raise ProgrammingError('%s: positional parameter %s is given '
'as keyword!' % ( 'as keyword!' % (
self.__class__.__name__, self.__class__.__name__,
pname)) pname))
@ -67,18 +66,23 @@ class Validator(object):
raise ProgrammingError('%s got unknown arguments: %s' % ( raise ProgrammingError('%s got unknown arguments: %s' % (
self.__class__.__name__, self.__class__.__name__,
', '.join(list(kwds.keys())))) ', '.join(list(kwds.keys()))))
params = []
def __repr__(self): for pn, pt in self.params:
params = ['%s=%r' % (pn[0], self.__dict__[pn[0]]) pv = getattr(self, pn)
for pn in self.params] if callable(pv):
return ('%s(%s)' % (self.__class__.__name__, ', '.join(params))) params.append('%s=%s' % (pn, validator_to_str(pv)))
else:
params.append('%s=%r' % (pn, pv))
self.argstr = ', '.join(params)
def __call__(self, value): def __call__(self, value):
return self.check(self.valuetype(value)) return self.check(self.valuetype(value))
def convert(self, value): def __repr__(self):
# transforms the 'internal' representation into the 'external' return self.to_string()
return self.valuetype(value)
def to_string(self):
return ('%s(%s)' % (self.__class__.__name__, self.argstr))
class floatrange(Validator): class floatrange(Validator):
@ -102,22 +106,6 @@ class intrange(Validator):
(value, self.lower, self.upper)) (value, self.lower, self.upper))
class positive(Validator):
def check(self, value):
if value > 0:
return value
raise ValueError('Value %r must be > 0!' % value)
class nonnegative(Validator):
def check(self, value):
if value >= 0:
return value
raise ValueError('Value %r must be >= 0!' % value)
class array(Validator): class array(Validator):
"""integral amount of data-elements which are described by the SAME validator """integral amount of data-elements which are described by the SAME validator
@ -129,12 +117,15 @@ class array(Validator):
def check(self, values): def check(self, values):
requested_size = len(values) requested_size = len(values)
if callable(self.size):
try: try:
allowed_size = self.size(requested_size) allowed_size = self.size(requested_size)
except ValueError as e: except ValueError as e:
raise ValueError( raise ValueError(
'illegal number of elements %d, need %r: (%s)' % 'illegal number of elements %d, need %r: (%s)' %
(requested_size, self.size, e)) (requested_size, self.size, e))
else:
allowed_size = self.size
if requested_size != allowed_size: if requested_size != allowed_size:
raise ValueError( raise ValueError(
'need %d elements (got %d)' % 'need %d elements (got %d)' %
@ -152,8 +143,9 @@ class array(Validator):
# more complicated validator may not be able to use validator base class # more complicated validator may not be able to use validator base class
class vector(object): class vector(Validator):
"""fixed length, eache element has its own validator""" """fixed length, eache element has its own validator"""
valuetype = tuple
def __init__(self, *args): def __init__(self, *args):
self.validators = args self.validators = args
@ -165,33 +157,30 @@ class vector(object):
len(self.validators), len(args)) len(self.validators), len(args))
return tuple(v(e) for v, e in zip(self.validators, args)) return tuple(v(e) for v, e in zip(self.validators, args))
def __repr__(self):
return ('%s(%s)' % (self.__class__.__name__, self.argstr))
# XXX: fixme!
class record(object): class record(Validator):
"""fixed length, eache element has its own name and validator""" """fixed length, eache element has its own name and validator"""
def __init__(self, **kwds): def __init__(self, **kwds):
self.validators = args self.validators = kwds
self.argstr = ', '.join([validator_to_str(e) for e in kwds.items()]) self.argstr = ', '.join(
['%s=%s' % (e[0], validator_to_str(e[1])) for e in kwds.items()])
def __call__(self, arg): def __call__(self, **args):
if len(args) != len(self.validators): if len(args) != len(self.validators):
raise ValueError('Vector: need exactly %d elementes (got %d)' % raise ValueError('Vector: need exactly %d elementes (got %d)' %
len(self.validators), len(args)) len(self.validators), len(args))
return tuple(v(e) for v, e in zip(self.validators, args)) return tuple(v(e) for v, e in zip(self.validators, args))
def __repr__(self):
return ('%s(%s)' % (self.__class__.__name__, self.argstr))
class oneof(Validator):
class oneof(object):
"""needs to comply with one of the given validators/values""" """needs to comply with one of the given validators/values"""
def __init__(self, *args): def __init__(self, *args):
self.oneof = args self.oneof = args
self.argstr = ', '.join([validator_to_str(e) for e in args]) self.argstr = ', '.join(
[validator_to_str(e) if callable(e) else repr(e) for e in args])
def __call__(self, arg): def __call__(self, arg):
for v in self.oneof: for v in self.oneof:
@ -206,11 +195,8 @@ class oneof(object):
return v return v
raise ValueError('Oneof: %r should be one of: %s' % (arg, self.argstr)) raise ValueError('Oneof: %r should be one of: %s' % (arg, self.argstr))
def __repr__(self):
return ('%s(%s)' % (self.__class__.__name__, self.argstr))
class enum(Validator):
class enum(object):
def __init__(self, *args, **kwds): def __init__(self, *args, **kwds):
self.mapping = {} self.mapping = {}
@ -226,8 +212,11 @@ class enum(object):
self.mapping[args.pop(0)] = i self.mapping[args.pop(0)] = i
# generate reverse mapping too for use by protocol # generate reverse mapping too for use by protocol
self.revmapping = {} self.revmapping = {}
for k, v in self.mapping.items(): params = []
for k, v in sorted(self.mapping.items(), key=lambda x: x[1]):
self.revmapping[v] = k self.revmapping[v] = k
params.append('%s=%r' % (k, v))
self.argstr = ', '.join(params)
def __call__(self, obj): def __call__(self, obj):
try: try:
@ -238,22 +227,68 @@ class enum(object):
return obj return obj
if obj in self.revmapping: if obj in self.revmapping:
return self.revmapping[obj] return self.revmapping[obj]
raise ValueError("%r should be one of %r" % raise ValueError("%r should be one of %s" %
(obj, list(self.mapping.keys()))) (obj, ', '.join(map(repr, self.mapping.keys()))))
def __repr__(self):
params = ['%s=%r' % (mname, mval)
for mname, mval in self.mapping.items()]
return ('%s(%s)' % (self.__class__.__name__, ', '.join(params)))
def convert(self, arg): def convert(self, arg):
return self.mapping.get(arg, arg) return self.mapping.get(arg, arg)
# Validators without parameters:
def positive(value=Ellipsis):
if value != Ellipsis:
if value > 0:
return value
raise ValueError('Value %r must be > 0!' % value)
return -1e-38 # small number > 0
positive.__repr__ = lambda x: validator_to_str(x)
def nonnegative(value=Ellipsis):
if value != Ellipsis:
if value >= 0:
return value
raise ValueError('Value %r must be >= 0!' % value)
return 0.0
nonnegative.__repr__ = lambda x: validator_to_str(x)
# helpers
def validator_to_str(validator): def validator_to_str(validator):
return str(validator) if not isinstance(validator, type) \ if isinstance(validator, Validator):
else validator.__name__ return validator.to_string()
if hasattr(validator, 'func_name'):
return getattr(validator, 'func_name')
for s in 'int str float'.split(' '):
t = eval(s)
if validator == t or isinstance(validator, t):
return s
print "##########", type(validator), repr(validator)
# XXX: better use a mapping here!
def validator_from_str(validator_str): def validator_from_str(validator_str):
return eval(validator_str) return eval(validator_str)
if __name__ == '__main__':
print "minimal testing: validators"
for val, good, bad in [(floatrange(3.09, 5.47), 4.13, 9.27),
(intrange(3, 5), 4, 8),
(array(size=3, datatype=int), (1, 2, 3), (1, 2, 3, 4)),
(vector(int, int), (12, 6), (1.23, 'X')),
(oneof('a', 'b', 'c', 1), 'b', 'x'),
#(record(a=int, b=float), dict(a=2,b=3.97), dict(c=9,d='X')),
(positive, 2, 0),
(nonnegative, 0, -1),
(enum(a=1, b=20), 1, 12),
]:
print validator_to_str(val), repr(validator_from_str(validator_to_str(val)))
print val(good), 'OK'
try:
val(bad)
print "FAIL"
raise ProgrammingError
except Exception as e:
print bad, e, 'OK'
print