updates from mlz repo

- asynconn: raise * from *
- asynconn: correct handling for timeout in AsynSerial
- add new py35compat
- target unit $

Change-Id: I052185ad3ebb3e3d1e3374f7ece9c7df06223951
This commit is contained in:
zolliker 2021-11-10 14:37:57 +01:00
parent 41ce909172
commit a7b741eaa4
3 changed files with 90 additions and 35 deletions

View File

@ -48,6 +48,7 @@ class ConnectionClosed(ConnectionError):
class AsynConn: class AsynConn:
timeout = 1 # inter byte timeout timeout = 1 # inter byte timeout
scheme = None
SCHEME_MAP = {} SCHEME_MAP = {}
connection = None # is not None, if connected connection = None # is not None, if connected
defaultport = None defaultport = None
@ -62,11 +63,11 @@ class AsynConn:
except (ValueError, TypeError, AssertionError): except (ValueError, TypeError, AssertionError):
if 'COM' in uri: if 'COM' in uri:
raise ValueError("the correct uri for a COM port is: " raise ValueError("the correct uri for a COM port is: "
"'serial://COM<i>[?<option>=<value>[+<option>=value ...]]'") "'serial://COM<i>[?<option>=<value>[+<option>=value ...]]'") from None
if '/dev' in uri: if '/dev' in uri:
raise ValueError("the correct uri for a serial port is: " raise ValueError("the correct uri for a serial port is: "
"'serial:///dev/<tty>[?<option>=<value>[+<option>=value ...]]'") "'serial:///dev/<tty>[?<option>=<value>[+<option>=value ...]]'") from None
raise ValueError('invalid uri: %s' % uri) raise ValueError('invalid uri: %s' % uri) from None
iocls = cls.SCHEME_MAP['tcp'] iocls = cls.SCHEME_MAP['tcp']
uri = 'tcp://%s:%d' % host_port uri = 'tcp://%s:%d' % host_port
return object.__new__(iocls) return object.__new__(iocls)
@ -80,7 +81,9 @@ class AsynConn:
@classmethod @classmethod
def __init_subclass__(cls): def __init_subclass__(cls):
cls.SCHEME_MAP[cls.scheme] = cls """register subclass to scheme, if available"""
if cls.scheme:
cls.SCHEME_MAP[cls.scheme] = cls
def disconnect(self): def disconnect(self):
raise NotImplementedError raise NotImplementedError
@ -166,7 +169,7 @@ class AsynTcp(AsynConn):
self.connection = tcpSocket(uri, self.defaultport, self.timeout) self.connection = tcpSocket(uri, self.defaultport, self.timeout)
except (ConnectionRefusedError, socket.gaierror) as e: except (ConnectionRefusedError, socket.gaierror) as e:
# indicate that retrying might make sense # indicate that retrying might make sense
raise CommunicationFailedError(str(e)) raise CommunicationFailedError(str(e)) from None
def disconnect(self): def disconnect(self):
if self.connection: if self.connection:
@ -237,8 +240,8 @@ class AsynSerial(AsynConn):
options = dict((kv.split('=') for kv in uri[1].split('+'))) options = dict((kv.split('=') for kv in uri[1].split('+')))
except IndexError: # no uri[1], no options except IndexError: # no uri[1], no options
options = {} options = {}
except ValueError: except ValueError as e:
raise ConfigError('illegal serial options') raise ConfigError('illegal serial options') from e
parity = options.pop('parity', None) # only parity is to be treated as text parity = options.pop('parity', None) # only parity is to be treated as text
for k, v in options.items(): for k, v in options.items():
try: try:
@ -251,14 +254,12 @@ class AsynSerial(AsynConn):
if not fullname.startswith(name): if not fullname.startswith(name):
raise ConfigError('illegal parity: %s' % parity) raise ConfigError('illegal parity: %s' % parity)
options['parity'] = name[0] options['parity'] = name[0]
if 'timeout' in options: if 'timeout' not in options:
options['timeout'] = float(self.timeout)
else:
options['timeout'] = self.timeout options['timeout'] = self.timeout
try: try:
self.connection = Serial(dev, **options) self.connection = Serial(dev, **options)
except ValueError as e: except ValueError as e:
raise ConfigError(e) raise ConfigError(e) from None
# TODO: turn exceptions into ConnectionFailedError, where a retry makes sense # TODO: turn exceptions into ConnectionFailedError, where a retry makes sense
def disconnect(self): def disconnect(self):

58
secop/lib/py35compat.py Normal file
View File

@ -0,0 +1,58 @@
# -*- 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>
#
# *****************************************************************************
"""workaround for python versions older than 3.6
``Object`` must be inherited for classes needing support for
__init_subclass__ and __set_name__
"""
if hasattr(object, '__init_subclass__'):
class Object:
pass
else:
class PEP487Metaclass(type):
# support for __set_name__ and __init_subclass__ for older python versions
# slightly modified from PEP487 doc
def __new__(cls, *args, **kwargs):
if len(args) != 3:
return super().__new__(cls, *args)
name, bases, ns = args
init = ns.get('__init_subclass__')
if callable(init):
ns['__init_subclass__'] = classmethod(init)
newtype = super().__new__(cls, name, bases, ns)
for k, v in newtype.__dict__.items():
func = getattr(v, '__set_name__', None)
if func is not None:
func(newtype, k)
if bases:
super(newtype, newtype).__init_subclass__(**kwargs) # pylint: disable=bad-super-call
return newtype
def __init__(cls, name, bases, ns, **kwargs):
super().__init__(name, bases, ns)
class Object(metaclass=PEP487Metaclass):
@classmethod
def __init_subclass__(cls, *args, **kwargs):
pass

