fix parameter inheritance using MRO

+ no update on unhchanged values within 1 sec

Change-Id: I3e3d50bb5541e8d4da2badc3133d243dd0a3b892
This commit is contained in:
zolliker 2021-10-06 08:32:47 +02:00
parent 544e42033d
commit e38cd11bfe
8 changed files with 400 additions and 164 deletions

View File

@ -102,7 +102,7 @@ class DataType(HasProperties):
self.setProperty(k, v)
self.checkProperties()
except Exception as e:
raise ProgrammingError(str(e))
raise ProgrammingError(str(e)) from None
def get_info(self, **kwds):
"""prepare dict for export or repr
@ -194,7 +194,7 @@ class FloatRange(DataType):
try:
value = float(value)
except Exception:
raise BadValueError('Can not convert %r to float' % value)
raise BadValueError('Can not convert %r to float' % value) from None
# map +/-infty to +/-max possible number
value = clamp(-sys.float_info.max, value, sys.float_info.max)
@ -268,7 +268,7 @@ class IntRange(DataType):
fvalue = float(value)
value = int(value)
except Exception:
raise BadValueError('Can not convert %r to int' % value)
raise BadValueError('Can not convert %r to int' % value) from None
if not self.min <= value <= self.max or round(fvalue) != fvalue:
raise BadValueError('%r should be an int between %d and %d' %
(value, self.min, self.max))
@ -371,7 +371,7 @@ class ScaledInteger(DataType):
try:
value = float(value)
except Exception:
raise BadValueError('Can not convert %r to float' % value)
raise BadValueError('Can not convert %r to float' % value) from None
prec = max(self.scale, abs(value * self.relative_resolution),
self.absolute_resolution)
if self.min - prec <= value <= self.max + prec:
@ -454,7 +454,7 @@ class EnumType(DataType):
try:
return self._enum[value]
except (KeyError, TypeError): # TypeError will be raised when value is not hashable
raise BadValueError('%r is not a member of enum %r' % (value, self._enum))
raise BadValueError('%r is not a member of enum %r' % (value, self._enum)) from None
def from_string(self, text):
return self(text)
@ -533,7 +533,7 @@ class BLOBType(DataType):
if self.minbytes < other.minbytes or self.maxbytes > other.maxbytes:
raise BadValueError('incompatible datatypes')
except AttributeError:
raise BadValueError('incompatible datatypes')
raise BadValueError('incompatible datatypes') from None
class StringType(DataType):
@ -572,7 +572,7 @@ class StringType(DataType):
try:
value.encode('ascii')
except UnicodeEncodeError:
raise BadValueError('%r contains non-ascii character!' % value)
raise BadValueError('%r contains non-ascii character!' % value) from None
size = len(value)
if size < self.minchars:
raise BadValueError(
@ -606,7 +606,7 @@ class StringType(DataType):
self.isUTF8 > other.isUTF8:
raise BadValueError('incompatible datatypes')
except AttributeError:
raise BadValueError('incompatible datatypes')
raise BadValueError('incompatible datatypes') from None
# TextType is a special StringType intended for longer texts (i.e. embedding \n),
@ -618,7 +618,7 @@ class TextType(StringType):
def __init__(self, maxchars=None):
if maxchars is None:
maxchars = UNLIMITED
super(TextType, self).__init__(0, maxchars)
super().__init__(0, maxchars)
def __repr__(self):
if self.maxchars == UNLIMITED:
@ -771,7 +771,7 @@ class ArrayOf(DataType):
raise BadValueError('incompatible datatypes')
self.members.compatible(other.members)
except AttributeError:
raise BadValueError('incompatible datatypes')
raise BadValueError('incompatible datatypes') from None
class TupleOf(DataType):
@ -811,7 +811,7 @@ class TupleOf(DataType):
return tuple(sub(elem)
for sub, elem in zip(self.members, value))
except Exception as exc:
raise BadValueError('Can not validate:', str(exc))
raise BadValueError('Can not validate:', str(exc)) from None
def export_value(self, value):
"""returns a python object fit for serialisation"""
@ -894,7 +894,7 @@ class StructOf(DataType):
return ImmutableDict((str(k), self.members[k](v))
for k, v in list(value.items()))
except Exception as exc:
raise BadValueError('Can not validate %s: %s' % (repr(value), str(exc)))
raise BadValueError('Can not validate %s: %s' % (repr(value), str(exc))) from None
def export_value(self, value):
"""returns a python object fit for serialisation"""
@ -924,7 +924,7 @@ class StructOf(DataType):
if mandatory:
raise BadValueError('incompatible datatypes')
except (AttributeError, TypeError, KeyError):
raise BadValueError('incompatible datatypes')
raise BadValueError('incompatible datatypes') from None
class CommandType(DataType):
@ -958,7 +958,7 @@ class CommandType(DataType):
argstr = repr(self.argument) if self.argument else ''
if self.result is None:
return 'CommandType(%s)' % argstr
return 'CommandType(%s)->%s' % (argstr, repr(self.result))
return 'CommandType(%s, %s)' % (argstr, repr(self.result))
def __call__(self, value):
"""return the validated argument value or raise"""
@ -987,7 +987,7 @@ class CommandType(DataType):
if self.result != other.result: # not both are None
other.result.compatible(self.result)
except AttributeError:
raise BadValueError('incompatible datatypes')
raise BadValueError('incompatible datatypes') from None
# internally used datatypes (i.e. only for programming the SEC-node)
@ -999,7 +999,10 @@ class DataTypeType(DataType):
returns the value or raises an appropriate exception"""
if isinstance(value, DataType):
return value
raise ProgrammingError('%r should be a DataType!' % value)
try:
return get_datatype(value)
except Exception as e:
raise ProgrammingError(e) from None
def export_value(self, value):
"""if needed, reformat value for transport"""
@ -1034,6 +1037,13 @@ class ValueType(DataType):
"""
raise NotImplementedError
def setProperty(self, key, value):
"""silently ignored
as ValueType is used for the datatype default, this makes code
shorter for cases, where the datatype may not yet be defined
"""
class NoneOr(DataType):
"""validates a None or smth. else"""
@ -1080,7 +1090,7 @@ UInt64 = IntRange(0, (1 << 64) - 1)
# Goodie: Convenience Datatypes for Programming
class LimitsType(TupleOf):
def __init__(self, members):
TupleOf.__init__(self, members, members)
super().__init__(members, members)
def __call__(self, value):
limits = TupleOf.__call__(self, value)
@ -1092,7 +1102,7 @@ class LimitsType(TupleOf):
class StatusType(TupleOf):
# shorten initialisation and allow access to status enumMembers from status values
def __init__(self, enum):
TupleOf.__init__(self, EnumType(enum), StringType())
super().__init__(EnumType(enum), StringType())
self._enum = enum
def __getattr__(self, key):
@ -1164,9 +1174,9 @@ def get_datatype(json, pname=''):
kwargs = json.copy()
base = kwargs.pop('type')
except (TypeError, KeyError, AttributeError):
raise BadValueError('a data descriptor must be a dict containing a "type" key, not %r' % json)
raise BadValueError('a data descriptor must be a dict containing a "type" key, not %r' % json) from None
try:
return DATATYPES[base](pname=pname, **kwargs)
except Exception as e:
raise BadValueError('invalid data descriptor: %r (%s)' % (json, str(e)))
raise BadValueError('invalid data descriptor: %r (%s)' % (json, str(e))) from None

View File

@ -25,9 +25,10 @@
import sys
import time
from collections import OrderedDict
from secop.datatypes import ArrayOf, BoolType, EnumType, FloatRange, \
IntRange, StatusType, StringType, TextType, TupleOf, get_datatype
IntRange, StatusType, StringType, TextType, TupleOf
from secop.errors import BadValueError, ConfigError, InternalError, \
ProgrammingError, SECoPError, SilentError, secop_error
from secop.lib import formatException, mkthread
@ -40,7 +41,7 @@ Done = object() #: a special return value for a read/write function indicating
class HasAccessibles(HasProperties):
"""base class of module
"""base class of Module
joining the class's properties, parameters and commands dicts with
those of base classes.
@ -52,40 +53,43 @@ class HasAccessibles(HasProperties):
super().__init_subclass__()
# merge accessibles from all sub-classes, treat overrides
# for now, allow to use also the old syntax (parameters/commands dict)
accessibles = {}
for base in reversed(cls.__bases__):
accessibles.update(getattr(base, 'accessibles', {}))
newaccessibles = {k: v for k, v in cls.__dict__.items() if isinstance(v, Accessible)}
for aname, aobj in list(accessibles.items()):
value = getattr(cls, aname, None)
if not isinstance(value, Accessible): # else override is already done in __set_name__
if value is None:
accessibles.pop(aname)
else:
# this is either a method overwriting a command
# or a value overwriting a property value or parameter default
anew = aobj.override(value)
newaccessibles[aname] = anew
setattr(cls, aname, anew)
anew.__set_name__(cls, aname)
ordered = {}
for aname in cls.__dict__.get('paramOrder', ()):
accessibles = OrderedDict() # dict of accessibles
merged_properties = {} # dict of dict of merged properties
new_names = [] # list of names of new accessibles
for base in reversed(cls.__mro__):
for key, value in base.__dict__.items():
if isinstance(value, Accessible):
value.updateProperties(merged_properties.setdefault(key, {}))
if base == cls and key not in accessibles:
new_names.append(key)
accessibles[key] = value
elif key in accessibles:
# either a bare value overriding a parameter
# or a method overriding a command
aobj = aobj.copy()
aobj.override(value)
accessibles[key] = aobj
for aname, aobj in accessibles.items():
if aobj != getattr(cls, aname, None):
aobj = aobj.copy()
setattr(cls, aname, aobj)
aobj.merge(merged_properties[aname])
accessibles[aname] = aobj
# rebuild order: (1) inherited items, (2) items from paramOrder, (3) new accessibles
# move (2) to the end
for aname in list(cls.__dict__.get('paramOrder', ())):
if aname in accessibles:
ordered[aname] = accessibles.pop(aname)
elif aname in newaccessibles:
ordered[aname] = newaccessibles.pop(aname)
# ignore unknown names
# starting from old accessibles not mentioned, append items from 'order'
accessibles.update(ordered)
# then new accessibles not mentioned
accessibles.update(newaccessibles)
accessibles.move_to_end(aname)
# ignore unknown names
# move (3) to the end
for aname in new_names:
accessibles.move_to_end(aname)
# note: for python < 3.6 the order of inherited items is not ensured between
# declarations within the same class
cls.accessibles = accessibles
# Correct naming of EnumTypes
for k, v in accessibles.items():
if isinstance(v, Parameter) and isinstance(v.datatype, EnumType):
v.datatype.set_name(k)
# moved to Parameter.__set_name__
# check validity of Parameter entries
for pname, pobj in accessibles.items():
@ -318,8 +322,9 @@ class Module(HasAccessibles):
paramobj = self.accessibles.get(paramname, None)
# paramobj might also be a command (not sure if this is needed)
if paramobj:
if propname == 'datatype':
propvalue = get_datatype(propvalue, k)
# no longer needed, this conversion is done by DataTypeType.__call__:
# if propname == 'datatype':
# propvalue = get_datatype(propvalue, k)
try:
paramobj.setProperty(propname, propvalue)
except KeyError:
@ -347,6 +352,10 @@ class Module(HasAccessibles):
self.valueCallbacks[pname] = []
self.errorCallbacks[pname] = []
if not pobj.hasDatatype():
errors.append('%s needs a datatype' % pname)
continue
if pname in cfgdict:
if not pobj.readonly and pobj.initwrite is not False:
# parameters given in cfgdict have to call write_<pname>
@ -393,11 +402,15 @@ class Module(HasAccessibles):
cfgdict.pop(k)
except (ValueError, TypeError) as e:
# self.log.exception(formatExtendedStack())
errors.append('module %s, parameter %s: %s' % (self.name, k, e))
errors.append('parameter %s: %s' % (k, e))
# ensure consistency
for aobj in self.accessibles.values():
aobj.finish()
# Modify units AFTER applying the cfgdict
for k, v in self.parameters.items():
dt = v.datatype
for pname, pobj in self.parameters.items():
dt = pobj.datatype
if '$' in dt.unit:
dt.setProperty('unit', dt.unit.replace('$', self.parameters['value'].datatype.unit))
@ -410,7 +423,7 @@ class Module(HasAccessibles):
for pname, p in self.parameters.items():
try:
p.checkProperties()
except ConfigError:
except ConfigError as e:
errors.append('%s: %s' % (pname, e))
if errors:
raise ConfigError(errors)
@ -426,7 +439,7 @@ class Module(HasAccessibles):
"""announce a changed value or readerror"""
pobj = self.parameters[pname]
timestamp = timestamp or time.time()
changed = pobj.value != value or timestamp > (pobj.timestamp or 0) + 1
changed = pobj.value != value
if value is not None:
pobj.value = value # store the value even in case of error
if err:
@ -439,8 +452,10 @@ class Module(HasAccessibles):
pobj.value = pobj.datatype(value)
except Exception as e:
err = secop_error(e)
if not changed:
return # experimental: do not update unchanged values within 1 sec
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
if pobj.export:

View File

@ -35,9 +35,17 @@ UNSET = object() # an argument not given, not even None
class Accessible(HasProperties):
"""base class for Parameter and Command"""
"""base class for Parameter and Command
kwds = None # is a dict if it might be used as Override
Inheritance mechanism:
param.propertyValues contains the properties, which will be used when the
owner class will be instantiated
param.ownProperties contains the properties to be used for inheritance
"""
ownProperties = None
def init(self, kwds):
# do not use self.propertyValues.update here, as no invalid values should be
@ -45,43 +53,47 @@ class Accessible(HasProperties):
for k, v in kwds.items():
self.setProperty(k, v)
def inherit(self, cls, owner):
for base in owner.__bases__:
if hasattr(base, self.name):
aobj = getattr(base, 'accessibles', {}).get(self.name)
if aobj:
if not isinstance(aobj, cls):
raise ProgrammingError('%s %s.%s can not inherit from a %s' %
(cls.__name__, owner.__name__, self.name, aobj.__class__.__name__))
# inherit from aobj
for pname, value in aobj.propertyValues.items():
if pname not in self.propertyValues:
self.propertyValues[pname] = value
break
def as_dict(self):
return self.propertyValues
def override(self, value=UNSET, **kwds):
"""return a copy, overridden by a bare attribute
and/or some properties"""
def override(self, value):
"""override with a bare value"""
raise NotImplementedError
def copy(self):
"""return a (deep) copy of ourselfs"""
def copy(self, **kwds):
"""return a (deep) copy of ourselfs
:param kwds: override given properties
"""
raise NotImplementedError
def updateProperties(self, merged_properties):
"""update merged_properties with our own properties"""
raise NotImplementedError
def merge(self, merged_properties):
"""merge with inherited properties
:param merged_properties: dict of properties to be updated
note: merged_properties may be modified
"""
raise NotImplementedError
def finish(self):
"""ensure consistency"""
raise NotImplementedError
def for_export(self):
"""prepare for serialisation"""
raise NotImplementedError
def hasDatatype(self):
return 'datatype' in self.propertyValues
def __repr__(self):
props = []
for k, prop in sorted(self.propertyDict.items()):
v = self.propertyValues.get(k, prop.default)
if v != prop.default:
props.append('%s=%r' % (k, v))
for k, v in sorted(self.propertyValues.items()):
props.append('%s=%r' % (k, v))
return '%s(%s)' % (self.__class__.__name__, ', '.join(props))
@ -100,7 +112,7 @@ class Parameter(Accessible):
extname='description', mandatory=True, export='always')
datatype = Property(
'datatype of the Parameter (SECoP datainfo)', DataTypeType(),
extname='datainfo', mandatory=True, export='always')
extname='datainfo', mandatory=True, export='always', default=ValueType())
readonly = Property(
'not changeable via SECoP (default True)', BoolType(),
extname='readonly', default=True, export='always')
@ -160,9 +172,13 @@ class Parameter(Accessible):
timestamp = 0
readerror = None
def __init__(self, description=None, datatype=None, inherit=True, *, unit=None, constant=None, **kwds):
def __init__(self, description=None, datatype=None, inherit=True, **kwds):
super().__init__()
if datatype is not None:
if datatype is None:
# collect datatype properties. these are not applied, as we have no datatype
self.ownProperties = {k: kwds.pop(k) for k in list(kwds) if k not in self.propertyDict}
else:
self.ownProperties = {}
if not isinstance(datatype, DataType):
if isinstance(datatype, type) and issubclass(datatype, DataType):
# goodie: make an instance from a class (forgotten ()???)
@ -174,15 +190,15 @@ class Parameter(Accessible):
if 'default' in kwds:
self.default = datatype(kwds['default'])
self.init(kwds) # datatype must be defined before we can treat dataset properties like fmtstr or unit
if description is not None:
self.description = inspect.cleandoc(description)
kwds['description'] = inspect.cleandoc(description)
# save for __set_name__
self._inherit = inherit
self._unit = unit # for legacy code only
self._constant = constant
self.init(kwds)
if inherit:
self.ownProperties.update(self.propertyValues)
else:
self.ownProperties = {k: getattr(self, k) for k in self.propertyDict}
def __get__(self, instance, owner):
# not used yet
@ -195,57 +211,74 @@ class Parameter(Accessible):
def __set_name__(self, owner, name):
self.name = name
if isinstance(self.datatype, EnumType):
self.datatype.set_name(name)
if self._inherit:
self.inherit(Parameter, owner)
if self.export is True:
predefined_cls = PREDEFINED_ACCESSIBLES.get(self.name, None)
if predefined_cls is Parameter:
self.export = self.name
elif predefined_cls is None:
self.export = '_' + self.name
else:
raise ProgrammingError('can not use %r as name of a Parameter' % self.name)
# check for completeness
missing_properties = [pname for pname in ('description', 'datatype') if pname not in self.propertyValues]
if missing_properties:
raise ProgrammingError('Parameter %s.%s needs a %s' %
(owner.__name__, name, ' and a '.join(missing_properties)))
if self._unit is not None:
self.datatype.setProperty('unit', self._unit)
def copy(self, **kwds):
"""return a (deep) copy of ourselfs
if self._constant is not None:
constant = self.datatype(self._constant)
:param kwds: override given properties
"""
res = type(self)()
res.name = self.name
res.init(self.propertyValues)
res.init(kwds)
if 'datatype' in self.propertyValues:
res.datatype = res.datatype.copy()
return res
def updateProperties(self, merged_properties):
"""update merged_properties with our own properties"""
datatype = self.ownProperties.get('datatype')
if datatype is not None:
# clear datatype properties, as they are overriden by datatype
for key in list(merged_properties):
if key not in self.propertyDict:
merged_properties.pop(key)
merged_properties.update(self.ownProperties)
def override(self, value):
"""override default"""
self.default = self.datatype(value)
def merge(self, merged_properties):
"""merge with inherited properties
:param merged_properties: dict of properties to be updated
note: merged_properties may be modified
"""
datatype = merged_properties.pop('datatype', None)
if datatype is not None:
self.datatype = datatype.copy()
self.init(merged_properties)
self.finish()
def finish(self):
"""ensure consistency"""
if self.constant is not None:
constant = self.datatype(self.constant)
# The value of the `constant` property should be the
# serialised version of the constant, or unset
self.constant = self.datatype.export_value(constant)
self.readonly = True
if 'default' in self.propertyValues:
# fixes in case datatype has changed
try:
self.datatype(self.default)
self.default = self.datatype(self.default)
except BadValueError:
# clear default, if it does not match datatype
self.propertyValues.pop('default')
if self.export is True:
predefined_cls = PREDEFINED_ACCESSIBLES.get(name, None)
if predefined_cls is Parameter:
self.export = name
elif predefined_cls is None:
self.export = '_' + name
else:
raise ProgrammingError('can not use %r as name of a Parameter' % name)
def copy(self):
# deep copy, as datatype might be altered from config
res = type(self)()
res.name = self.name
res.init(self.propertyValues)
res.datatype = res.datatype.copy()
return res
def override(self, value=UNSET, **kwds):
res = self.copy()
res.init(kwds)
if value is not UNSET:
res.value = res.datatype(value)
return res
def export_value(self):
return self.datatype.export_value(self.value)
@ -255,15 +288,23 @@ class Parameter(Accessible):
def getProperties(self):
"""get also properties of datatype"""
super_prop = super().getProperties().copy()
super_prop.update(self.datatype.getProperties())
if self.datatype:
super_prop.update(self.datatype.getProperties())
return super_prop
def setProperty(self, key, value):
"""set also properties of datatype"""
if key in self.propertyDict:
super().setProperty(key, value)
else:
self.datatype.setProperty(key, value)
try:
if key in self.propertyDict:
super().setProperty(key, value)
else:
try:
self.datatype.setProperty(key, value)
except KeyError:
raise ProgrammingError('cannot set %s on parameter with datatype %s'
% (key, type(self.datatype).__name__)) from None
except ValueError as e:
raise ProgrammingError('property %s: %s' % (key, str(e))) from None
def checkProperties(self):
super().checkProperties()
@ -336,10 +377,9 @@ class Command(Accessible):
if self.func is None:
raise ProgrammingError('Command %s.%s must be used as a method decorator' %
(owner.__name__, name))
if self._inherit:
self.inherit(Command, owner)
self.datatype = CommandType(self.argument, self.result)
self.ownProperties = self.propertyValues.copy()
if self.export is True:
predefined_cls = PREDEFINED_ACCESSIBLES.get(name, None)
if predefined_cls is Command:
@ -347,39 +387,77 @@ class Command(Accessible):
elif predefined_cls is None:
self.export = '_' + name
else:
raise ProgrammingError('can not use %r as name of a Command' % name)
raise ProgrammingError('can not use %r as name of a Command' % name) from None
if not self._inherit:
for key, pobj in self.properties.items():
if key not in self.propertyValues:
self.propertyValues[key] = pobj.default
def __get__(self, obj, owner=None):
if obj is None:
return self
if not self.func:
raise ProgrammingError('Command %s not properly configured' % self.name)
raise ProgrammingError('Command %s not properly configured' % self.name) from None
return self.func.__get__(obj, owner)
def __call__(self, func):
"""called when used as decorator"""
if 'description' not in self.propertyValues and func.__doc__:
self.description = inspect.cleandoc(func.__doc__)
self.func = func
return self
def copy(self):
def copy(self, **kwds):
"""return a (deep) copy of ourselfs
:param kwds: override given properties
"""
res = type(self)()
res.name = self.name
res.func = self.func
res.init(self.propertyValues)
res.init(kwds)
if res.argument:
res.argument = res.argument.copy()
if res.result:
res.result = res.result.copy()
res.datatype = CommandType(res.argument, res.result)
self.finish()
return res
def override(self, value=UNSET, **kwds):
res = self.copy()
res.init(kwds)
if value is not UNSET:
res.func = value
return res
def updateProperties(self, merged_properties):
"""update merged_properties with our own properties"""
merged_properties.update(self.ownProperties)
def override(self, value):
"""override method
this is needed when the @Command is missing on a method overriding a command"""
if not callable(value):
raise ProgrammingError('%s = %r is overriding a Command' % (self.name, value))
self.func = value
def merge(self, merged_properties):
"""merge with inherited properties
:param merged_properties: dict of properties to be updated
"""
self.init(merged_properties)
self.finish()
def finish(self):
"""ensure consistency"""
self.datatype = CommandType(self.argument, self.result)
def setProperty(self, key, value):
"""special treatment of datatype"""
try:
if key == 'datatype':
command = DataTypeType()(value)
super().setProperty('argument', command.argument)
super().setProperty('result', command.result)
super().setProperty(key, value)
except ValueError as e:
raise ProgrammingError('property %s: %s' % (key, str(e))) from None
def do(self, module_obj, argument):
"""perform function call

