New Syntax: - define properties and parameters as class attributes directly instead of items in class attribute dicts - define commands with decorator @usercommand(...) - old syntax is still supported for now still to do (with decreasing priority): - turn parameters into descriptors (vs. creating getters/setters) - migrate all existing code to new syntax - get rid of or reduce code in metaclasses using __set_name__ and __init_subclass__ instead, including a fix for allowing py < 3.6 Change-Id: Id47e0f89c506f50c40fa518b01822c6e5bbf4e98 Reviewed-on: https://forge.frm2.tum.de/review/c/sine2020/secop/playground/+/24991 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>
204 lines
8.2 KiB
Python
204 lines
8.2 KiB
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:
|
|
# Enrico Faulhaber <enrico.faulhaber@frm2.tum.de>
|
|
# Markus Zolliker <markus.zolliker@psi.ch>
|
|
#
|
|
# *****************************************************************************
|
|
"""Define validated data types."""
|
|
|
|
|
|
from collections import OrderedDict
|
|
|
|
from secop.errors import ProgrammingError, ConfigError, 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
|
|
|
|
|
|
# storage for 'properties of a property'
|
|
class Property:
|
|
'''base class holding info about a property
|
|
|
|
properties are only sent to the ECS if export is True, or an extname is set
|
|
if mandatory is True, they MUST have a value in the cfg file assigned to them.
|
|
otherwise, this is optional in which case the default value is applied.
|
|
All values MUST pass the datatype.
|
|
'''
|
|
# 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):
|
|
if not callable(datatype):
|
|
raise ValueError('datatype MUST be a valid DataType or a basic_validator')
|
|
self.description = description
|
|
self.default = datatype.default if default is None else datatype(default)
|
|
self.datatype = datatype
|
|
self.extname = extname
|
|
self.export = export or bool(extname)
|
|
if mandatory is None:
|
|
mandatory = default is None
|
|
self.mandatory = mandatory
|
|
self.settable = settable or mandatory # settable means settable from the cfg file
|
|
|
|
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)
|
|
|
|
|
|
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 = {}
|
|
|
|
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
|
|
|
|
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:
|
|
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():
|
|
if pn.startswith('min'):
|
|
maxname = 'max' + pn[3:]
|
|
minval = self.properties[pn]
|
|
maxval = self.properties.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
|
|
|
|
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):
|
|
try:
|
|
val = po.datatype.export_value(val)
|
|
except AttributeError:
|
|
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)
|