View File

@ -29,7 +29,7 @@ from collections import OrderedDict
from secop.datatypes import ArrayOf, BoolType, EnumType, FloatRange, \ from secop.datatypes import ArrayOf, BoolType, EnumType, FloatRange, \
IntRange, StatusType, StringType, TextType, TupleOf IntRange, StatusType, StringType, TextType, TupleOf
from secop.errors import BadValueError, ConfigError, InternalError, \ from secop.errors import BadValueError, ConfigError, \
ProgrammingError, SECoPError, SilentError, secop_error ProgrammingError, SECoPError, SilentError, secop_error
from secop.lib import formatException, mkthread from secop.lib import formatException, mkthread
from secop.lib.enum import Enum from secop.lib.enum import Enum
@ -57,6 +57,7 @@ class HasAccessibles(HasProperties):
merged_properties = {} # dict of dict of merged properties merged_properties = {} # dict of dict of merged properties
new_names = [] # list of names of new accessibles new_names = [] # list of names of new accessibles
override_values = {} # bare values overriding a parameter and methods overriding a command override_values = {} # bare values overriding a parameter and methods overriding a command
for base in reversed(cls.__mro__): for base in reversed(cls.__mro__):
for key, value in base.__dict__.items(): for key, value in base.__dict__.items():
if isinstance(value, Accessible): if isinstance(value, Accessible):
@ -66,8 +67,6 @@ class HasAccessibles(HasProperties):
accessibles[key] = value accessibles[key] = value
override_values.pop(key, None) override_values.pop(key, None)
elif key in accessibles: elif key in accessibles:
# either a bare value overriding a parameter
# or a method overriding a command
override_values[key] = value override_values[key] = value
for aname, aobj in accessibles.items(): for aname, aobj in accessibles.items():
if aname in override_values: if aname in override_values:
@ -244,12 +243,12 @@ class Module(HasAccessibles):
# reference to the dispatcher (used for sending async updates) # reference to the dispatcher (used for sending async updates)
DISPATCHER = None DISPATCHER = None
pollerClass = Poller #: default poller used pollerClass = Poller #: default poller used
def __init__(self, name, logger, cfgdict, srv): def __init__(self, name, logger, cfgdict, srv):
# remember the dispatcher object (for the async callbacks) # remember the dispatcher object (for the async callbacks)
self.DISPATCHER = srv.dispatcher self.DISPATCHER = srv.dispatcher
self.omit_unchanged_within = getattr(self.DISPATCHER, 'omit_unchanged_within', 0.1)
self.log = logger self.log = logger
self.name = name self.name = name
self.valueCallbacks = {} self.valueCallbacks = {}
@ -444,23 +443,20 @@ class Module(HasAccessibles):
pobj = self.parameters[pname] pobj = self.parameters[pname]
timestamp = timestamp or time.time() timestamp = timestamp or time.time()
changed = pobj.value != value changed = pobj.value != value
if value is not None: try:
pobj.value = value # store the value even in case of error # store the value even in case of error
pobj.value = pobj.datatype(value)
except Exception as e:
if not err: # do not overwrite given error
err = e
if err: if err:
if not isinstance(err, SECoPError): err = secop_error(err)
err = InternalError(err)
if str(err) == str(pobj.readerror): if str(err) == str(pobj.readerror):
return # do call updates for repeated errors return # do call updates for repeated errors
else: elif not changed and timestamp < (pobj.timestamp or 0) + self.omit_unchanged_within:
try: # no change within short time -> omit
pobj.value = pobj.datatype(value) return
except Exception as e: pobj.timestamp = timestamp or time.time()
err = secop_error(e)
if not changed and timestamp < ((pobj.timestamp or 0)
+ self.DISPATCHER.OMIT_UNCHANGED_WITHIN):
# no change within short time -> omit
return
pobj.timestamp = timestamp
pobj.readerror = err pobj.readerror = err
if pobj.export: if pobj.export:
self.DISPATCHER.announce_update(self.name, pname, pobj) self.DISPATCHER.announce_update(self.name, pname, pobj)
@ -495,15 +491,15 @@ class Module(HasAccessibles):
for pname in self.parameters: for pname in self.parameters:
errfunc = getattr(modobj, 'error_update_' + pname, None) errfunc = getattr(modobj, 'error_update_' + pname, None)
if errfunc: if errfunc:
def errcb(err, p=pname, m=modobj, efunc=errfunc): def errcb(err, p=pname, efunc=errfunc):
try: try:
efunc(err) efunc(err)
except Exception as e: except Exception as e:
m.announceUpdate(p, err=e) modobj.announceUpdate(p, err=e)
self.errorCallbacks[pname].append(errcb) self.errorCallbacks[pname].append(errcb)
else: else:
def errcb(err, p=pname, m=modobj): def errcb(err, p=pname):
m.announceUpdate(p, err=err) modobj.announceUpdate(p, err=err)
if pname in autoupdate: if pname in autoupdate:
self.errorCallbacks[pname].append(errcb) self.errorCallbacks[pname].append(errcb)
@ -516,8 +512,8 @@ class Module(HasAccessibles):
efunc(e) efunc(e)
self.valueCallbacks[pname].append(cb) self.valueCallbacks[pname].append(cb)
elif pname in autoupdate: elif pname in autoupdate:
def cb(value, p=pname, m=modobj): def cb(value, p=pname):
m.announceUpdate(p, value) modobj.announceUpdate(p, value)
self.valueCallbacks[pname].append(cb) self.valueCallbacks[pname].append(cb)
def isBusy(self, status=None): def isBusy(self, status=None):