View File

@ -61,6 +61,8 @@ def make_update(modulename, pobj):
class Dispatcher:
OMIT_UNCHANGED_WITHIN = 1 # do not send unchanged updates within 1 sec
def __init__(self, name, logger, options, srv):
# to avoid errors, we want to eat all options here
self.equipment_id = options.pop('id', name)

View File

@ -71,6 +71,8 @@ class Data:
class DispatcherStub:
OMIT_UNCHANGED_WITHIN = 0
def __init__(self, updates):
self.updates = updates
@ -106,7 +108,6 @@ def test_IOHandler():
group1 = Hdl('group1', 'SIMPLE?', '%g')
group2 = Hdl('group2', 'CMD?%(channel)d', '%g,%s,%d')
class Module1(Module):
channel = Property('the channel', IntRange(), default=3)
loop = Property('the loop', IntRange(), default=2)

View File

@ -26,14 +26,16 @@ import threading
import pytest
from secop.datatypes import BoolType, FloatRange, StringType
from secop.errors import ProgrammingError
from secop.modules import Communicator, Drivable, Module
from secop.datatypes import BoolType, FloatRange, StringType, IntRange, CommandType
from secop.errors import ProgrammingError, ConfigError
from secop.modules import Communicator, Drivable, Readable, Module
from secop.params import Command, Parameter
from secop.poller import BasicPoller
class DispatcherStub:
OMIT_UNCHANGED_WITHIN = 0
def __init__(self, updates):
self.updates = updates
@ -51,6 +53,9 @@ class LoggerStub:
info = warning = exception = debug
logger = LoggerStub()
class ServerStub:
def __init__(self, updates):
self.dispatcher = DispatcherStub(updates)
@ -125,7 +130,7 @@ def test_ModuleMagic():
value = Parameter(datatype=FloatRange(unit='deg'))
a1 = Parameter(datatype=FloatRange(unit='$/s'), readonly=False)
b2 = Parameter('<b2>', datatype=BoolType(), default=True,
poll=True, readonly=False, initwrite=True)
poll=True, readonly=False, initwrite=True)
def write_a1(self, value):
self._a1_written = value
@ -142,7 +147,6 @@ def test_ModuleMagic():
sortcheck2 = ['status', 'pollinterval', 'target', 'stop',
'a1', 'a2', 'cmd2', 'param1', 'param2', 'cmd', 'value', 'b2']
logger = LoggerStub()
updates = {}
srv = ServerStub(updates)
@ -228,3 +232,103 @@ def test_ModuleMagic():
o.earlyInit()
for o in objects:
o.initModule()
def test_param_inheritance():
srv = ServerStub({})
class Base(Module):
param = Parameter()
class MissingDatatype(Base):
param = Parameter('param')
class MissingDescription(Base):
param = Parameter(datatype=FloatRange(), default=0)
# missing datatype and/or description of a parameter has to be detected
# at instantation and only then
with pytest.raises(ConfigError) as e_info:
MissingDatatype('o', logger, {'description': ''}, srv)
assert 'datatype' in repr(e_info.value)
with pytest.raises(ConfigError) as e_info:
MissingDescription('o', logger, {'description': ''}, srv)
assert 'description' in repr(e_info.value)
with pytest.raises(ConfigError) as e_info:
Base('o', logger, {'description': ''}, srv)
def test_mixin():
# srv = ServerStub({})
class Mixin: # no need to inherit from Module or HasAccessible
value = Parameter(unit='K') # missing datatype and description acceptable in mixins
param1 = Parameter('no datatype yet', fmtstr='%.5f')
param2 = Parameter('no datatype yet', default=1)
class MixedReadable(Mixin, Readable):
pass
class MixedDrivable(MixedReadable, Drivable):
value = Parameter(unit='Ohm', fmtstr='%.3f')
param1 = Parameter(datatype=FloatRange())
with pytest.raises(ProgrammingError):
class MixedModule(Mixin):
param1 = Parameter('', FloatRange(), fmtstr=0) # fmtstr must be a string
assert repr(MixedDrivable.status.datatype) == repr(Drivable.status.datatype)
assert repr(MixedReadable.status.datatype) == repr(Readable.status.datatype)
assert MixedReadable.value.datatype.unit == 'K'
assert MixedDrivable.value.datatype.unit == 'Ohm'
assert MixedDrivable.value.datatype.fmtstr == '%.3f'
# when datatype is overridden, fmtstr falls back to default:
assert MixedDrivable.param1.datatype.fmtstr == '%g'
srv = ServerStub({})
MixedDrivable('o', logger, {
'description': '',
'param1.description': 'param 1',
'param1': 0,
'param2.datatype': {"type": "double"},
}, srv)
with pytest.raises(ConfigError):
MixedReadable('o', logger, {
'description': '',
'param1.description': 'param 1',
'param1': 0,
'param2.datatype': {"type": "double"},
}, srv)
def test_command_config():
class Mod(Module):
@Command(IntRange(0, 1), result=IntRange(0, 1))
def convert(self, value):
return value
srv = ServerStub({})
mod = Mod('o', logger, {
'description': '',
'convert.argument': {'type': 'bool'},
}, srv)
assert mod.commands['convert'].datatype.export_datatype() == {
'type': 'command',
'argument': {'type': 'bool'},
'result': {'type': 'int', 'min': 0, 'max': 1},
}
mod = Mod('o', logger, {
'description': '',
'convert.datatype': {'type': 'command', 'argument': {'type': 'bool'}, 'result': {'type': 'bool'}},
}, srv)
assert mod.commands['convert'].datatype.export_datatype() == {
'type': 'command',
'argument': {'type': 'bool'},
'result': {'type': 'bool'},
}

