replace validators with datatypes

Change-Id: I446c4e14e24afa3f65e79c8b6e07eec3271532b0
This commit is contained in:
Enrico Faulhaber 2017-07-03 15:15:46 +02:00
parent a87e568b55
commit bc3253a01a
11 changed files with 233 additions and 438 deletions

View File

@ -30,8 +30,8 @@ import Queue
import mlzlog import mlzlog
from secop.validators import validator_from_str from secop.datatypes import get_datatype
from secop.lib import mkthread from secop.lib import mkthread, formatException
from secop.lib.parsing import parse_time, format_time 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
@ -271,6 +271,7 @@ class Client(object):
except ValueError: except ValueError:
# keep as string # keep as string
data = json_data data = json_data
# print formatException()
return msgtype, spec, data return msgtype, spec, data
def _handle_event(self, spec, data): def _handle_event(self, spec, data):
@ -310,9 +311,9 @@ class Client(object):
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():
validator = validator_from_str(parameterData['validator']) datatype = get_datatype(parameterData['datatype'])
self.describing_data['modules'][module]['parameters'] \ self.describing_data['modules'][module]['parameters'] \
[parameter]['validator'] = validator [parameter]['datatype'] = datatype
def register_callback(self, module, parameter, cb): def register_callback(self, module, parameter, cb):
self.log.debug('registering callback %r for %s:%s' % self.log.debug('registering callback %r for %s:%s' %
@ -440,10 +441,10 @@ class Client(object):
return self.communicate('read', '%s:%s' % (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, datatype = self._getDescribingParameterData(module,
parameter)['validator'] parameter)['datatype']
value = validator(value) value = datatype.export(datatype.validate(value))
self.communicate('change', '%s:%s' % (module, parameter), value) self.communicate('change', '%s:%s' % (module, parameter), value)
@property @property

View File

@ -21,14 +21,6 @@
# ***************************************************************************** # *****************************************************************************
"""Define validated data types.""" """Define validated data types."""
# a Validator returns a validated object or raises an ValueError
# also validators should have a __repr__ returning a 'python' string
# which recreates them
# if a validator does a mapping, it normally maps to the
# internal representation with method :meth:`validate`
# to get the external representation (aöso for logging),
# call method :meth:`export`
from .errors import ProgrammingError from .errors import ProgrammingError
@ -56,10 +48,10 @@ class FloatRange(DataType):
"""Restricted float type""" """Restricted float type"""
def __init__(self, min=None, max=None): def __init__(self, min=None, max=None):
self.min = float('-Inf') if min is None else float(min) self.min = None if min is None else float(min)
self.max = float('+Inf') if max is None else float(max) self.max = None if max is None else float(max)
# note: as we may compare to Inf all comparisons would be false # note: as we may compare to Inf all comparisons would be false
if self.min <= self.max: if (self.min or float('-inf')) <= (self.max or float('+inf')):
self.as_json = ['double', min, max] self.as_json = ['double', min, max]
else: else:
raise ValueError('Max must be larger then min!') raise ValueError('Max must be larger then min!')
@ -67,15 +59,25 @@ class FloatRange(DataType):
def validate(self, value): def validate(self, value):
try: try:
value = float(value) value = float(value)
if self.min <= value <= self.max:
return value
raise ValueError('%r should be an float between %.3f and %.3f' %
(value, self.min, self.max))
except: except:
raise ValueError('Can not validate %r to float' % value) raise ValueError('Can not validate %r to float' % value)
if self.min is not None and value < self.min:
raise ValueError('%r should not be less then %s' % (value, self.min))
if self.max is not None and value > self.max:
raise ValueError('%r should not be greater than %s' % (value, self.max))
if None in (self.min, self.max):
return value
if self.min <= value <= self.max:
return value
raise ValueError('%r should be an float between %.3f and %.3f' %
(value, self.min, self.max))
def __repr__(self): def __repr__(self):
return "FloatRange(%f, %f)" % (self.min, self.max) if self.max != None:
return "FloatRange(%r, %r)" % (float('-inf') if self.min is None else self.min, self.max)
if self.min != None:
return "FloatRange(%r)" % self.min
return "FloatRange()"
def export(self, value): def export(self, value):
"""returns a python object fit for serialisation""" """returns a python object fit for serialisation"""
@ -105,7 +107,11 @@ class IntRange(DataType):
raise ValueError('Can not validate %r to int' % value) raise ValueError('Can not validate %r to int' % value)
def __repr__(self): def __repr__(self):
return "IntRange(%d, %d)" % (self.min, self.max) if self.max is not None:
return "IntRange(%d, %d)" % (self.min, self.max)
if self.min is not None:
return "IntRange(%d)" % self.min
return "IntRange(%d)" % self.min
def export(self, value): def export(self, value):
"""returns a python object fit for serialisation""" """returns a python object fit for serialisation"""
@ -119,7 +125,8 @@ class EnumType(DataType):
self.entries = {} self.entries = {}
num = 0 num = 0
for arg in args: for arg in args:
if type(args) != str: if type(arg) != str:
print arg, type(arg)
raise ValueError('EnumType entries MUST be strings!') raise ValueError('EnumType entries MUST be strings!')
self.entries[num] = arg self.entries[num] = arg
num += 1 num += 1
@ -138,7 +145,7 @@ class EnumType(DataType):
self.as_json = ['enum', self.reversed.copy()] self.as_json = ['enum', self.reversed.copy()]
def __repr__(self): def __repr__(self):
return "EnumType(%s)" % ', '.join(['%r=%d' % (v,k) for k,v in self.entries.items()]) return "EnumType(%s)" % ', '.join(['%s=%d' % (v,k) for k,v in self.entries.items()])
def export(self, value): def export(self, value):
"""returns a python object fit for serialisation""" """returns a python object fit for serialisation"""
@ -169,8 +176,10 @@ class BLOBType(DataType):
raise ValueError('maxsize must be bigger than minsize!') raise ValueError('maxsize must be bigger than minsize!')
def __repr__(self): def __repr__(self):
if self.minsize or self.maxsize: if self.maxsize:
return 'BLOB(%d, %s)' % (self.minsize, self.maxsize) return 'BLOB(%s, %s)' % (str(self.minsize), str(self.maxsize))
if self.minsize:
return 'BLOB(%d)' % self.minsize
return 'BLOB()' return 'BLOB()'
def validate(self, value): def validate(self, value):
@ -203,7 +212,11 @@ class StringType(DataType):
raise ValueError('maxsize must be bigger than minsize!') raise ValueError('maxsize must be bigger than minsize!')
def __repr__(self): def __repr__(self):
return 'StringType(%d, %s)' % (self.minsize, self.maxsize) if self.maxsize:
return 'StringType(%s, %s)' % (str(self.minsize), str(self.maxsize))
if self.minsize:
return 'StringType(%d)' % str(self.minsize)
return 'StringType()'
def validate(self, value): def validate(self, value):
"""return the validated (internal) value or raise""" """return the validated (internal) value or raise"""
@ -223,6 +236,7 @@ class StringType(DataType):
"""returns a python object fit for serialisation""" """returns a python object fit for serialisation"""
return '%s' % value return '%s' % value
# Bool is a special enum # Bool is a special enum
class BoolType(DataType): class BoolType(DataType):
as_json = ['bool'] as_json = ['bool']
@ -265,6 +279,9 @@ class ArrayOf(DataType):
if self.minsize is not None and self.maxsize is not None and self.minsize > self.maxsize: if self.minsize is not None and self.maxsize is not None and self.minsize > self.maxsize:
raise ValueError('Maximum size must be >= Minimum size') raise ValueError('Maximum size must be >= Minimum size')
def __repr__(self):
return 'ArrayOf(%s, %s, %s)' % (repr(self.subtype), self.minsize, self.maxsize)
def validate(self, value): def validate(self, value):
"""validate a external representation to an internal one""" """validate a external representation to an internal one"""
if isinstance(value, (tuple, list)): if isinstance(value, (tuple, list)):
@ -292,6 +309,9 @@ class TupleOf(DataType):
self.subtypes = subtypes self.subtypes = subtypes
self.as_json = ['tuple', [subtype.as_json for subtype in subtypes]] self.as_json = ['tuple', [subtype.as_json for subtype in subtypes]]
def __repr__(self):
return 'TupleOf(%s)' % ', '.join([repr(st) for st in self.subtypes])
def validate(self, value): def validate(self, value):
"""return the validated value or raise""" """return the validated value or raise"""
# keep the ordering! # keep the ordering!
@ -320,6 +340,9 @@ class StructOf(DataType):
self.named_subtypes = named_subtypes self.named_subtypes = named_subtypes
self.as_json = ['struct', dict((n,s.as_json) for n,s in named_subtypes.items())] self.as_json = ['struct', dict((n,s.as_json) for n,s in named_subtypes.items())]
def __repr__(self):
return 'StructOf(%s)' % ', '.join(['%s=%s'%(n,repr(st)) for n,st in self.named_subtypes.iteritems()])
def validate(self, value): def validate(self, value):
"""return the validated value or raise""" """return the validated value or raise"""
try: try:
@ -358,10 +381,14 @@ DATATYPES = dict(
# probably not needed... # probably not needed...
def export_datatype(datatype): def export_datatype(datatype):
if datatype is None:
return datatype
return datatype.as_json return datatype.as_json
# important for getting the right datatype from formerly jsonified descr. # important for getting the right datatype from formerly jsonified descr.
def get_datatype(json): def get_datatype(json):
if json is None:
return json
if not isinstance(json, list): if not isinstance(json, list):
raise ValueError('Argument must be a properly formatted list!') raise ValueError('Argument must be a properly formatted list!')
if len(json)<1: if len(json)<1:

View File

@ -34,7 +34,8 @@ 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, validator_to_str from secop.datatypes import DataType, EnumType, TupleOf, StringType, FloatRange, export_datatype
EVENT_ONLY_ON_CHANGED_VALUES = False EVENT_ONLY_ON_CHANGED_VALUES = False
@ -49,7 +50,7 @@ class PARAM(object):
def __init__(self, def __init__(self,
description, description,
validator=float, datatype=None,
default=Ellipsis, default=Ellipsis,
unit=None, unit=None,
readonly=True, readonly=True,
@ -59,8 +60,14 @@ class PARAM(object):
# make a copy of a PARAM object # make a copy of a PARAM object
self.__dict__.update(description.__dict__) self.__dict__.update(description.__dict__)
return return
if not isinstance(datatype, DataType):
if issubclass(datatype, DataType):
# goodie: make an instance from a class (forgotten ()???)
datatype = datatype()
else:
raise ValueError('Datatype MUST be from datatypes!')
self.description = description self.description = description
self.validator = validator self.datatype = datatype
self.default = default self.default = default
self.unit = unit self.unit = unit
self.readonly = readonly self.readonly = readonly
@ -79,7 +86,7 @@ class PARAM(object):
res = dict( res = dict(
description=self.description, description=self.description,
readonly=self.readonly, readonly=self.readonly,
validator=validator_to_str(self.validator), datatype=export_datatype(self.datatype),
) )
if self.unit: if self.unit:
res['unit'] = self.unit res['unit'] = self.unit
@ -98,9 +105,9 @@ class CMD(object):
def __init__(self, description, arguments, result): def __init__(self, description, arguments, result):
# descriptive text for humans # descriptive text for humans
self.description = description self.description = description
# list of validators for arguments # list of datatypes for arguments
self.arguments = arguments self.arguments = arguments
# validator for result # datatype for result
self.resulttype = result self.resulttype = result
def __repr__(self): def __repr__(self):
@ -111,8 +118,8 @@ class CMD(object):
# used for serialisation only # used for serialisation only
return dict( return dict(
description=self.description, description=self.description,
arguments=repr(self.arguments), arguments=map(export_datatype, self.arguments),
resulttype=repr(self.resulttype), ) resulttype=export_datatype(self.resulttype), )
# Meta class # Meta class
# warning: MAGIC! # warning: MAGIC!
@ -163,7 +170,7 @@ class DeviceMeta(type):
def wrapped_wfunc(self, value, pname=pname, wfunc=wfunc): def wrapped_wfunc(self, value, pname=pname, wfunc=wfunc):
self.log.debug("wfunc: set %s to %r" % (pname, value)) self.log.debug("wfunc: set %s to %r" % (pname, value))
pobj = self.PARAMS[pname] pobj = self.PARAMS[pname]
value = pobj.validator(value) if pobj.validator else value value = pobj.datatype.validate(value) if pobj.datatype else value
if wfunc: if wfunc:
value = wfunc(self, value) or value value = wfunc(self, value) or value
# XXX: use setattr or direct manipulation # XXX: use setattr or direct manipulation
@ -180,7 +187,7 @@ class DeviceMeta(type):
def setter(self, value, pname=pname): def setter(self, value, pname=pname):
pobj = self.PARAMS[pname] pobj = self.PARAMS[pname]
value = pobj.validator(value) if pobj.validator else value value = pobj.datatype.validate(value) if pobj.datatype else value
pobj.timestamp = time.time() pobj.timestamp = time.time()
if not EVENT_ONLY_ON_CHANGED_VALUES or (value != pobj.value): if not EVENT_ONLY_ON_CHANGED_VALUES or (value != pobj.value):
pobj.value = value pobj.value = value
@ -309,14 +316,14 @@ class Device(object):
self.PARAMS[k] = param_type(self.PARAMS[k]) self.PARAMS[k] = param_type(self.PARAMS[k])
# now 'apply' config: # now 'apply' config:
# pass values through the validators and store as attributes # pass values through the datatypes and store as attributes
for k, v in cfgdict.items(): for k, v in cfgdict.items():
# apply validator, complain if type does not fit # apply datatype, complain if type does not fit
validator = self.PARAMS[k].validator datatype = self.PARAMS[k].datatype
if validator is not None: if datatype is not None:
# only check if validator given # only check if datatype given
try: try:
v = validator(v) v = datatype.validate(v)
except (ValueError, TypeError) 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))
@ -338,19 +345,20 @@ class Readable(Device):
providing the readonly parameter 'value' and 'status' providing the readonly parameter 'value' and 'status'
""" """
PARAMS = { PARAMS = {
'value': PARAM('current value of the device', readonly=True, default=0.), 'value': PARAM('current value of the device', readonly=True, default=0.,
datatype=FloatRange()),
'pollinterval': PARAM('sleeptime between polls', default=5, 'pollinterval': PARAM('sleeptime between polls', default=5,
readonly=False, validator=floatrange(0.1, 120), ), readonly=False, datatype=FloatRange(0.1, 120), ),
'status': PARAM('current status of the device', default=(status.OK, ''), 'status': PARAM('current status of the device', default=(status.OK, ''),
validator=vector( datatype=TupleOf(
enum(**{ EnumType(**{
'IDLE': status.OK, '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
}), str), }), StringType() ),
readonly=True), readonly=True),
} }
@ -378,6 +386,7 @@ class Driveable(Readable):
""" """
PARAMS = { PARAMS = {
'target': PARAM('target value of the device', default=0., readonly=False, 'target': PARAM('target value of the device', default=0., readonly=False,
datatype=FloatRange(),
), ),
} }
# XXX: CMDS ???? auto deriving working well enough? # XXX: CMDS ???? auto deriving working well enough?

View File

@ -27,7 +27,7 @@ import threading
from secop.devices.core import Driveable, CMD, PARAM from secop.devices.core import Driveable, CMD, PARAM
from secop.protocol import status from secop.protocol import status
from secop.validators import floatrange, positive, enum, nonnegative, vector from secop.datatypes import FloatRange, EnumType, TupleOf
from secop.lib import clamp, mkthread from secop.lib import clamp, mkthread
@ -44,81 +44,82 @@ class Cryostat(CryoBase):
""" """
PARAMS = dict( PARAMS = dict(
jitter=PARAM("amount of random noise on readout values", jitter=PARAM("amount of random noise on readout values",
validator=floatrange(0, 1), unit="K", datatype=FloatRange(0, 1), unit="K",
default=0.1, readonly=False, export=False, default=0.1, readonly=False, export=False,
), ),
T_start=PARAM("starting temperature for simulation", T_start=PARAM("starting temperature for simulation",
validator=positive, default=10, datatype=FloatRange(0), default=10,
export=False, export=False,
), ),
looptime=PARAM("timestep for simulation", looptime=PARAM("timestep for simulation",
validator=floatrange(0.01, 10), unit="s", default=1, datatype=FloatRange(0.01, 10), unit="s", default=1,
readonly=False, export=False, readonly=False, export=False,
), ),
ramp=PARAM("ramping speed of the setpoint", ramp=PARAM("ramping speed of the setpoint",
validator=floatrange(0, 1e3), unit="K/min", default=1, datatype=FloatRange(0, 1e3), unit="K/min", default=1,
readonly=False, readonly=False,
), ),
setpoint=PARAM("current setpoint during ramping else target", setpoint=PARAM("current setpoint during ramping else target",
validator=float, default=1, unit='K', datatype=FloatRange(), default=1, unit='K',
), ),
maxpower=PARAM("Maximum heater power", maxpower=PARAM("Maximum heater power",
validator=nonnegative, default=1, unit="W", datatype=FloatRange(0), default=1, unit="W",
readonly=False, readonly=False,
group='heater_settings', group='heater_settings',
), ),
heater=PARAM("current heater setting", heater=PARAM("current heater setting",
validator=floatrange(0, 100), default=0, unit="%", datatype=FloatRange(0, 100), default=0, unit="%",
group='heater_settings', group='heater_settings',
), ),
heaterpower=PARAM("current heater power", heaterpower=PARAM("current heater power",
validator=nonnegative, default=0, unit="W", datatype=FloatRange(0), default=0, unit="W",
group='heater_settings', group='heater_settings',
), ),
target=PARAM("target temperature", target=PARAM("target temperature",
validator=nonnegative, default=0, unit="K", datatype=FloatRange(0), default=0, unit="K",
readonly=False, readonly=False,
), ),
value=PARAM("regulation temperature", value=PARAM("regulation temperature",
validator=nonnegative, default=0, unit="K", datatype=FloatRange(0), default=0, unit="K",
), ),
pid=PARAM("regulation coefficients", pid=PARAM("regulation coefficients",
validator=vector(nonnegative, floatrange( datatype=TupleOf(FloatRange(0), FloatRange(0, 100),
0, 100), floatrange(0, 100)), FloatRange(0, 100)),
default=(40, 10, 2), readonly=False, default=(40, 10, 2), readonly=False,
group='pid', group='pid',
), ),
p=PARAM("regulation coefficient 'p'", p=PARAM("regulation coefficient 'p'",
validator=nonnegative, default=40, unit="%/K", readonly=False, datatype=FloatRange(0), default=40, unit="%/K", readonly=False,
group='pid', group='pid',
), ),
i=PARAM("regulation coefficient 'i'", i=PARAM("regulation coefficient 'i'",
validator=floatrange(0, 100), default=10, readonly=False, datatype=FloatRange(0, 100), default=10, readonly=False,
group='pid', group='pid',
), ),
d=PARAM("regulation coefficient 'd'", d=PARAM("regulation coefficient 'd'",
validator=floatrange(0, 100), default=2, readonly=False, datatype=FloatRange(0, 100), default=2, readonly=False,
group='pid', group='pid',
), ),
mode=PARAM("mode of regulation", mode=PARAM("mode of regulation",
validator=enum('ramp', 'pid', 'openloop'), default='ramp', datatype=EnumType('ramp', 'pid', 'openloop'),
default='ramp',
readonly=False, readonly=False,
), ),
pollinterval=PARAM("polling interval", pollinterval=PARAM("polling interval",
validator=positive, default=5, datatype=FloatRange(0), default=5,
), ),
tolerance=PARAM("temperature range for stability checking", tolerance=PARAM("temperature range for stability checking",
validator=floatrange(0, 100), default=0.1, unit='K', datatype=FloatRange(0, 100), default=0.1, unit='K',
readonly=False, readonly=False,
group='stability', group='stability',
), ),
window=PARAM("time window for stability checking", window=PARAM("time window for stability checking",
validator=floatrange(1, 900), default=30, unit='s', datatype=FloatRange(1, 900), default=30, unit='s',
readonly=False, readonly=False,
group='stability', group='stability',
), ),
timeout=PARAM("max waiting time for stabilisation check", timeout=PARAM("max waiting time for stabilisation check",
validator=floatrange(1, 36000), default=900, unit='s', datatype=FloatRange(1, 36000), default=900, unit='s',
readonly=False, readonly=False,
group='stability', group='stability',
), ),

View File

@ -25,7 +25,7 @@ import random
import threading import threading
from secop.devices.core import Readable, Driveable, PARAM from secop.devices.core import Readable, Driveable, PARAM
from secop.validators import * from secop.datatypes import EnumType, FloatRange, IntRange, ArrayOf, StringType, TupleOf, StructOf, BoolType
from secop.protocol import status from secop.protocol import status
@ -34,18 +34,18 @@ class Switch(Driveable):
""" """
PARAMS = { PARAMS = {
'value': PARAM('current state (on or off)', 'value': PARAM('current state (on or off)',
validator=enum(on=1, off=0), default=0, datatype=EnumType(on=1, off=0), default=0,
), ),
'target': PARAM('wanted state (on or off)', 'target': PARAM('wanted state (on or off)',
validator=enum(on=1, off=0), default=0, datatype=EnumType(on=1, off=0), default=0,
readonly=False, readonly=False,
), ),
'switch_on_time': PARAM('seconds to wait after activating the switch', 'switch_on_time': PARAM('seconds to wait after activating the switch',
validator=floatrange(0, 60), unit='s', datatype=FloatRange(0, 60), unit='s',
default=10, export=False, default=10, export=False,
), ),
'switch_off_time': PARAM('cool-down time in seconds', 'switch_off_time': PARAM('cool-down time in seconds',
validator=floatrange(0, 60), unit='s', datatype=FloatRange(0, 60), unit='s',
default=10, export=False, default=10, export=False,
), ),
} }
@ -99,22 +99,22 @@ class MagneticField(Driveable):
""" """
PARAMS = { PARAMS = {
'value': PARAM('current field in T', 'value': PARAM('current field in T',
unit='T', validator=floatrange(-15, 15), default=0, unit='T', datatype=FloatRange(-15, 15), default=0,
), ),
'target': PARAM('target field in T', 'target': PARAM('target field in T',
unit='T', validator=floatrange(-15, 15), default=0, unit='T', datatype=FloatRange(-15, 15), default=0,
readonly=False, readonly=False,
), ),
'ramp': PARAM('ramping speed', 'ramp': PARAM('ramping speed',
unit='T/min', validator=floatrange(0, 1), default=0.1, unit='T/min', datatype=FloatRange(0, 1), default=0.1,
readonly=False, readonly=False,
), ),
'mode': PARAM('what to do after changing field', 'mode': PARAM('what to do after changing field',
default=1, validator=enum(persistent=1, hold=0), default=1, datatype=EnumType(persistent=1, hold=0),
readonly=False, readonly=False,
), ),
'heatswitch': PARAM('name of heat switch device', 'heatswitch': PARAM('name of heat switch device',
validator=str, export=False, datatype=StringType(), export=False,
), ),
} }
@ -183,10 +183,10 @@ class CoilTemp(Readable):
""" """
PARAMS = { PARAMS = {
'value': PARAM('Coil temperatur', 'value': PARAM('Coil temperatur',
unit='K', validator=float, default=0, unit='K', datatype=FloatRange(), default=0,
), ),
'sensor': PARAM("Sensor number or calibration id", 'sensor': PARAM("Sensor number or calibration id",
validator=str, readonly=True, datatype=StringType(), readonly=True,
), ),
} }
@ -199,13 +199,13 @@ class SampleTemp(Driveable):
""" """
PARAMS = { PARAMS = {
'value': PARAM('Sample temperature', 'value': PARAM('Sample temperature',
unit='K', validator=float, default=10, unit='K', datatype=FloatRange(), default=10,
), ),
'sensor': PARAM("Sensor number or calibration id", 'sensor': PARAM("Sensor number or calibration id",
validator=str, readonly=True, datatype=StringType(), readonly=True,
), ),
'ramp': PARAM('moving speed in K/min', 'ramp': PARAM('moving speed in K/min',
validator=floatrange(0, 100), unit='K/min', default=0.1, datatype=FloatRange(0, 100), unit='K/min', default=0.1,
readonly=False, readonly=False,
), ),
} }
@ -243,16 +243,16 @@ class Label(Readable):
""" """
PARAMS = { PARAMS = {
'system': PARAM("Name of the magnet system", 'system': PARAM("Name of the magnet system",
validator=str, export=False, datatype=StringType, export=False,
), ),
'subdev_mf': PARAM("name of subdevice for magnet status", 'subdev_mf': PARAM("name of subdevice for magnet status",
validator=str, export=False, datatype=StringType, export=False,
), ),
'subdev_ts': PARAM("name of subdevice for sample temp", 'subdev_ts': PARAM("name of subdevice for sample temp",
validator=str, export=False, datatype=StringType, export=False,
), ),
'value': PARAM("final value of label string", 'value': PARAM("final value of label string",
validator=str, datatype=StringType,
), ),
} }
@ -283,25 +283,22 @@ class Label(Readable):
return '; '.join(strings) return '; '.join(strings)
class ValidatorTest(Readable): class DatatypesTest(Readable):
""" """
""" """
PARAMS = { PARAMS = {
'oneof': PARAM('oneof',
validator=oneof(int, 'X', 2.718), readonly=False, default=4.0),
'enum': PARAM('enum', 'enum': PARAM('enum',
validator=enum('boo', 'faar', z=9), readonly=False, default=1), datatype=EnumType('boo', 'faar', z=9), readonly=False, default=1),
'vector': PARAM('vector of int, float and str', 'tupleof': PARAM('tuple of int, float and str',
validator=vector(int, float, str), readonly=False, default=(1, 2.3, 'a')), datatype=TupleOf(IntRange(), FloatRange(), StringType()), readonly=False, default=(1, 2.3, 'a')),
'array': PARAM('array: 2..3 times oneof(0,1)', 'arrayof': PARAM('array: 2..3 times bool',
validator=array(oneof(2, 3), oneof(0, 1)), readonly=False, default=[1, 0, 1]), datatype=ArrayOf(BoolType(), 2, 3), readonly=False, default=[1, 0, 1]),
'nonnegative': PARAM('nonnegative',
validator=nonnegative, readonly=False, default=0),
'positive': PARAM('positive',
validator=positive, readonly=False, default=1),
'intrange': PARAM('intrange', 'intrange': PARAM('intrange',
validator=intrange(2, 9), readonly=False, default=4), datatype=IntRange(2, 9), readonly=False, default=4),
'floatrange': PARAM('floatrange', 'floatrange': PARAM('floatrange',
validator=floatrange(-1, 1), readonly=False, default=0, datatype=FloatRange(-1, 1), readonly=False, default=0,
),
'struct': PARAM('struct(a=str, b=int, c=bool)',
datatype=StructOf(a=StringType(), b=IntRange(), c=BoolType()),
), ),
} }

View File

@ -23,7 +23,7 @@
import random import random
from secop.lib.parsing import format_time from secop.lib.parsing import format_time
from secop.validators import enum, vector, floatrange, validator_to_str from secop.datatypes import EnumType, TupleOf, FloatRange, get_datatype, StringType
from secop.devices.core import Readable, Device, Driveable, PARAM from secop.devices.core import Readable, Device, Driveable, PARAM
from secop.protocol import status from secop.protocol import status
@ -62,14 +62,17 @@ class EpicsReadable(Readable):
"""EpicsDriveable handles a Driveable interfacing to EPICS v4""" """EpicsDriveable handles a Driveable interfacing to EPICS v4"""
# Commmon PARAMS for all EPICS devices # Commmon PARAMS for all EPICS devices
PARAMS = { PARAMS = {
'value': PARAM('EPICS generic value', validator=float, 'value': PARAM('EPICS generic value',
datatype=FloatRange(),
default=300.0,), default=300.0,),
'epics_version': PARAM("EPICS version used, v3 or v4", 'epics_version': PARAM("EPICS version used, v3 or v4",
validator=str,), datatype=EnumType(v3=3, v4=4),),
# 'private' parameters: not remotely accessible # 'private' parameters: not remotely accessible
'value_pv': PARAM('EPICS pv_name of value', validator=str, 'value_pv': PARAM('EPICS pv_name of value',
datatype=StringType(),
default="unset", export=False), default="unset", export=False),
'status_pv': PARAM('EPICS pv_name of status', validator=str, 'status_pv': PARAM('EPICS pv_name of status',
datatype=StringType(),
default="unset", export=False), default="unset", export=False),
} }
@ -119,18 +122,18 @@ class EpicsDriveable(Driveable):
"""EpicsDriveable handles a Driveable interfacing to EPICS v4""" """EpicsDriveable handles a Driveable interfacing to EPICS v4"""
# Commmon PARAMS for all EPICS devices # Commmon PARAMS for all EPICS devices
PARAMS = { PARAMS = {
'target': PARAM('EPICS generic target', validator=float, 'target': PARAM('EPICS generic target', datatype=FloatRange(),
default=300.0, readonly=False), default=300.0, readonly=False),
'value': PARAM('EPICS generic value', validator=float, 'value': PARAM('EPICS generic value', datatype=FloatRange(),
default=300.0,), default=300.0,),
'epics_version': PARAM("EPICS version used, v3 or v4", 'epics_version': PARAM("EPICS version used, v3 or v4",
validator=str,), datatype=StringType(),),
# 'private' parameters: not remotely accessible # 'private' parameters: not remotely accessible
'target_pv': PARAM('EPICS pv_name of target', validator=str, 'target_pv': PARAM('EPICS pv_name of target', datatype=StringType(),
default="unset", export=False), default="unset", export=False),
'value_pv': PARAM('EPICS pv_name of value', validator=str, 'value_pv': PARAM('EPICS pv_name of value', datatype=StringType(),
default="unset", export=False), default="unset", export=False),
'status_pv': PARAM('EPICS pv_name of status', validator=str, 'status_pv': PARAM('EPICS pv_name of status', datatype=StringType(),
default="unset", export=False), default="unset", export=False),
} }
@ -191,15 +194,15 @@ class EpicsDriveable(Driveable):
class EpicsTempCtrl(EpicsDriveable): class EpicsTempCtrl(EpicsDriveable):
PARAMS = { PARAMS = {
# TODO: restrict possible values with oneof validator # TODO: restrict possible values with oneof datatype
'heaterrange': PARAM('Heater range', validator=str, 'heaterrange': PARAM('Heater range', datatype=StringType(),
default='Off', readonly=False,), default='Off', readonly=False,),
'tolerance': PARAM('allowed deviation between value and target', 'tolerance': PARAM('allowed deviation between value and target',
validator=floatrange(1e-6, 1e6), default=0.1, datatype=FloatRange(1e-6, 1e6), default=0.1,
readonly=False,), readonly=False,),
# 'private' parameters: not remotely accessible # 'private' parameters: not remotely accessible
'heaterrange_pv': PARAM('EPICS pv_name of heater range', 'heaterrange_pv': PARAM('EPICS pv_name of heater range',
validator=str, default="unset", export=False,), datatype=StringType(), default="unset", export=False,),
} }
def read_target(self, maxage=0): def read_target(self, maxage=0):

View File

@ -23,8 +23,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, positive from secop.datatypes import FloatRange, StringType
class LN2(Readable): class LN2(Readable):
"""Just a readable. """Just a readable.
@ -45,7 +44,7 @@ class Heater(Driveable):
""" """
PARAMS = { PARAMS = {
'maxheaterpower': PARAM('maximum allowed heater power', 'maxheaterpower': PARAM('maximum allowed heater power',
validator=floatrange(0, 100), unit='W', datatype=FloatRange(0, 100), unit='W',
), ),
} }
@ -64,10 +63,10 @@ class Temp(Driveable):
""" """
PARAMS = { PARAMS = {
'sensor': PARAM("Sensor number or calibration id", 'sensor': PARAM("Sensor number or calibration id",
validator=str, readonly=True, datatype=StringType(8,16), readonly=True,
), ),
'target': PARAM("Target temperature", 'target': PARAM("Target temperature",
default=300.0, validator=positive, readonly=False, unit='K', default=300.0, datatype=FloatRange(0), readonly=False, unit='K',
), ),
} }

View File

@ -25,7 +25,7 @@ from PyQt4.QtGui import QWidget, QLabel, QSizePolicy
from PyQt4.QtCore import pyqtSignature as qtsig, Qt, pyqtSignal from PyQt4.QtCore import pyqtSignature as qtsig, Qt, pyqtSignal
from secop.gui.util import loadUi from secop.gui.util import loadUi
from secop.validators import validator_to_str from secop.datatypes import get_datatype
class ParameterView(QWidget): class ParameterView(QWidget):
@ -59,10 +59,7 @@ class ParameterView(QWidget):
label.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred) label.setSizePolicy(QSizePolicy.Minimum, QSizePolicy.Preferred)
# make 'display' label # make 'display' label
if prop == 'validator': view = QLabel(str(props[prop]))
view = QLabel(validator_to_str(props[prop]))
else:
view = QLabel(str(props[prop]))
view.setFont(self.font()) view.setFont(self.font())
view.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred) view.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Preferred)
view.setWordWrap(True) view.setWordWrap(True)

View File

@ -64,6 +64,73 @@ def mkthread(func, *args, **kwds):
return t return t
import sys
import linecache
import traceback
def formatExtendedFrame(frame):
ret = []
for key, value in frame.f_locals.iteritems():
try:
valstr = repr(value)[:256]
except Exception:
valstr = '<cannot be displayed>'
ret.append(' %-20s = %s\n' % (key, valstr))
ret.append('\n')
return ret
def formatExtendedTraceback(etype, value, tb):
ret = ['Traceback (most recent call last):\n']
while tb is not None:
frame = tb.tb_frame
filename = frame.f_code.co_filename
item = ' File "%s", line %d, in %s\n' % (filename, tb.tb_lineno,
frame.f_code.co_name)
linecache.checkcache(filename)
line = linecache.getline(filename, tb.tb_lineno, frame.f_globals)
if line:
item = item + ' %s\n' % line.strip()
ret.append(item)
if filename not in ('<script>', '<string>'):
ret += formatExtendedFrame(tb.tb_frame)
tb = tb.tb_next
ret += traceback.format_exception_only(etype, value)
return ''.join(ret).rstrip('\n')
def formatExtendedStack(level=1):
f = sys._getframe(level)
ret = ['Stack trace (most recent call last):\n\n']
while f is not None:
lineno = f.f_lineno
co = f.f_code
filename = co.co_filename
name = co.co_name
item = ' File "%s", line %d, in %s\n' % (filename, lineno, name)
linecache.checkcache(filename)
line = linecache.getline(filename, lineno, f.f_globals)
if line:
item = item + ' %s\n' % line.strip()
ret.insert(1, item)
if filename != '<script>':
ret[2:2] = formatExtendedFrame(f)
f = f.f_back
return ''.join(ret).rstrip('\n')
def formatException(cut=0, exc_info=None):
"""Format an exception with traceback, but leave out the first `cut`
number of frames.
"""
if exc_info is None:
typ, val, tb = sys.exc_info()
else:
typ, val, tb = exc_info
res = ['Traceback (most recent call last):\n']
tbres = traceback.format_tb(tb, sys.maxsize)
res += tbres[cut:]
res += traceback.format_exception_only(typ, val)
return ''.join(res)
if __name__ == '__main__': if __name__ == '__main__':
print "minimal testing: lib" print "minimal testing: lib"
d = attrdict(a=1, b=2) d = attrdict(a=1, b=2)

View File

@ -43,6 +43,7 @@ import threading
from messages import * from messages import *
from errors import * from errors import *
from secop.lib.parsing import format_time from secop.lib.parsing import format_time
from secop.lib import formatExtendedStack, formatException
class Dispatcher(object): class Dispatcher(object):
@ -88,6 +89,7 @@ class Dispatcher(object):
errorclass=err.__class__.__name__, errorclass=err.__class__.__name__,
errorinfo=[repr(err), str(msg)]) errorinfo=[repr(err), str(msg)])
except (ValueError, TypeError) as err: except (ValueError, TypeError) as err:
self.log.exception(err)
reply = msg.get_error( reply = msg.get_error(
errorclass='BadValue', errorclass='BadValue',
errorinfo=[repr(err), str(msg)]) errorinfo=[repr(err), str(msg)])
@ -95,7 +97,7 @@ class Dispatcher(object):
self.log.exception(err) self.log.exception(err)
reply = msg.get_error( reply = msg.get_error(
errorclass='InternalError', errorclass='InternalError',
errorinfo=[repr(err), str(msg)]) errorinfo=[formatException(), str(msg), formatExtendedStack()])
else: else:
self.log.debug('Can not handle msg %r' % msg) self.log.debug('Can not handle msg %r' % msg)
reply = self.unhandled(conn, msg) reply = self.unhandled(conn, msg)

View File

@ -1,308 +0,0 @@
# -*- coding: utf-8 -*-
# *****************************************************************************
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 2 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc.,
# 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
#
# Module authors:
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
#
# *****************************************************************************
"""Define validators."""
# a Validator returns a validated object or raises an ValueError
# easy python validators: int(), float(), str()
# also validators should have a __repr__ returning a 'python' string
# which recreates them
# 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
from errors import ProgrammingError
class Validator(object):
# list of tuples: (name, converter)
params = []
valuetype = float
argstr = ''
def __init__(self, *args, **kwds):
plist = self.params[:]
if len(args) > len(plist):
raise ProgrammingError('%s takes %d parameters only (%d given)' % (
self.__class__.__name__, len(plist), len(args)))
for pval in args:
pname, pconv = plist.pop(0)
if pname in kwds:
raise ProgrammingError('%s: positional parameter %s is given '
'as keyword!' %
(self.__class__.__name__, pname))
self.__dict__[pname] = pconv(pval)
for pname, pconv in plist:
if pname in kwds:
pval = kwds.pop(pname)
self.__dict__[pname] = pconv(pval)
else:
raise ProgrammingError('%s: param %s left unspecified!' %
(self.__class__.__name__, pname))
if kwds:
raise ProgrammingError('%s got unknown arguments: %s' % (
self.__class__.__name__, ', '.join(list(kwds.keys()))))
params = []
for pn, pt in self.params:
pv = getattr(self, pn)
if callable(pv):
params.append('%s=%s' % (pn, validator_to_str(pv)))
else:
params.append('%s=%r' % (pn, pv))
self.argstr = ', '.join(params)
def __call__(self, value):
return self.check(self.valuetype(value))
def __repr__(self):
return self.to_string()
def to_string(self):
return ('%s(%s)' % (self.__class__.__name__, self.argstr))
class floatrange(Validator):
params = [('lower', float), ('upper', float)]
def check(self, value):
try:
value = float(value)
if self.lower <= value <= self.upper:
return value
raise ValueError(
'Floatrange: value %r must be a float within %f and %f' %
(value, self.lower, self.upper))
except TypeError:
raise ValueError(
'Floatrange: value %r must be a float within %f and %f' %
(value, self.lower, self.upper))
class intrange(Validator):
params = [('lower', int), ('upper', int)]
valuetype = int
def check(self, value):
if self.lower <= int(value) <= self.upper:
return value
raise ValueError(
'Intrange: value %r must be an integer within %f and %f' %
(value, self.lower, self.upper))
class array(Validator):
"""integral amount of data-elements which are described by the SAME validator
The size of the array can also be described by an validator
"""
valuetype = list
params = [('size', lambda x: x), ('datatype', lambda x: x)]
def check(self, values):
requested_size = len(values)
if callable(self.size):
try:
allowed_size = self.size(requested_size)
except ValueError as e:
raise ValueError('illegal number of elements %d, need %r: (%s)'
% (requested_size, self.size, e))
else:
allowed_size = self.size
if requested_size != allowed_size:
raise ValueError('need %d elements (got %d)' %
(allowed_size, requested_size))
# apply data-type validator to all elements and return
res = []
for idx, el in enumerate(values):
try:
res.append(self.datatype(el))
except ValueError as e:
raise ValueError(
'Array Element %s (=%r) not conforming to %r: (%s)' %
(idx, el, self.datatype, e))
return res
# more complicated validator may not be able to use validator base class
class vector(Validator):
"""fixed length, eache element has its own validator"""
valuetype = tuple
def __init__(self, *args):
self.validators = args
self.argstr = ', '.join([validator_to_str(e) for e in args])
def __call__(self, args):
if type(args) in (str, unicode):
args = eval(args)
if len(args) != len(self.validators):
raise ValueError('Vector: need exactly %d elementes (got %d)' %
len(self.validators), len(args))
res = tuple(v(e) for v, e in zip(self.validators, args))
return res
# XXX: fixme!
class record(Validator):
"""fixed length, eache element has its own name and validator"""
def __init__(self, **kwds):
self.validators = kwds
self.argstr = ', '.join(
['%s=%s' % (e[0], validator_to_str(e[1])) for e in kwds.items()])
def __call__(self, **args):
if len(args) != len(self.validators):
raise ValueError('Vector: need exactly %d elementes (got %d)' %
len(self.validators), len(args))
return tuple(v(e) for v, e in zip(self.validators, args))
class oneof(Validator):
"""needs to comply with one of the given validators/values"""
def __init__(self, *args):
self.oneof = args
self.argstr = ', '.join(
[validator_to_str(e) if callable(e) else repr(e) for e in args])
def __call__(self, arg):
for v in self.oneof:
if callable(v):
try:
if (v == int) and (float(arg) != int(arg)):
continue
return v(arg)
except ValueError:
pass # try next validator
elif v == arg:
return v
raise ValueError('Oneof: %r should be one of: %s' % (arg, self.argstr))
class enum(Validator):
def __init__(self, *args, **kwds):
self.mapping = {}
# use given kwds directly
self.mapping.update(kwds)
# enumerate args
i = -1
args = list(args)
while args:
i += 1
if i in self.mapping:
continue
self.mapping[args.pop(0)] = i
# generate reverse mapping too for use by protocol
self.revmapping = {}
params = []
for k, v in sorted(self.mapping.items(), key=lambda x: x[1]):
self.revmapping[v] = k
params.append('%s=%r' % (k, v))
self.argstr = ', '.join(params)
def __call__(self, obj):
try:
obj = int(obj)
except ValueError:
pass
if obj in self.mapping:
return obj
if obj in self.revmapping:
return self.revmapping[obj]
raise ValueError("%r should be one of %s" %
(obj, ', '.join(map(repr, self.mapping.keys()))))
def convert(self, arg):
return self.mapping.get(arg, arg)
# Validators without parameters:
def positive(value=Ellipsis):
if value != Ellipsis:
if value > 0:
return float(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 float(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):
if isinstance(validator, Validator):
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):
validator_str = validator_str.replace("<type 'str'>", "str")
validator_str = validator_str.replace("<type 'float'>", "float")
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