ReadHandler and WriteHandler decorators
modules with a couple of parameters with similar read_* or write_* methods may handle them by generic methods wrapped with decorators ReadHandler / WriteHandler The trinamic driver is included in this change for demonstrating how it works. In a further step, the special handling for the iohandler stuff can be moved away from secop.server and secop.params, using this feature. + fix problem on startup of trinamic driver (needs MultiEvent.queue) + some other small fixes + apply recommended functools.wraps for wrapping Change-Id: Ibfeff9209f53c47194628463466cee28366e17ac Reviewed-on: https://forge.frm2.tum.de/review/c/sine2020/secop/playground/+/27460 Tested-by: Jenkins Automated Tests <pedersen+jenkins@frm2.tum.de> Reviewed-by: Enrico Faulhaber <enrico.faulhaber@frm2.tum.de> Reviewed-by: Markus Zolliker <markus.zolliker@psi.ch>
This commit is contained in:
@@ -86,7 +86,7 @@ class IOBase(Communicator):
|
||||
uri = Property('hostname:portnumber', datatype=StringType())
|
||||
timeout = Parameter('timeout', datatype=FloatRange(0), default=2)
|
||||
wait_before = Parameter('wait time before sending', datatype=FloatRange(), default=0)
|
||||
is_connected = Parameter('connection state', datatype=BoolType(), readonly=False, poll=REGULAR)
|
||||
is_connected = Parameter('connection state', datatype=BoolType(), readonly=False, default=False, poll=REGULAR)
|
||||
pollinterval = Parameter('reconnect interval', datatype=FloatRange(0), readonly=False, default=10)
|
||||
|
||||
_reconnectCallbacks = None
|
||||
@@ -95,6 +95,7 @@ class IOBase(Communicator):
|
||||
_lock = None
|
||||
|
||||
def earlyInit(self):
|
||||
super().earlyInit()
|
||||
self._lock = threading.RLock()
|
||||
|
||||
def connectStart(self):
|
||||
|
||||
@@ -323,4 +323,4 @@ class UniqueObject:
|
||||
self.name = name
|
||||
|
||||
def __repr__(self):
|
||||
return 'UniqueObject(%r)' % self.name
|
||||
return self.name
|
||||
|
||||
@@ -59,6 +59,7 @@ class MultiEvent(threading.Event):
|
||||
self._lock = threading.Lock()
|
||||
self.default_timeout = default_timeout or None # treat 0 as None
|
||||
self.name = None # default event name
|
||||
self._actions = [] # actions to be executed on trigger
|
||||
super().__init__()
|
||||
|
||||
def new(self, timeout=None, name=None):
|
||||
@@ -81,6 +82,12 @@ class MultiEvent(threading.Event):
|
||||
self.events.discard(event)
|
||||
if self.events:
|
||||
return
|
||||
try:
|
||||
for action in self._actions:
|
||||
action()
|
||||
except Exception:
|
||||
pass # we silently ignore errors here
|
||||
self._actions = []
|
||||
super().set()
|
||||
|
||||
def clear_(self, event):
|
||||
@@ -116,3 +123,19 @@ class MultiEvent(threading.Event):
|
||||
as a convenience method
|
||||
"""
|
||||
return self.new(timeout, name).set
|
||||
|
||||
def queue(self, action):
|
||||
"""add an action to the queue of actions to be executed at end
|
||||
|
||||
:param action: a function, to be executed after the last event is triggered,
|
||||
and before the multievent is set
|
||||
|
||||
- if no events are waiting, the actions are executed immediately
|
||||
- if an action raises an exception, it is silently ignore and further
|
||||
actions in the queue are skipped
|
||||
- if this is not desired, the action should handle errors by itself
|
||||
"""
|
||||
with self._lock:
|
||||
self._actions.append(action)
|
||||
if self.is_set():
|
||||
self.set_(None)
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
import sys
|
||||
import time
|
||||
from collections import OrderedDict
|
||||
from functools import wraps
|
||||
|
||||
from secop.datatypes import ArrayOf, BoolType, EnumType, FloatRange, \
|
||||
IntRange, StatusType, StringType, TextType, TupleOf
|
||||
@@ -38,6 +39,7 @@ from secop.poller import BasicPoller, Poller
|
||||
from secop.properties import HasProperties, Property
|
||||
from secop.logging import RemoteLogHandler, HasComlog
|
||||
|
||||
|
||||
Done = UniqueObject('already set')
|
||||
"""a special return value for a read/write function
|
||||
|
||||
@@ -107,12 +109,14 @@ class HasAccessibles(HasProperties):
|
||||
# XXX: create getters for the units of params ??
|
||||
|
||||
# wrap of reading/writing funcs
|
||||
if isinstance(pobj, Command):
|
||||
# nothing to do for now
|
||||
if not isinstance(pobj, Parameter):
|
||||
# nothing to do for Commands
|
||||
continue
|
||||
|
||||
rfunc = getattr(cls, 'read_' + pname, None)
|
||||
# TODO: remove handler stuff here
|
||||
rfunc_handler = pobj.handler.get_read_func(cls, pname) if pobj.handler else None
|
||||
wrapped = hasattr(rfunc, '__wrapped__')
|
||||
wrapped = getattr(rfunc, 'wrapped', False) # meaning: wrapped or auto generated
|
||||
if rfunc_handler:
|
||||
if 'read_' + pname in cls.__dict__:
|
||||
if pname in cls.__dict__:
|
||||
@@ -126,73 +130,78 @@ class HasAccessibles(HasProperties):
|
||||
# create wrapper except when read function is already wrapped
|
||||
if not wrapped:
|
||||
|
||||
def wrapped_rfunc(self, pname=pname, rfunc=rfunc):
|
||||
if rfunc:
|
||||
self.log.debug("call read_%s" % pname)
|
||||
if rfunc:
|
||||
|
||||
@wraps(rfunc) # handles __wrapped__ and __doc__
|
||||
def new_rfunc(self, pname=pname, rfunc=rfunc):
|
||||
try:
|
||||
value = rfunc(self)
|
||||
if value is Done: # the setter is already triggered
|
||||
value = getattr(self, pname)
|
||||
self.log.debug("read_%s returned Done (%r)" % (pname, value))
|
||||
return value
|
||||
self.log.debug("read_%s returned %r" % (pname, value))
|
||||
self.log.debug("read_%s returned %r", pname, value)
|
||||
except Exception as e:
|
||||
self.log.debug("read_%s failed %r" % (pname, e))
|
||||
self.announceUpdate(pname, None, e)
|
||||
self.log.debug("read_%s failed with %r", pname, e)
|
||||
raise
|
||||
else:
|
||||
# return cached value
|
||||
value = self.accessibles[pname].value
|
||||
self.log.debug("return cached %s = %r" % (pname, value))
|
||||
setattr(self, pname, value) # important! trigger the setter
|
||||
return value
|
||||
if value is Done:
|
||||
return getattr(self, pname)
|
||||
setattr(self, pname, value) # important! trigger the setter
|
||||
return value
|
||||
else:
|
||||
|
||||
if rfunc:
|
||||
wrapped_rfunc.__doc__ = rfunc.__doc__
|
||||
setattr(cls, 'read_' + pname, wrapped_rfunc)
|
||||
wrapped_rfunc.__wrapped__ = True
|
||||
def new_rfunc(self, pname=pname):
|
||||
return getattr(self, pname)
|
||||
|
||||
new_rfunc.__doc__ = 'auto generated read method for ' + pname
|
||||
|
||||
new_rfunc.wrapped = True # indicate to subclasses that no more wrapping is needed
|
||||
setattr(cls, 'read_' + pname, new_rfunc)
|
||||
|
||||
if not pobj.readonly:
|
||||
wfunc = getattr(cls, 'write_' + pname, None)
|
||||
wrapped = hasattr(wfunc, '__wrapped__')
|
||||
wrapped = getattr(wfunc, 'wrapped', False) # meaning: wrapped or auto generated
|
||||
if (wfunc is None or wrapped) and pobj.handler:
|
||||
# ignore the handler, if a write function is present
|
||||
# TODO: remove handler stuff here
|
||||
wfunc = pobj.handler.get_write_func(pname)
|
||||
wrapped = False
|
||||
|
||||
# create wrapper except when write function is already wrapped
|
||||
if not wrapped:
|
||||
|
||||
def wrapped_wfunc(self, value, pname=pname, wfunc=wfunc):
|
||||
pobj = self.accessibles[pname]
|
||||
if wfunc:
|
||||
self.log.debug("check and call write_%s(%r)" % (pname, value))
|
||||
if wfunc:
|
||||
|
||||
@wraps(wfunc) # handles __wrapped__ and __doc__
|
||||
def new_wfunc(self, value, pname=pname, wfunc=wfunc):
|
||||
pobj = self.accessibles[pname]
|
||||
self.log.debug('validate %r for %r', value, pname)
|
||||
# we do not need to handle errors here, we do not
|
||||
# want to make a parameter invalid, when a write failed
|
||||
value = pobj.datatype(value)
|
||||
returned_value = wfunc(self, value)
|
||||
if returned_value is Done: # the setter is already triggered
|
||||
self.log.debug('write_%s(%r) returned %r', pname, value, returned_value)
|
||||
if returned_value is Done:
|
||||
# setattr(self, pname, getattr(self, pname))
|
||||
return getattr(self, pname)
|
||||
if returned_value is not None: # goodie: accept missing return value
|
||||
value = returned_value
|
||||
else:
|
||||
self.log.debug("check %s = %r" % (pname, value))
|
||||
value = pobj.datatype(value)
|
||||
setattr(self, pname, value)
|
||||
return value
|
||||
setattr(self, pname, value) # important! trigger the setter
|
||||
return value
|
||||
else:
|
||||
|
||||
if wfunc:
|
||||
wrapped_wfunc.__doc__ = wfunc.__doc__
|
||||
setattr(cls, 'write_' + pname, wrapped_wfunc)
|
||||
wrapped_wfunc.__wrapped__ = True
|
||||
def new_wfunc(self, value, pname=pname):
|
||||
setattr(self, pname, value)
|
||||
return value
|
||||
|
||||
new_wfunc.__doc__ = 'auto generated write method for ' + pname
|
||||
|
||||
new_wfunc.wrapped = True # indicate to subclasses that no more wrapping is needed
|
||||
setattr(cls, 'write_' + pname, new_wfunc)
|
||||
|
||||
# check for programming errors
|
||||
for attrname in cls.__dict__:
|
||||
for attrname, attrvalue in cls.__dict__.items():
|
||||
prefix, _, pname = attrname.partition('_')
|
||||
if not pname:
|
||||
continue
|
||||
if prefix == 'do':
|
||||
raise ProgrammingError('%r: old style command %r not supported anymore'
|
||||
% (cls.__name__, attrname))
|
||||
if prefix in ('read', 'write') and not isinstance(accessibles.get(pname), Parameter):
|
||||
if prefix in ('read', 'write') and not getattr(attrvalue, 'wrapped', False):
|
||||
raise ProgrammingError('%s.%s defined, but %r is no parameter'
|
||||
% (cls.__name__, attrname, pname))
|
||||
|
||||
@@ -394,7 +403,7 @@ class Module(HasAccessibles):
|
||||
'value and was not given in config!' % pname)
|
||||
# we do not want to call the setter for this parameter for now,
|
||||
# this should happen on the first read
|
||||
pobj.readerror = ConfigError('not initialized')
|
||||
pobj.readerror = ConfigError('parameter %r not initialized' % pname)
|
||||
# above error will be triggered on activate after startup,
|
||||
# when not all hardware parameters are read because of startup timeout
|
||||
pobj.value = pobj.datatype(pobj.datatype.default)
|
||||
@@ -459,11 +468,14 @@ class Module(HasAccessibles):
|
||||
|
||||
def announceUpdate(self, pname, value=None, err=None, timestamp=None):
|
||||
"""announce a changed value or readerror"""
|
||||
|
||||
# TODO: remove readerror 'property' and replace value with exception
|
||||
pobj = self.parameters[pname]
|
||||
timestamp = timestamp or time.time()
|
||||
changed = pobj.value != value
|
||||
try:
|
||||
# store the value even in case of error
|
||||
# TODO: we should neither check limits nor convert string to float here
|
||||
pobj.value = pobj.datatype(value)
|
||||
except Exception as e:
|
||||
if not err: # do not overwrite given error
|
||||
|
||||
@@ -196,7 +196,6 @@ class Parameter(Accessible):
|
||||
self.ownProperties = {k: getattr(self, k) for k in self.propertyDict}
|
||||
|
||||
def __get__(self, instance, owner):
|
||||
# not used yet
|
||||
if instance is None:
|
||||
return self
|
||||
return instance.parameters[self.name].value
|
||||
|
||||
@@ -103,6 +103,7 @@ class PersistentMixin(HasAccessibles):
|
||||
try:
|
||||
value = pobj.datatype.import_value(self.persistentData[pname])
|
||||
pobj.value = value
|
||||
pobj.readerror = None
|
||||
if not pobj.readonly:
|
||||
writeDict[pname] = value
|
||||
except Exception as e:
|
||||
@@ -144,5 +145,6 @@ class PersistentMixin(HasAccessibles):
|
||||
|
||||
@Command()
|
||||
def factory_reset(self):
|
||||
"""reset to values from config / default values"""
|
||||
self.writeDict.update(self.initData)
|
||||
self.writeInitParams()
|
||||
|
||||
@@ -238,13 +238,9 @@ class Dispatcher:
|
||||
|
||||
# validate!
|
||||
value = pobj.datatype(value)
|
||||
writefunc = getattr(moduleobj, 'write_%s' % pname, None)
|
||||
# note: exceptions are handled in handle_request, not here!
|
||||
if writefunc:
|
||||
# return value is ignored here, as it is automatically set on the pobj and broadcast
|
||||
writefunc(value)
|
||||
else:
|
||||
setattr(moduleobj, pname, value)
|
||||
getattr(moduleobj, 'write_' + pname)(value)
|
||||
# return value is ignored here, as already handled
|
||||
return pobj.export_value(), dict(t=pobj.timestamp) if pobj.timestamp else {}
|
||||
|
||||
def _getParameterValue(self, modulename, exportedname):
|
||||
@@ -261,11 +257,9 @@ class Dispatcher:
|
||||
# raise ReadOnlyError('This parameter is constant and can not be accessed remotely.')
|
||||
return pobj.datatype.export_value(pobj.constant)
|
||||
|
||||
readfunc = getattr(moduleobj, 'read_%s' % pname, None)
|
||||
if readfunc:
|
||||
# should also update the pobj (via the setter from the metaclass)
|
||||
# note: exceptions are handled in handle_request, not here!
|
||||
readfunc()
|
||||
# note: exceptions are handled in handle_request, not here!
|
||||
getattr(moduleobj, 'read_' + pname)()
|
||||
# return value is ignored here, as already handled
|
||||
return pobj.export_value(), dict(t=pobj.timestamp) if pobj.timestamp else {}
|
||||
|
||||
#
|
||||
|
||||
135
secop/rwhandler.py
Normal file
135
secop/rwhandler.py
Normal file
@@ -0,0 +1,135 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- 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:
|
||||
# Markus Zolliker <markus.zolliker@psi.ch>
|
||||
# *****************************************************************************
|
||||
|
||||
"""decorator class for common read_/write_ methods
|
||||
|
||||
Usage:
|
||||
|
||||
Example 1: combined read/write for multiple parameters
|
||||
|
||||
PID_PARAMS = ['p', 'i', 'd']
|
||||
|
||||
@ReadHandler(PID_PARAMS)
|
||||
def read_pid(self, pname):
|
||||
self.p, self.i, self.d = self.get_pid_from_hw()
|
||||
return Done # Done is indicating that the parameters are already assigned
|
||||
|
||||
@WriteHandler(PID_PARAMS)
|
||||
def write_pid(self, pname, value):
|
||||
pid = self.get_pid_from_hw() # assume this returns a list
|
||||
pid[PID_PARAMS.index(pname)] = value
|
||||
self.put_pid_to_hw(pid)
|
||||
return self.read_pid()
|
||||
|
||||
Example 2: addressable HW parameters
|
||||
|
||||
HW_ADDR = {'p': 25, 'i': 26, 'd': 27}
|
||||
|
||||
@ReadHandler(HW_ADDR)
|
||||
def read_addressed(self, pname):
|
||||
return self.get_hw_register(HW_ADDR[pname])
|
||||
|
||||
@WriteHandler(HW_ADDR)
|
||||
def write_addressed(self, pname, value):
|
||||
self.put_hw_register(HW_ADDR[pname], value)
|
||||
return self.get_hw_register(HW_ADDR[pname])
|
||||
"""
|
||||
|
||||
from functools import wraps
|
||||
from secop.modules import Done
|
||||
from secop.errors import ProgrammingError
|
||||
|
||||
|
||||
class Handler:
|
||||
func = None
|
||||
method_names = set() # this is shared among all instances of handlers!
|
||||
wrapped = True # allow to use read_* or write_* as name of the decorated method
|
||||
|
||||
def __init__(self, keys):
|
||||
"""initialize the decorator
|
||||
|
||||
:param keys: parameter names (an iterable)
|
||||
"""
|
||||
self.keys = set(keys)
|
||||
|
||||
def __call__(self, func):
|
||||
"""decorator call"""
|
||||
self.func = func
|
||||
if func.__qualname__ in self.method_names:
|
||||
raise ProgrammingError('duplicate method %r' % func.__qualname__)
|
||||
func.wrapped = False
|
||||
# __qualname__ used here (avoid conflicts between different modules)
|
||||
self.method_names.add(func.__qualname__)
|
||||
return self
|
||||
|
||||
def __get__(self, obj, owner=None):
|
||||
"""allow access to the common method"""
|
||||
if obj is None:
|
||||
return self
|
||||
return self.func.__get__(obj, owner)
|
||||
|
||||
|
||||
class ReadHandler(Handler):
|
||||
"""decorator for read methods"""
|
||||
|
||||
def __set_name__(self, owner, name):
|
||||
"""create the wrapped read_* methods"""
|
||||
|
||||
self.method_names.discard(self.func.__qualname__)
|
||||
for key in self.keys:
|
||||
|
||||
@wraps(self.func)
|
||||
def wrapped(module, pname=key, func=self.func):
|
||||
value = func(module, pname)
|
||||
if value is not Done:
|
||||
setattr(module, pname, value)
|
||||
return value
|
||||
|
||||
wrapped.wrapped = True
|
||||
method = 'read_' + key
|
||||
if hasattr(owner, method):
|
||||
raise ProgrammingError('superfluous method %s.%s (overwritten by ReadHandler)'
|
||||
% (owner.__name__, method))
|
||||
setattr(owner, method, wrapped)
|
||||
|
||||
|
||||
class WriteHandler(Handler):
|
||||
"""decorator for write methods"""
|
||||
|
||||
def __set_name__(self, owner, name):
|
||||
"""create the wrapped write_* methods"""
|
||||
|
||||
self.method_names.discard(self.func.__qualname__)
|
||||
for key in self.keys:
|
||||
|
||||
@wraps(self.func)
|
||||
def wrapped(module, value, pname=key, func=self.func):
|
||||
value = func(module, pname, value)
|
||||
if value is not Done:
|
||||
setattr(module, pname, value)
|
||||
return value
|
||||
|
||||
wrapped.wrapped = True
|
||||
method = 'write_' + key
|
||||
if hasattr(owner, method):
|
||||
raise ProgrammingError('superfluous method %s.%s (overwritten by WriteHandler)'
|
||||
% (owner.__name__, method))
|
||||
setattr(owner, method, wrapped)
|
||||
Reference in New Issue
Block a user