improve and fix errors with parameter limits
- in order to work properly, readonly=True in limit parameters has to be set before creating the write_* method - more explicit: Use e.g. target_max=Limit() - fix an error in the loop over the base classes when creating the check_* method - more concise error message when a limit is violated + fix an error in playground when using persistent parameters Change-Id: Ibd557b55d6c0d9a2612cda4460b16e3c70e1bc9e Reviewed-on: https://forge.frm2.tum.de/review/c/secop/frappy/+/31017 Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de> Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
This commit is contained in:
parent
85166344d2
commit
3b95013b69
@ -31,11 +31,11 @@ from frappy.datatypes import ArrayOf, BLOBType, BoolType, EnumType, \
|
|||||||
from frappy.lib.enum import Enum
|
from frappy.lib.enum import Enum
|
||||||
from frappy.modules import Attached, Communicator, \
|
from frappy.modules import Attached, Communicator, \
|
||||||
Done, Drivable, Feature, Module, Readable, Writable, HasAccessibles
|
Done, Drivable, Feature, Module, Readable, Writable, HasAccessibles
|
||||||
from frappy.params import Command, Parameter
|
from frappy.params import Command, Parameter, Limit
|
||||||
from frappy.properties import Property
|
from frappy.properties import Property
|
||||||
from frappy.proxy import Proxy, SecNode, proxy_class
|
from frappy.proxy import Proxy, SecNode, proxy_class
|
||||||
from frappy.io import HasIO, StringIO, BytesIO, HasIodev # TODO: remove HasIodev (legacy stuff)
|
from frappy.io import HasIO, StringIO, BytesIO, HasIodev # TODO: remove HasIodev (legacy stuff)
|
||||||
from frappy.persistent import PersistentMixin, PersistentParam
|
from frappy.persistent import PersistentMixin, PersistentParam, PersistentLimit
|
||||||
from frappy.rwhandler import ReadHandler, WriteHandler, CommonReadHandler, \
|
from frappy.rwhandler import ReadHandler, WriteHandler, CommonReadHandler, \
|
||||||
CommonWriteHandler, nopoll
|
CommonWriteHandler, nopoll
|
||||||
|
|
||||||
|
@ -34,7 +34,7 @@ from frappy.errors import BadValueError, CommunicationFailedError, ConfigError,
|
|||||||
ProgrammingError, SECoPError, secop_error, RangeError
|
ProgrammingError, SECoPError, secop_error, RangeError
|
||||||
from frappy.lib import formatException, mkthread, UniqueObject
|
from frappy.lib import formatException, mkthread, UniqueObject
|
||||||
from frappy.lib.enum import Enum
|
from frappy.lib.enum import Enum
|
||||||
from frappy.params import Accessible, Command, Parameter
|
from frappy.params import Accessible, Command, Parameter, Limit
|
||||||
from frappy.properties import HasProperties, Property
|
from frappy.properties import HasProperties, Property
|
||||||
from frappy.logging import RemoteLogHandler, HasComlog
|
from frappy.logging import RemoteLogHandler, HasComlog
|
||||||
|
|
||||||
@ -161,7 +161,7 @@ class HasAccessibles(HasProperties):
|
|||||||
# find the base class, where the parameter <limname> is defined first.
|
# find the base class, where the parameter <limname> is defined first.
|
||||||
# we have to check all bases, as they may not be treated yet when
|
# we have to check all bases, as they may not be treated yet when
|
||||||
# not inheriting from HasAccessibles
|
# not inheriting from HasAccessibles
|
||||||
base = next(b for b in reversed(base.__mro__) if limname in b.__dict__)
|
base = next(b for b in reversed(cls.__mro__) if limname in b.__dict__)
|
||||||
if cname not in base.__dict__:
|
if cname not in base.__dict__:
|
||||||
# there is no check method yet at this class
|
# there is no check method yet at this class
|
||||||
# add check function to the class where the limit was defined
|
# add check function to the class where the limit was defined
|
||||||
@ -431,28 +431,19 @@ class Module(HasAccessibles):
|
|||||||
self.valueCallbacks[pname] = []
|
self.valueCallbacks[pname] = []
|
||||||
self.errorCallbacks[pname] = []
|
self.errorCallbacks[pname] = []
|
||||||
|
|
||||||
if not pobj.hasDatatype():
|
if isinstance(pobj, Limit):
|
||||||
head, _, postfix = pname.rpartition('_')
|
basepname = pname.rpartition('_')[0]
|
||||||
if postfix not in ('min', 'max', 'limits'):
|
baseparam = self.parameters.get(basepname)
|
||||||
errors.append(f'{pname} needs a datatype')
|
|
||||||
continue
|
|
||||||
# when datatype is not given, properties are set automagically
|
|
||||||
pobj.setProperty('readonly', False)
|
|
||||||
baseparam = self.parameters.get(head)
|
|
||||||
if not baseparam:
|
if not baseparam:
|
||||||
errors.append(f'parameter {pname!r} is given, but not {head!r}')
|
errors.append(f'limit {pname!r} is given, but not {basepname!r}')
|
||||||
continue
|
continue
|
||||||
dt = baseparam.datatype
|
if baseparam.datatype is None:
|
||||||
if dt is None:
|
|
||||||
continue # an error will be reported on baseparam
|
continue # an error will be reported on baseparam
|
||||||
if postfix == 'limits':
|
pobj.set_datatype(baseparam.datatype)
|
||||||
pobj.setProperty('datatype', TupleOf(dt, dt))
|
|
||||||
pobj.setProperty('default', (dt.min, dt.max))
|
if not pobj.hasDatatype():
|
||||||
else:
|
errors.append(f'{pname} needs a datatype')
|
||||||
pobj.setProperty('datatype', dt)
|
continue
|
||||||
pobj.setProperty('default', getattr(dt, postfix))
|
|
||||||
if not pobj.description:
|
|
||||||
pobj.setProperty('description', f'limit for {pname}')
|
|
||||||
|
|
||||||
if pobj.value is None:
|
if pobj.value is None:
|
||||||
if pobj.needscfg:
|
if pobj.needscfg:
|
||||||
@ -805,11 +796,11 @@ class Module(HasAccessibles):
|
|||||||
raise ValueError('remote handler not found')
|
raise ValueError('remote handler not found')
|
||||||
self.remoteLogHandler.set_conn_level(self, conn, level)
|
self.remoteLogHandler.set_conn_level(self, conn, level)
|
||||||
|
|
||||||
def checkLimits(self, value, parametername='target'):
|
def checkLimits(self, value, pname='target'):
|
||||||
"""check for limits
|
"""check for limits
|
||||||
|
|
||||||
:param value: the value to be checked for <parametername>_min <= value <= <parametername>_max
|
:param value: the value to be checked for <pname>_min <= value <= <pname>_max
|
||||||
:param parametername: parameter name, default is 'target'
|
:param pname: parameter name, default is 'target'
|
||||||
|
|
||||||
raises RangeError in case the value is not valid
|
raises RangeError in case the value is not valid
|
||||||
|
|
||||||
@ -818,14 +809,20 @@ class Module(HasAccessibles):
|
|||||||
when no automatic super call is desired.
|
when no automatic super call is desired.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
min_, max_ = getattr(self, parametername + '_limits')
|
min_, max_ = getattr(self, pname + '_limits')
|
||||||
|
if not min_ <= value <= max_:
|
||||||
|
raise RangeError(f'{pname} outside {pname}_limits')
|
||||||
|
return
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
min_ = getattr(self, parametername + '_min', float('-inf'))
|
pass
|
||||||
max_ = getattr(self, parametername + '_max', float('inf'))
|
min_ = getattr(self, pname + '_min', float('-inf'))
|
||||||
if not min_ <= value <= max_:
|
max_ = getattr(self, pname + '_max', float('inf'))
|
||||||
if min_ > max_:
|
if min_ > max_:
|
||||||
raise RangeError(f'invalid limits: [{min_:g}, {max_:g}]')
|
raise RangeError(f'invalid limits: {pname}_min > {pname}_max')
|
||||||
raise RangeError(f'limits violation: {value:g} outside [{min_:g}, {max_:g}]')
|
if value < min_:
|
||||||
|
raise RangeError(f'{pname} below {pname}_min')
|
||||||
|
if value > max_:
|
||||||
|
raise RangeError(f'{pname} above {pname}_max')
|
||||||
|
|
||||||
|
|
||||||
class Readable(Module):
|
class Readable(Module):
|
||||||
|
@ -514,6 +514,35 @@ class Command(Accessible):
|
|||||||
return result[:-1] + f', {self.func!r})' if self.func else result
|
return result[:-1] + f', {self.func!r})' if self.func else result
|
||||||
|
|
||||||
|
|
||||||
|
class Limit(Parameter):
|
||||||
|
"""a special limit parameter"""
|
||||||
|
POSTFIXES = {'min', 'max', 'limits'} # allowed postfixes
|
||||||
|
|
||||||
|
def __set_name__(self, owner, name):
|
||||||
|
super().__set_name__(owner, name)
|
||||||
|
head, _, postfix = name.rpartition('_')
|
||||||
|
if postfix not in self.POSTFIXES:
|
||||||
|
raise ProgrammingError(f'Limit name must end with one of {self.POSTFIXES}')
|
||||||
|
if 'readonly' not in self.propertyValues:
|
||||||
|
self.readonly = False
|
||||||
|
if not self.description:
|
||||||
|
self.description = f'limit for {head}'
|
||||||
|
if self.export.startswith('_') and PREDEFINED_ACCESSIBLES.get(head):
|
||||||
|
self.export = self.export[1:]
|
||||||
|
|
||||||
|
def set_datatype(self, datatype):
|
||||||
|
if self.hasDatatype():
|
||||||
|
return # the programmer is responsible that a given datatype is correct
|
||||||
|
postfix = self.name.rpartition('_')[-1]
|
||||||
|
postfix = self.name.rpartition('_')[-1]
|
||||||
|
if postfix == 'limits':
|
||||||
|
self.datatype = TupleOf(datatype, datatype)
|
||||||
|
self.default = (datatype.min, datatype.max)
|
||||||
|
else: # min, max
|
||||||
|
self.datatype = datatype
|
||||||
|
self.default = getattr(datatype, postfix)
|
||||||
|
|
||||||
|
|
||||||
# list of predefined accessibles with their type
|
# list of predefined accessibles with their type
|
||||||
PREDEFINED_ACCESSIBLES = {
|
PREDEFINED_ACCESSIBLES = {
|
||||||
'value': Parameter,
|
'value': Parameter,
|
||||||
|
@ -57,7 +57,7 @@ import json
|
|||||||
|
|
||||||
from frappy.lib import generalConfig
|
from frappy.lib import generalConfig
|
||||||
from frappy.datatypes import EnumType
|
from frappy.datatypes import EnumType
|
||||||
from frappy.params import Parameter, Property, Command
|
from frappy.params import Parameter, Property, Command, Limit
|
||||||
from frappy.modules import Module
|
from frappy.modules import Module
|
||||||
|
|
||||||
|
|
||||||
@ -67,6 +67,10 @@ class PersistentParam(Parameter):
|
|||||||
given = False
|
given = False
|
||||||
|
|
||||||
|
|
||||||
|
class PersistentLimit(Limit, Parameter):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class PersistentMixin(Module):
|
class PersistentMixin(Module):
|
||||||
persistentData = None # dict containing persistent data after startup
|
persistentData = None # dict containing persistent data after startup
|
||||||
|
|
||||||
|
@ -81,6 +81,7 @@ class Dispatcher:
|
|||||||
def __init__(self, name, log, opts, srv):
|
def __init__(self, name, log, opts, srv):
|
||||||
self.log = log
|
self.log = log
|
||||||
self._modules = {}
|
self._modules = {}
|
||||||
|
self.equipment_id = opts.pop('equipment_id', name)
|
||||||
|
|
||||||
def announce_update(self, modulename, pname, pobj):
|
def announce_update(self, modulename, pname, pobj):
|
||||||
if pobj.readerror:
|
if pobj.readerror:
|
||||||
|
@ -29,7 +29,7 @@ import pytest
|
|||||||
from frappy.datatypes import BoolType, FloatRange, StringType, IntRange, ScaledInteger
|
from frappy.datatypes import BoolType, FloatRange, StringType, IntRange, ScaledInteger
|
||||||
from frappy.errors import ProgrammingError, ConfigError, RangeError
|
from frappy.errors import ProgrammingError, ConfigError, RangeError
|
||||||
from frappy.modules import Communicator, Drivable, Readable, Module
|
from frappy.modules import Communicator, Drivable, Readable, Module
|
||||||
from frappy.params import Command, Parameter
|
from frappy.params import Command, Parameter, Limit
|
||||||
from frappy.rwhandler import ReadHandler, WriteHandler, nopoll
|
from frappy.rwhandler import ReadHandler, WriteHandler, nopoll
|
||||||
from frappy.lib import generalConfig
|
from frappy.lib import generalConfig
|
||||||
|
|
||||||
@ -795,17 +795,17 @@ stdlim = {
|
|||||||
|
|
||||||
class Lim(Module):
|
class Lim(Module):
|
||||||
a = Parameter('', FloatRange(-10, 10), readonly=False, default=0)
|
a = Parameter('', FloatRange(-10, 10), readonly=False, default=0)
|
||||||
a_min = Parameter()
|
a_min = Limit()
|
||||||
a_max = Parameter()
|
a_max = Limit()
|
||||||
|
|
||||||
b = Parameter('', FloatRange(0, None), readonly=False, default=0)
|
b = Parameter('', FloatRange(0, None), readonly=False, default=0)
|
||||||
b_min = Parameter()
|
b_min = Limit()
|
||||||
|
|
||||||
c = Parameter('', IntRange(None, 100), readonly=False, default=0)
|
c = Parameter('', IntRange(None, 100), readonly=False, default=0)
|
||||||
c_max = Parameter()
|
c_max = Limit()
|
||||||
|
|
||||||
d = Parameter('', FloatRange(-5, 5), readonly=False, default=0)
|
d = Parameter('', FloatRange(-5, 5), readonly=False, default=0)
|
||||||
d_limits = Parameter()
|
d_limits = Limit()
|
||||||
|
|
||||||
e = Parameter('', IntRange(0, 8), readonly=False, default=0)
|
e = Parameter('', IntRange(0, 8), readonly=False, default=0)
|
||||||
|
|
||||||
@ -872,8 +872,8 @@ def test_limit_inheritance():
|
|||||||
raise ValueError('value is not a multiple of 0.25')
|
raise ValueError('value is not a multiple of 0.25')
|
||||||
|
|
||||||
class Mixin:
|
class Mixin:
|
||||||
a_min = Parameter()
|
a_min = Limit()
|
||||||
a_max = Parameter()
|
a_max = Limit()
|
||||||
|
|
||||||
class Mod(Mixin, Base):
|
class Mod(Mixin, Base):
|
||||||
def check_a(self, value):
|
def check_a(self, value):
|
||||||
|
Loading…
x
Reference in New Issue
Block a user