Bug hunting and polishing
Change-Id: I0f05730dd4e01e926ab0c4870c27ed5754f3ccfd
This commit is contained in:
@ -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)
|
||||||
|
|
||||||
|
10
etc/demo.cfg
10
etc/demo.cfg
@ -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]
|
||||||
|
@ -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])
|
||||||
|
@ -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,
|
||||||
|
@ -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,),
|
||||||
}
|
}
|
||||||
|
@ -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):
|
||||||
|
@ -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:
|
||||||
|
@ -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)
|
||||||
|
@ -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
81
secop/gui/paramview.py
Normal 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]))
|
@ -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
99
secop/gui/ui/paramview.ui
Normal 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>
|
@ -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
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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....")
|
||||||
|
|
||||||
|
@ -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!
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
Reference in New Issue
Block a user