# -*- 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 # Markus Zolliker # # ***************************************************************************** """Define validated data types.""" from collections import OrderedDict from secop.errors import ProgrammingError, ConfigError, BadValueError # 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 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): 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', repr(self)) 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)