View File

@ -88,15 +88,18 @@ def test_Override():
p1 = Parameter(default=True)
p2 = Parameter() # override without change
assert Mod.p1 != Base.p1
assert Mod.p2 != Base.p2
assert Mod.p3 == Base.p3
assert id(Mod.p2) != id(Base.p2) # must be a new object
assert repr(Mod.p2) == repr(Base.p2) # but must be a clone
assert id(Mod.p1) != id(Base.p1)
assert id(Mod.p2) != id(Base.p2)
assert id(Mod.p3) == id(Base.p3)
assert repr(Mod.p2) == repr(Base.p2) # must be a clone
assert repr(Mod.p3) == repr(Base.p3) # must be a clone
assert Mod.p1.default == True
# manipulating default makes Base.p1 and Mod.p1 match
Mod.p1.default = False
assert repr(Mod.p1) == repr(Base.p1)
def test_Export():
class Mod:
class Mod(HasAccessibles):
param = Parameter('description1', datatype=BoolType, default=False)
assert Mod.param.export == '_param'

View File

@ -159,3 +159,26 @@ def test_Property_override():
a = 's'
assert 'can not set' in str(e.value)
def test_Properties_mro():
class A(HasProperties):
p = Property('base', StringType(), 'base', export='always')
class B(A):
pass
class C(A):
p = Property('sub', FloatRange(), extname='p')
class D(C, B):
p = 1
class E(B, C):
p = 2
assert B().exportProperties() == {'_p': 'base'}
assert D().exportProperties() == {'p': 1.0}
# in an older implementation the following would fail, as B.p is constructed first
# and then B.p overrides C.p
assert E().exportProperties() == {'p': 2.0}