removed old style syntax
- removed secop/metaclass.py - moved code from ModuleMeta to modules.HasAccessibles.__init_subclass__ - reworked properties: assignment obj.property = value now always allowed - reworked Parameters and Command to be true descriptors - Command must now be solely used as decorator - renamed 'usercommand' to 'Command' - command methods no longer start with 'do_' - reworked mechanism to determine accessible order: the attribute paramOrder, if given, determines order of accessibles + fixed some issues makeing the IDE more happy + simplified code for StatusType and added a test for it Change-Id: I8045cf38ee6f4d4862428272df0b12a7c8abaca7 Reviewed-on: https://forge.frm2.tum.de/review/c/sine2020/secop/playground/+/25049 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:
@ -23,27 +23,44 @@
|
||||
"""Define validated data types."""
|
||||
|
||||
|
||||
import sys
|
||||
import inspect
|
||||
from collections import OrderedDict
|
||||
|
||||
from secop.errors import ProgrammingError, ConfigError, BadValueError
|
||||
from secop.errors import ConfigError, ProgrammingError, BadValueError
|
||||
|
||||
|
||||
def flatten_dict(dictname, itemcls, attrs, remove=True):
|
||||
properties = {}
|
||||
# allow to declare properties directly as class attribute
|
||||
# all these attributes are removed
|
||||
for k, v in attrs.items():
|
||||
if isinstance(v, tuple) and v and isinstance(v[0], itemcls):
|
||||
# this might happen when migrating from old to new style
|
||||
raise ProgrammingError('declared %r with trailing comma' % k)
|
||||
if isinstance(v, itemcls):
|
||||
properties[k] = v
|
||||
if remove:
|
||||
for k in properties:
|
||||
attrs.pop(k)
|
||||
properties.update(attrs.get(dictname, {}))
|
||||
attrs[dictname] = properties
|
||||
class HasDescriptorMeta(type):
|
||||
def __new__(cls, name, bases, attrs):
|
||||
newtype = type.__new__(cls, name, bases, attrs)
|
||||
if sys.version_info < (3, 6):
|
||||
# support older python versions
|
||||
for key, attr in attrs.items():
|
||||
if hasattr(attr, '__set_name__'):
|
||||
attr.__set_name__(newtype, key)
|
||||
newtype.__init_subclass__()
|
||||
return newtype
|
||||
|
||||
|
||||
class HasDescriptors(metaclass=HasDescriptorMeta):
|
||||
@classmethod
|
||||
def __init_subclass__(cls):
|
||||
# when migrating old style declarations, sometimes the trailing comma is not removed
|
||||
bad = [k for k, v in cls.__dict__.items()
|
||||
if isinstance(v, tuple) and len(v) == 1 and hasattr(v[0], '__set_name__')]
|
||||
if bad:
|
||||
raise ProgrammingError('misplaced trailing comma after %s.%s' % (cls.__name__, '/'.join(bad)))
|
||||
|
||||
@classmethod
|
||||
def filterDescriptors(cls, filter_type):
|
||||
res = {}
|
||||
for name in dir(cls):
|
||||
desc = getattr(cls, name, None)
|
||||
if isinstance(desc, filter_type):
|
||||
res[name] = desc
|
||||
return res
|
||||
|
||||
|
||||
UNSET = object() # an unset value, not even None
|
||||
|
||||
|
||||
# storage for 'properties of a property'
|
||||
@ -56,7 +73,8 @@ class Property:
|
||||
:param default: a default value. SECoP properties are normally not sent to the ECS,
|
||||
when they match the default
|
||||
:param extname: external name
|
||||
:param export: sent to the ECS when True. defaults to True, when ``extname`` is given
|
||||
:param export: sent to the ECS when True. defaults to True, when ``extname`` is given.
|
||||
special value 'always': export also when matching the default
|
||||
:param mandatory: defaults to True, when ``default`` is not given. indicates that it must have a value
|
||||
assigned from the cfg file (or, in case of a module property, it may be assigned as a class attribute)
|
||||
:param settable: settable from the cfg file
|
||||
@ -64,148 +82,134 @@ class Property:
|
||||
|
||||
# note: this is intended to be used on base classes.
|
||||
# the VALUES of the properties are on the instances!
|
||||
def __init__(self, description, datatype, default=None, extname='', export=False, mandatory=None, settable=True):
|
||||
def __init__(self, description, datatype, default=UNSET, extname='', export=False, mandatory=None,
|
||||
settable=True, value=UNSET, name=''):
|
||||
if not callable(datatype):
|
||||
raise ValueError('datatype MUST be a valid DataType or a basic_validator')
|
||||
self.description = inspect.cleandoc(description)
|
||||
self.default = datatype.default if default is None else datatype(default)
|
||||
self.default = datatype.default if default is UNSET else datatype(default)
|
||||
self.datatype = datatype
|
||||
self.extname = extname
|
||||
self.export = export or bool(extname)
|
||||
if mandatory is None:
|
||||
mandatory = default is None
|
||||
mandatory = default is UNSET
|
||||
self.mandatory = mandatory
|
||||
self.settable = settable or mandatory # settable means settable from the cfg file
|
||||
self.value = UNSET if value is UNSET else datatype(value)
|
||||
self.name = name
|
||||
|
||||
def __get__(self, instance, owner):
|
||||
if instance is None:
|
||||
return self
|
||||
return instance.propertyValues.get(self.name, self.default)
|
||||
|
||||
def __set__(self, instance, value):
|
||||
instance.propertyValues[self.name] = self.datatype(value)
|
||||
|
||||
def __set_name__(self, owner, name):
|
||||
self.name = name
|
||||
if self.export and not self.extname:
|
||||
self.extname = '_' + name
|
||||
if self.description == '_':
|
||||
# the programmer indicates, that the name is already speaking for itself
|
||||
self.description = name.replace('_', ' ')
|
||||
|
||||
def __repr__(self):
|
||||
return 'Property(%r, %s, default=%r, extname=%r, export=%r, mandatory=%r, settable=%r)' % (
|
||||
self.description, self.datatype, self.default, self.extname, self.export,
|
||||
self.mandatory, self.settable)
|
||||
extras = ['default=%s' % repr(self.default)]
|
||||
if self.export:
|
||||
extras.append('extname=%r' % self.extname)
|
||||
extras.append('export=%r' % self.export)
|
||||
if self.mandatory:
|
||||
extras.append('mandatory=True')
|
||||
if not self.settable:
|
||||
extras.append('settable=False')
|
||||
if self.value is not UNSET:
|
||||
extras.append('value=%s' % repr(self.value))
|
||||
if not self.name:
|
||||
extras.append('name=%r' % self.name)
|
||||
return 'Property(%r, %s, %s)' % (self.description, self.datatype, ', '.join(extras))
|
||||
|
||||
|
||||
class Properties(OrderedDict):
|
||||
"""a collection of `Property` objects
|
||||
|
||||
checks values upon assignment.
|
||||
You can either assign a Property object, or a value
|
||||
(which must pass the validator of the already existing Property)
|
||||
"""
|
||||
def __setitem__(self, key, value):
|
||||
if not isinstance(value, Property):
|
||||
raise ProgrammingError('setting property %r on classes is not supported!' % key)
|
||||
# make sure, extname is valid if export is True
|
||||
if not value.extname and value.export:
|
||||
value.extname = '_%s' % key # generate custom key
|
||||
elif value.extname and not value.export:
|
||||
value.export = True
|
||||
OrderedDict.__setitem__(self, key, value)
|
||||
|
||||
def __delitem__(self, key):
|
||||
raise ProgrammingError('deleting Properties is not supported!')
|
||||
|
||||
|
||||
class PropertyMeta(type):
|
||||
"""Metaclass for HasProperties
|
||||
|
||||
joining the class's properties with those of base classes.
|
||||
"""
|
||||
|
||||
def __new__(cls, name, bases, attrs):
|
||||
newtype = type.__new__(cls, name, bases, attrs)
|
||||
if '__constructed__' in attrs:
|
||||
return newtype
|
||||
|
||||
flatten_dict('properties', Property, attrs)
|
||||
newtype = cls.__join_properties__(newtype, name, bases, attrs)
|
||||
|
||||
attrs['__constructed__'] = True
|
||||
return newtype
|
||||
|
||||
@classmethod
|
||||
def __join_properties__(cls, newtype, name, bases, attrs):
|
||||
# merge properties from all sub-classes
|
||||
properties = Properties()
|
||||
for base in reversed(bases):
|
||||
properties.update(getattr(base, "properties", {}))
|
||||
# update with properties from new class
|
||||
properties.update(attrs.get('properties', {}))
|
||||
newtype.properties = properties
|
||||
|
||||
# generate getters
|
||||
for k, po in properties.items():
|
||||
|
||||
def getter(self, pname=k):
|
||||
val = self.__class__.properties[pname].default
|
||||
return self.properties.get(pname, val)
|
||||
|
||||
if k in attrs and not isinstance(attrs[k], (property, Property)):
|
||||
if callable(attrs[k]):
|
||||
raise ProgrammingError('%r: property %r collides with method'
|
||||
% (newtype, k))
|
||||
# store the attribute value for putting on the instance later
|
||||
try:
|
||||
# for inheritance reasons, it seems best to store it as a renamed attribute
|
||||
setattr(newtype, '_initProp_' + k, po.datatype(attrs[k]))
|
||||
except BadValueError:
|
||||
raise ProgrammingError('%r: property %r can not be set to %r'
|
||||
% (newtype, k, attrs[k]))
|
||||
setattr(newtype, k, property(getter))
|
||||
return newtype
|
||||
|
||||
|
||||
class HasProperties(metaclass=PropertyMeta):
|
||||
properties = {}
|
||||
class HasProperties(HasDescriptors):
|
||||
propertyValues = None
|
||||
|
||||
def __init__(self):
|
||||
super(HasProperties, self).__init__()
|
||||
self.initProperties()
|
||||
|
||||
def initProperties(self):
|
||||
# store property values in the instance, keep descriptors on the class
|
||||
self.properties = {}
|
||||
# pre-init with properties default value (if any)
|
||||
for pn, po in self.__class__.properties.items():
|
||||
value = getattr(self, '_initProp_' + pn, self)
|
||||
if value is not self: # property value was given as attribute
|
||||
self.properties[pn] = value
|
||||
elif not po.mandatory:
|
||||
self.properties[pn] = po.default
|
||||
self.propertyValues = {}
|
||||
# pre-init
|
||||
for pn, po in self.propertyDict.items():
|
||||
if po.value is not UNSET:
|
||||
self.setProperty(pn, po.value)
|
||||
|
||||
@classmethod
|
||||
def __init_subclass__(cls):
|
||||
super().__init_subclass__()
|
||||
# raise an error when an attribute is a tuple with one single descriptor as element
|
||||
# when migrating old style declarations, sometimes the trailing comma is not removed
|
||||
bad = [k for k, v in cls.__dict__.items()
|
||||
if isinstance(v, tuple) and len(v) == 1 and hasattr(v[0], '__set_name__')]
|
||||
if bad:
|
||||
raise ProgrammingError('misplaced trailing comma after %s.%s' % (cls.__name__, '/'.join(bad)))
|
||||
properties = {}
|
||||
for base in cls.__bases__:
|
||||
properties.update(getattr(base, 'propertyDict', {}))
|
||||
properties.update(cls.filterDescriptors(Property))
|
||||
cls.propertyDict = properties
|
||||
# treat overriding properties with bare values
|
||||
for pn, po in properties.items():
|
||||
value = cls.__dict__.get(pn, po)
|
||||
if not isinstance(value, Property): # attribute is a bare value
|
||||
po = Property(**po.__dict__)
|
||||
try:
|
||||
po.value = po.datatype(value)
|
||||
except BadValueError:
|
||||
for base in cls.__bases__:
|
||||
if pn in getattr(base, 'propertyDict', {}):
|
||||
if callable(value):
|
||||
raise ProgrammingError('method %s.%s collides with property of %s' %
|
||||
(cls.__name__, pn, base.__name__))
|
||||
raise ProgrammingError('can not set property %s.%s to %r' %
|
||||
(cls.__name__, pn, value))
|
||||
cls.propertyDict[pn] = po
|
||||
|
||||
def checkProperties(self):
|
||||
"""validates properties and checks for min... <= max..."""
|
||||
for pn, po in self.__class__.properties.items():
|
||||
if po.export and po.mandatory:
|
||||
if pn not in self.properties:
|
||||
for pn, po in self.propertyDict.items():
|
||||
if po.mandatory:
|
||||
if pn not in self.propertyDict:
|
||||
name = getattr(self, 'name', self.__class__.__name__)
|
||||
raise ConfigError('Property %r of %s needs a value of type %r!' % (pn, name, po.datatype))
|
||||
# apply validator (which may complain further)
|
||||
self.properties[pn] = po.datatype(self.properties[pn])
|
||||
for pn, po in self.__class__.properties.items():
|
||||
self.propertyValues[pn] = po.datatype(self.propertyValues[pn])
|
||||
for pn, po in self.propertyDict.items():
|
||||
if pn.startswith('min'):
|
||||
maxname = 'max' + pn[3:]
|
||||
minval = self.properties[pn]
|
||||
maxval = self.properties.get(maxname, minval)
|
||||
minval = self.propertyValues.get(pn, po.default)
|
||||
maxval = self.propertyValues.get(maxname, minval)
|
||||
if minval > maxval:
|
||||
raise ConfigError('%s=%r must be <= %s=%r for %r' % (pn, minval, maxname, maxval, self))
|
||||
|
||||
|
||||
def getProperties(self):
|
||||
return self.__class__.properties
|
||||
return self.propertyDict
|
||||
|
||||
def exportProperties(self):
|
||||
# export properties which have
|
||||
# export=True and
|
||||
# mandatory=True or non_default=True
|
||||
res = {}
|
||||
for pn, po in self.__class__.properties.items():
|
||||
val = self.properties.get(pn, None)
|
||||
if po.export and (po.mandatory or val != po.default):
|
||||
for pn, po in self.propertyDict.items():
|
||||
val = self.propertyValues.get(pn, po.default)
|
||||
if po.export and (po.export == 'always' or val != po.default):
|
||||
try:
|
||||
val = po.datatype.export_value(val)
|
||||
except AttributeError:
|
||||
pass # for properties, accept simple datatypes without export_value
|
||||
pass # for properties, accept simple datatypes without export_value
|
||||
res[po.extname] = val
|
||||
return res
|
||||
|
||||
def setProperty(self, key, value):
|
||||
self.properties[key] = self.__class__.properties[key].datatype(value)
|
||||
# this is overwritten by Param.setProperty and DataType.setProperty
|
||||
# in oder to extend setting to inner properties
|
||||
# otherwise direct setting of self.<key> = value is preferred
|
||||
self.propertyValues[key] = self.propertyDict[key].datatype(value)
|
||||
|
Reference in New Issue
Block a user