Source code for spawn.util.property

# spawn
# Copyright (C) 2018-2019, Simmovation Ltd.
#
# 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 3 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301  USA
"""Property class attributes to allow validation and type checking
"""
import re
import copy
import functools

from spawn.util.validation import validate_type

def _not_implemented_message(obj, name, problem):
    return "'{}' {} in '{}'".format(name, problem, obj.__class__.__name__)

[docs]def typed_property(type_): """Function decorator for :class:`TypedProperty` """ def _wrapper(fget): return TypedProperty(type_, fget) return _wrapper
[docs]def int_property(fget): """Function decorator for :class:`IntProperty` """ return IntProperty(fget)
[docs]def float_property(fget): """Function decorator for :class:`FloatProperty` """ return FloatProperty(fget)
[docs]def string_property(fget): """Function decorator for :class:`StringProperty` """ return StringProperty(fget)
class PropertyBase: """Base class for properties """ def __init__( self, type_, fget=None, fset=None, fdel=None, fvalidate=None, default=None, doc=None, abstract=False, readonly=False): """Initialises :class:`PropertyBase` :param fget: Getter function for property :type fget: func :param fset: Setter function for property :type fset: func :param fdel: Deleter function for property :type fdel: func :param fvalidate: Validation function for property :type fvalidate: func :param default: The default value for this property :type default: object :param doc: The docstring for this property :type doc: str :param abstract: ``True`` if this property is abstract (requires implementation); ``False`` otherwise. :type abstract: bool """ self._type = type_ self._fget = fget self._fset = fset self._fdel = fdel self._fvalidate = fvalidate self._default = default self.__doc__ = doc if doc is not None else fget.__doc__ if fget is not None else None self._name = None self._abstract = abstract self._readonly = readonly def __set_name__(self, _obj, name): self._name = name @property def type(self): """The type of this property :returns: The type :rtype: type """ return self._type def getter(self, fget): """Acts as a function decorator to provide a :class:`TypedProperty` with a getter :param fget: The getter function :type fget: func """ if self._fget is not None: raise ValueError('Cannot set fget more than once') other = copy.copy(self) #pylint: disable=protected-access other._fget = fget return other def setter(self, fset): """Acts as a function decorator to provide a :class:`TypedProperty` with a setter :param fset: The setter function :type fset: func """ if self._fset is not None: raise ValueError('Cannot set fset more than once') if self._fget is None: raise ValueError('Must set getter before setting fset') other = copy.copy(self) #pylint: disable=protected-access other._fset = fset return other def deleter(self, fdel): """Acts as a function decorator to provide a :class:`TypedProperty` with a deleter :param fdel: The deleter function :type fdel: func """ if self._fdel is not None: raise ValueError('Cannot set fdel more than once') if self._fget is None or self._fset is None: raise ValueError('Must set getter and setter before setting fdel') other = copy.copy(self) #pylint: disable=protected-access other._fdel = fdel return other def validator(self, fvalidate): """Acts as a function decorator to provide a :class:`TypedProperty` with a validator :param fvalidate: The validator function :type fvalidate: func """ other = copy.copy(self) #pylint: disable=protected-access other._fvalidate = fvalidate return other def _get_fname(self, function): return '{}_{}'.format(function, self._name) def __call__(self, fget): return self.getter(fget)
[docs]class TypedProperty(PropertyBase): """Base class for typed properties """ def __get__(self, obj, _type=None): if obj is None: return self if self._name is None: raise ValueError('Cannot get property, name has not been set') if hasattr(obj, self._get_fname('get')): return getattr(obj, self._get_fname('get'))() if self._fget: return self._fget(obj) if self._abstract: raise NotImplementedError(_not_implemented_message(obj, self._name, "not implemented (abstract)")) return obj.__dict__.get(self._name, self._default) def __set__(self, obj, value): if obj is None: return if self._name is None: raise ValueError('Cannot set property, name has not been set') if self._readonly: raise NotImplementedError(_not_implemented_message(obj, self._name, "cannot be set because it's read-only")) self._validate(obj, value) if hasattr(obj, self._get_fname('validate')): getattr(obj, self._get_fname('validate'))(value) elif self._fvalidate: self._fvalidate(obj, value) if hasattr(obj, self._get_fname('set')): getattr(obj, self._get_fname('set'))(value) elif self._fset: self._fset(obj, value) elif self._abstract: raise NotImplementedError(_not_implemented_message(obj, self._name, "not implemented (abstract)")) else: obj.__dict__[self._name] = value def __delete__(self, obj): if self._name is None: raise ValueError('Cannot delete property, name has not been set') if hasattr(obj, self._get_fname('delete')): getattr(obj, self._get_fname('delete'))() elif self._fdel: self._fdel(obj) elif self._abstract: raise NotImplementedError(_not_implemented_message(obj, self._name, "not implemented (abstract)")) else: del obj.__dict__[self._name] def _validate(self, _obj, value): if not isinstance(value, self._type): raise TypeError('value')
class NumericProperty(TypedProperty): """Implementation of :class:`TypedProperty` for numeric (int or float) properties Adds min and max options """ #pylint: disable=redefined-builtin def __init__( self, type_, fget=None, fset=None, fdel=None, fvalidate=None, default=None, doc=None, abstract=False, readonly=False, min=None, max=None): """Initialises :class:`NumericProperty` :param fget: Getter function for property :type fget: func :param fset: Setter function for property :type fset: func :param fdel: Deleter function for property :type fdel: func :param fvalidate: Validation function for property :type fvalidate: func :param default: The default value for this property :type default: numeric :param doc: The docstring for this property :type doc: str :param abstract: ``True`` if this property is abstract (requires implementation); ``False`` otherwise. :type abstract: bool :param min: Minimum allowed value for this property :type min: numeric :param max: Maximum allowed value for this property :type max: numeric """ super().__init__(type_, fget, fset, fdel, fvalidate, default, doc, abstract, readonly) if min is not None and not isinstance(min, type_): raise TypeError('min') if max is not None and not isinstance(max, type_): raise TypeError('max') self._min = min self._max = max def _validate(self, obj, value): super()._validate(obj, value) if self._min is not None and value < self._min: raise ValueError('{} < {}'.format(value, self._min)) if self._max is not None and value > self._max: raise ValueError('{} > {}'.format(value, self._max))
[docs]class IntProperty(NumericProperty): """Implementation of :class:`NumericProperty` for int properties """ #pylint: disable=redefined-builtin def __init__( self, fget=None, fset=None, fdel=None, fvalidate=None, default=None, doc=None, abstract=False, readonly=False, min=None, max=None): """Initialises :class:`IntProperty` :param fget: Getter function for property :type fget: func :param fset: Setter function for property :type fset: func :param fdel: Deleter function for property :type fdel: func :param fvalidate: Validation function for property :type fvalidate: func :param default: The default value for this property :type default: int :param doc: The docstring for this property :type doc: str :param abstract: ``True`` if this property is abstract (requires implementation); ``False`` otherwise. :type abstract: bool :param min: Minimum allowed value for this property :type min: int :param max: Maximum allowed value for this property :type max: int """ super().__init__( int, fget, fset, fdel, fvalidate, default, doc, abstract, readonly, min, max )
[docs]class FloatProperty(NumericProperty): """Implementation of :class:`NumericProperty` for float properties """ #pylint: disable=redefined-builtin def __init__( self, fget=None, fset=None, fdel=None, fvalidate=None, default=None, doc=None, abstract=False, readonly=False, min=None, max=None): """Initialises :class:`FloatProperty` :param fget: Getter function for property :type fget: func :param fset: Setter function for property :type fset: func :param fdel: Deleter function for property :type fdel: func :param fvalidate: Validation function for property :type fvalidate: func :param default: The default value for this property :type default: float :param doc: The docstring for this property :type doc: str :param abstract: ``True`` if this property is abstract (requires implementation); ``False`` otherwise. :type abstract: bool :param min: Minimum allowed value for this property :type min: float :param max: Maximum allowed value for this property :type max: float """ super().__init__( float, fget, fset, fdel, fvalidate, default, doc, abstract, readonly, min, max )
[docs]class StringProperty(TypedProperty): """Implementation of :class:`TypedProperty` for string properties """ def __init__( self, fget=None, fset=None, fdel=None, fvalidate=None, default=None, doc=None, abstract=False, readonly=False, possible_values=None, regex=None): """Initialises :class:`StringProperty` :param fget: Getter function for property :type fget: func :param fset: Setter function for property :type fset: func :param fdel: Deleter function for property :type fdel: func :param fvalidate: Validation function for property :type fvalidate: func :param default: The default value for this property :type default: str :param doc: The docstring for this property :type doc: str :param abstract: ``True`` if this property is abstract (requires implementation); ``False`` otherwise. :type abstract: bool :param possible_values: Array of possible values for this property :type possible_values: list :param regex: Regex that this property must match :type regex: str """ super().__init__(str, fget, fset, fdel, fvalidate, default, doc, abstract, readonly) self._possible_values = possible_values self._regex = regex def _validate(self, obj, value): super()._validate(obj, value) if self._possible_values is not None and value not in self._possible_values: raise ValueError('"{}" not in {}'.format(value, self._possible_values)) if self._regex is not None and not re.search(self._regex, value): raise ValueError('"{}" does not match pattern "{}"'.format(value, self._regex))
[docs]class ArrayProperty(PropertyBase): """Implementation of :class:`PropertyBase` for array properties :meth:`__get__`, :meth:`__set__` and :meth:`__delete__` return array wrappers that allow indexes to be used """ def __get__(self, obj, _type=None): if obj is None: return self if self._name is None: raise ValueError('Cannot get property, name has not been set') return self._wrapper(obj) def __set__(self, obj, value): if obj is None: return if self._name is None: raise ValueError('Cannot set property, name has not been set') if self._readonly or self._abstract: raise NotImplementedError(_not_implemented_message(obj, self._name, "not implemented (abstract)")) validate_type(value, list, 'value') wrapper = self._wrapper(obj) for i, v in enumerate(value): wrapper[i] = v def __delete__(self, obj): if self._name is None: raise ValueError('Cannot delete property, name has not been set') if hasattr(obj, self._get_fname('delete')): getattr(obj, self._get_fname('delete'))() if self._abstract: raise NotImplementedError(_not_implemented_message(obj, self._name, "not implemented (abstract)")) if self._fget or self._fset or self._fdel: raise ValueError('Cannot delete array with custom getters and setters') del obj.__dict__[self._name] def _wrapper(self, obj): fget = functools.partial(self._fget, obj) if self._fget else self._get_method(obj, 'get') fset = functools.partial(self._fset, obj) if self._fset else self._get_method(obj, 'set') fdel = functools.partial(self._fdel, obj) if self._fdel else self._get_method(obj, 'delete') fvalidate = ( functools.partial(self._fvalidate, obj) if self._fvalidate else self._get_method(obj, 'validate') ) obj.__dict__.setdefault(self._name, []) return ArrayWrapper(self._type, fget, fset, fdel, fvalidate, obj.__dict__[self._name]) def _get_method(self, obj, name): if hasattr(obj, self._get_fname(name)): return getattr(obj, self._get_fname(name)) return None
class ArrayWrapper: """Wrapper for arrays that allows custom getters, setters, deleters and validators to be used """ def __init__(self, type_, fget=None, fset=None, fdel=None, fvalidate=None, store=None): """Initialises :class:`ArrayWrapper` :param type_: The type of array :type type_: type :param fget: The getter for the array :type fget: func :param fset: The setter for the array :type fset: func :param fdel: The deleter for the array :type fdel: func :param fvalidate: The validator for the array :type fvalidate: func :param store: The store for the array :type store: list """ validate_type(store, list, 'store') self._type = type_ self._fget = fget self._fset = fset self._fdel = fdel self._fvalidate = fvalidate self._store = store def __getitem__(self, index): validate_type(index, int, 'index') if self._fget: return self._fget(index) if self._store is not None: self._extend(index + 1) return self._store[index] raise ValueError('Could not get value for property, no store and no getter specified') def __setitem__(self, index, value): validate_type(index, int, 'index') validate_type(value, self._type, 'value') if self._fvalidate: self._fvalidate(index, value) if self._fset: self._fset(index, value) elif self._store is not None: self._extend(index + 1) self._store[index] = value else: raise ValueError('Could not set value for property, no store and no setter specified') def __delitem__(self, index): validate_type(index, int, 'index') if self._fdel: self._fdel(index) elif self._store is not None: self._extend(index + 1) del self._store[index] else: raise ValueError( 'Could not deleted index for property, no store and no setter specified' ) def _extend(self, length): if self._store is None: raise ValueError('store not defined for array') while len(self._store) < length: self._store.append(None)