From 8e3dea845dc8abf36b43f0d584c65ae3b4266592 Mon Sep 17 00:00:00 2001 From: fwkoch Date: Thu, 20 Jul 2017 14:14:16 -0600 Subject: [PATCH 1/5] Accommodate numpy container/bool classes, if available --- properties/base/containers.py | 9 ++++++++- properties/basic.py | 9 ++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/properties/base/containers.py b/properties/base/containers.py index 6d1a38e..71dbc3d 100644 --- a/properties/base/containers.py +++ b/properties/base/containers.py @@ -19,6 +19,13 @@ else: CLASS_TYPES = (type,) +CONTAINERS = (list, tuple, set) +try: + import numpy as np + CONTAINERS += (np.ndarray,) +except ImportError: + pass + def add_properties_callbacks(cls): """Class decorator to add change notifications to builtin containers""" @@ -252,7 +259,7 @@ def validate(self, instance, value): """ if not self.coerce and not isinstance(value, self._class_default): self.error(instance, value) - if self.coerce and not isinstance(value, (list, tuple, set)): + if self.coerce and not isinstance(value, CONTAINERS): value = [value] out = [] for val in value: diff --git a/properties/basic.py b/properties/basic.py index 3bc54ea..47f4c12 100644 --- a/properties/basic.py +++ b/properties/basic.py @@ -19,6 +19,13 @@ TOL = 1e-9 +BOOLEAN_TYPES = (bool,) +try: + import numpy as np + BOOLEAN_TYPES += (np.bool_,) +except ImportError: + pass + PropertyTerms = collections.namedtuple( 'PropertyTerms', ('name', 'cls', 'args', 'kwargs', 'meta'), @@ -698,7 +705,7 @@ def validate(self, instance, value): value = bool(value) except ValueError: self.error(instance, value) - if not isinstance(value, bool): + if not isinstance(value, BOOLEAN_TYPES): self.error(instance, value) return value From 0b684482045661e82e00f21ec56d0b82d5b53a3b Mon Sep 17 00:00:00 2001 From: fwkoch Date: Thu, 20 Jul 2017 14:14:48 -0600 Subject: [PATCH 2/5] Add alias property types for math properties if numpy/vmath aren't available --- properties/math.py | 96 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 88 insertions(+), 8 deletions(-) diff --git a/properties/math.py b/properties/math.py index 2e9d600..a2a049f 100644 --- a/properties/math.py +++ b/properties/math.py @@ -4,11 +4,19 @@ from __future__ import print_function from __future__ import unicode_literals -import numpy as np +try: + import numpy as np +except ImportError: + np = None +try: + import vectormath as vmath +except ImportError: + vmath = None + from six import integer_types, string_types -import vectormath as vmath -from .basic import Property, TOL +from .base import List, Union +from .basic import Bool, Float, Integer, Property, TOL TYPE_MAPPINGS = { int: 'i', @@ -16,6 +24,12 @@ bool: 'b', } +PROP_MAPPINGS = { + int: Integer, + float: Float, + bool: Bool, +} + class Array(Property): """Property for :class:`numpy arrays ` @@ -56,6 +70,10 @@ def shape(self): @shape.setter def shape(self, value): + self._shape = self._validate_shape(value) + + @staticmethod + def _validate_shape(value): if not isinstance(value, tuple): raise TypeError("{}: Invalid shape - must be a tuple " "(e.g. ('*',3) for an array of length-3 " @@ -64,7 +82,7 @@ def shape(self, value): if shp != '*' and not isinstance(shp, integer_types): raise TypeError("{}: Invalid shape - values " "must be '*' or int".format(value)) - self._shape = value + return value @property def dtype(self): @@ -76,6 +94,10 @@ def dtype(self): @dtype.setter def dtype(self, value): + self._dtype = self._validate_dtype(value) + + @staticmethod + def _validate_dtype(value): if not isinstance(value, (list, tuple)): value = (value,) if not value: @@ -84,7 +106,7 @@ def dtype(self, value): if any([val not in TYPE_MAPPINGS for val in value]): raise TypeError('{}: Invalid dtype - must be int, float, ' 'and/or bool'.format(value)) - self._dtype = value + return value @property def info(self): @@ -102,10 +124,10 @@ def validate(self, instance, value): value = self.wrapper(value) if not isinstance(value, np.ndarray): raise NotImplementedError( - 'Array validation is only implmented for wrappers that are ' - 'subclasses of numpy.ndarray' + 'Array validation is only implemented for wrappers that are ' + 'subclasses of numpy.ndarray or list' ) - if value.dtype.kind not in (TYPE_MAPPINGS[typ] for typ in self.dtype): + if value.dtype.kind not in (TYPE_MAPPINGS[t] for t in self.dtype): self.error(instance, value) if len(self.shape) != value.ndim: self.error(instance, value) @@ -187,6 +209,28 @@ def _recurse_list(val): def from_json(value, **kwargs): return np.array(value).astype(float) + def __new__(cls, *args, **kwargs): + """If np not available, use equivalent List""" + if not np: + shape = cls._validate_shape(kwargs.pop('shape', ('*',))) + dtype = cls._validate_dtype(kwargs.pop('dtype', (float, int))) + kwargs['coerce'] = True + + def _get_list_prop(list_kw, ind=0): + if ind + 1 == len(shape): + list_kw['prop'] = Union( + doc='', + props=[PROP_MAPPINGS[t]('') for t in dtype], + ) + else: + list_kw['prop'] = _get_list_prop(kwargs.copy(), ind+1) + if shape[ind] != '*': + list_kw['min_length'] = list_kw['max_length'] = shape[ind] + return List(*args, **list_kw) + + return _get_list_prop(kwargs.copy()) + return cls.__init__(*args, **kwargs) + class BaseVector(Array): """Base class for Vector properties""" @@ -281,6 +325,15 @@ def validate(self, instance, value): def from_json(value, **kwargs): return vmath.Vector3(value) + def __new__(cls, *args, **kwargs): + """If vmath not available, use equivalent Array""" + if not vmath: + kwargs.pop('length', None) + kwargs['shape'] = (3,) + kwargs['dtype'] = (float,) + return Array(*args, **kwargs) + return cls.__init__(*args, **kwargs) + class Vector2(BaseVector): """Property for :class:`2D vectors ` @@ -329,6 +382,15 @@ def validate(self, instance, value): def from_json(value, **kwargs): return vmath.Vector2(value) + def __new__(cls, *args, **kwargs): + """If vmath not available, use equivalent Array""" + if not vmath: + kwargs.pop('length', None) + kwargs['shape'] = (2,) + kwargs['dtype'] = (float,) + return Array(*args, **kwargs) + return cls.__init__(*args, **kwargs) + class Vector3Array(BaseVector): """Property for an :class:`array of 3D vectors ` @@ -381,6 +443,15 @@ def validate(self, instance, value): def from_json(value, **kwargs): return vmath.Vector3Array(value) + def __new__(cls, *args, **kwargs): + """If vmath not available, use equivalent Array""" + if not vmath: + kwargs.pop('length', None) + kwargs['shape'] = ('*', 3) + kwargs['dtype'] = (float,) + return Array(*args, **kwargs) + return cls.__init__(*args, **kwargs) + class Vector2Array(BaseVector): """Property for an :class:`array of 2D vectors ` @@ -436,6 +507,15 @@ def validate(self, instance, value): def from_json(value, **kwargs): return vmath.Vector2Array(value) + def __new__(cls, *args, **kwargs): + """If vmath not available, use equivalent Array""" + if not vmath: + kwargs.pop('length', None) + kwargs['shape'] = ('*', 2) + kwargs['dtype'] = (float,) + return Array(*args, **kwargs) + return cls.__init__(*args, **kwargs) + VECTOR_DIRECTIONS = { 'ZERO': [0, 0, 0], From 46739213fe0cdb1af459c0136968998eaa1593fc Mon Sep 17 00:00:00 2001 From: fwkoch Date: Thu, 20 Jul 2017 14:46:01 -0600 Subject: [PATCH 3/5] Fix super calls in __new__ methods --- properties/math.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/properties/math.py b/properties/math.py index a2a049f..9cb0ce2 100644 --- a/properties/math.py +++ b/properties/math.py @@ -229,7 +229,7 @@ def _get_list_prop(list_kw, ind=0): return List(*args, **list_kw) return _get_list_prop(kwargs.copy()) - return cls.__init__(*args, **kwargs) + return super(Array, cls).__new__(cls, *args, **kwargs) class BaseVector(Array): @@ -332,7 +332,7 @@ def __new__(cls, *args, **kwargs): kwargs['shape'] = (3,) kwargs['dtype'] = (float,) return Array(*args, **kwargs) - return cls.__init__(*args, **kwargs) + return super(Vector3, cls).__new__(cls, *args, **kwargs) class Vector2(BaseVector): @@ -389,7 +389,7 @@ def __new__(cls, *args, **kwargs): kwargs['shape'] = (2,) kwargs['dtype'] = (float,) return Array(*args, **kwargs) - return cls.__init__(*args, **kwargs) + return super(Vector2, cls).__new__(cls, *args, **kwargs) class Vector3Array(BaseVector): @@ -450,7 +450,7 @@ def __new__(cls, *args, **kwargs): kwargs['shape'] = ('*', 3) kwargs['dtype'] = (float,) return Array(*args, **kwargs) - return cls.__init__(*args, **kwargs) + return super(Vector3Array, cls).__new__(cls, *args, **kwargs) class Vector2Array(BaseVector): @@ -514,7 +514,7 @@ def __new__(cls, *args, **kwargs): kwargs['shape'] = ('*', 2) kwargs['dtype'] = (float,) return Array(*args, **kwargs) - return cls.__init__(*args, **kwargs) + return super(Vector2Array, cls).__new__(cls, *args, **kwargs) VECTOR_DIRECTIONS = { From 7345fff5d7248a80485a8eb942a4099bd8e99197 Mon Sep 17 00:00:00 2001 From: fwkoch Date: Thu, 20 Jul 2017 16:31:23 -0600 Subject: [PATCH 4/5] Relocate VECTOR_DIRECTIONS --- properties/math.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/properties/math.py b/properties/math.py index 9cb0ce2..3ce5981 100644 --- a/properties/math.py +++ b/properties/math.py @@ -30,6 +30,22 @@ bool: Bool, } +VECTOR_DIRECTIONS = { + 'ZERO': [0, 0, 0], + 'X': [1, 0, 0], + 'Y': [0, 1, 0], + 'Z': [0, 0, 1], + '-X': [-1, 0, 0], + '-Y': [0, -1, 0], + '-Z': [0, 0, -1], + 'EAST': [1, 0, 0], + 'WEST': [-1, 0, 0], + 'NORTH': [0, 1, 0], + 'SOUTH': [0, -1, 0], + 'UP': [0, 0, 1], + 'DOWN': [0, 0, -1], +} + class Array(Property): """Property for :class:`numpy arrays ` @@ -147,7 +163,6 @@ def equal(self, value_a, value_b): except TypeError: return False - def error(self, instance, value, error_class=None, extra=''): """Generates a ValueError on setting property to an invalid value""" error_class = error_class if error_class is not None else ValueError @@ -517,18 +532,3 @@ def __new__(cls, *args, **kwargs): return super(Vector2Array, cls).__new__(cls, *args, **kwargs) -VECTOR_DIRECTIONS = { - 'ZERO': [0, 0, 0], - 'X': [1, 0, 0], - 'Y': [0, 1, 0], - 'Z': [0, 0, 1], - '-X': [-1, 0, 0], - '-Y': [0, -1, 0], - '-Z': [0, 0, -1], - 'EAST': [1, 0, 0], - 'WEST': [-1, 0, 0], - 'NORTH': [0, 1, 0], - 'SOUTH': [0, -1, 0], - 'UP': [0, 0, 1], - 'DOWN': [0, 0, -1], -} From 3f0ffa9391f61c2c1af13d7b86d058b6242b7e18 Mon Sep 17 00:00:00 2001 From: fwkoch Date: Thu, 20 Jul 2017 16:51:38 -0600 Subject: [PATCH 5/5] Get rid of __new__ magic in favor of overriding Array classes with functions --- properties/math.py | 163 ++++++++++++++++++++++----------------------- 1 file changed, 80 insertions(+), 83 deletions(-) diff --git a/properties/math.py b/properties/math.py index 3ce5981..d798bef 100644 --- a/properties/math.py +++ b/properties/math.py @@ -47,6 +47,30 @@ } +def _validate_shape(value): + if not isinstance(value, tuple): + raise TypeError("{}: Invalid shape - must be a tuple " + "(e.g. ('*',3) for an array of length-3 " + "arrays)".format(value)) + for shp in value: + if shp != '*' and not isinstance(shp, integer_types): + raise TypeError("{}: Invalid shape - values " + "must be '*' or int".format(value)) + return value + + +def _validate_dtype(value): + if not isinstance(value, (list, tuple)): + value = (value,) + if not value: + raise TypeError('No dtype specified - must be int, float, ' + 'and/or bool') + if any([val not in TYPE_MAPPINGS for val in value]): + raise TypeError('{}: Invalid dtype - must be int, float, ' + 'and/or bool'.format(value)) + return value + + class Array(Property): """Property for :class:`numpy arrays ` @@ -86,19 +110,7 @@ def shape(self): @shape.setter def shape(self, value): - self._shape = self._validate_shape(value) - - @staticmethod - def _validate_shape(value): - if not isinstance(value, tuple): - raise TypeError("{}: Invalid shape - must be a tuple " - "(e.g. ('*',3) for an array of length-3 " - "arrays)".format(value)) - for shp in value: - if shp != '*' and not isinstance(shp, integer_types): - raise TypeError("{}: Invalid shape - values " - "must be '*' or int".format(value)) - return value + self._shape = _validate_shape(value) @property def dtype(self): @@ -110,19 +122,7 @@ def dtype(self): @dtype.setter def dtype(self, value): - self._dtype = self._validate_dtype(value) - - @staticmethod - def _validate_dtype(value): - if not isinstance(value, (list, tuple)): - value = (value,) - if not value: - raise TypeError('No dtype specified - must be int, float, ' - 'and/or bool') - if any([val not in TYPE_MAPPINGS for val in value]): - raise TypeError('{}: Invalid dtype - must be int, float, ' - 'and/or bool'.format(value)) - return value + self._dtype = _validate_dtype(value) @property def info(self): @@ -224,28 +224,6 @@ def _recurse_list(val): def from_json(value, **kwargs): return np.array(value).astype(float) - def __new__(cls, *args, **kwargs): - """If np not available, use equivalent List""" - if not np: - shape = cls._validate_shape(kwargs.pop('shape', ('*',))) - dtype = cls._validate_dtype(kwargs.pop('dtype', (float, int))) - kwargs['coerce'] = True - - def _get_list_prop(list_kw, ind=0): - if ind + 1 == len(shape): - list_kw['prop'] = Union( - doc='', - props=[PROP_MAPPINGS[t]('') for t in dtype], - ) - else: - list_kw['prop'] = _get_list_prop(kwargs.copy(), ind+1) - if shape[ind] != '*': - list_kw['min_length'] = list_kw['max_length'] = shape[ind] - return List(*args, **list_kw) - - return _get_list_prop(kwargs.copy()) - return super(Array, cls).__new__(cls, *args, **kwargs) - class BaseVector(Array): """Base class for Vector properties""" @@ -340,15 +318,6 @@ def validate(self, instance, value): def from_json(value, **kwargs): return vmath.Vector3(value) - def __new__(cls, *args, **kwargs): - """If vmath not available, use equivalent Array""" - if not vmath: - kwargs.pop('length', None) - kwargs['shape'] = (3,) - kwargs['dtype'] = (float,) - return Array(*args, **kwargs) - return super(Vector3, cls).__new__(cls, *args, **kwargs) - class Vector2(BaseVector): """Property for :class:`2D vectors ` @@ -397,15 +366,6 @@ def validate(self, instance, value): def from_json(value, **kwargs): return vmath.Vector2(value) - def __new__(cls, *args, **kwargs): - """If vmath not available, use equivalent Array""" - if not vmath: - kwargs.pop('length', None) - kwargs['shape'] = (2,) - kwargs['dtype'] = (float,) - return Array(*args, **kwargs) - return super(Vector2, cls).__new__(cls, *args, **kwargs) - class Vector3Array(BaseVector): """Property for an :class:`array of 3D vectors ` @@ -458,15 +418,6 @@ def validate(self, instance, value): def from_json(value, **kwargs): return vmath.Vector3Array(value) - def __new__(cls, *args, **kwargs): - """If vmath not available, use equivalent Array""" - if not vmath: - kwargs.pop('length', None) - kwargs['shape'] = ('*', 3) - kwargs['dtype'] = (float,) - return Array(*args, **kwargs) - return super(Vector3Array, cls).__new__(cls, *args, **kwargs) - class Vector2Array(BaseVector): """Property for an :class:`array of 2D vectors ` @@ -522,13 +473,59 @@ def validate(self, instance, value): def from_json(value, **kwargs): return vmath.Vector2Array(value) - def __new__(cls, *args, **kwargs): - """If vmath not available, use equivalent Array""" - if not vmath: - kwargs.pop('length', None) - kwargs['shape'] = ('*', 2) - kwargs['dtype'] = (float,) - return Array(*args, **kwargs) - return super(Vector2Array, cls).__new__(cls, *args, **kwargs) +# The following are aliases for Array and Vector classes if library +# dependencies are not available. This is especially useful for using +# properties in lightweight environments without numpy. +if not np: + + def Array(*args, **kwargs): #pylint: disable=invalid-name,function-redefined + """If numpy not available, Array is replaced with equivalent List""" + shape = _validate_shape(kwargs.pop('shape', ('*',))) + dtype = _validate_dtype(kwargs.pop('dtype', (float, int))) + kwargs['coerce'] = True + def _get_list_prop(list_kw, ind=0): + if ind + 1 == len(shape): + list_kw['prop'] = Union( + doc='', + props=[PROP_MAPPINGS[t]('') for t in dtype], + ) + else: + list_kw['prop'] = _get_list_prop(kwargs.copy(), ind+1) + if shape[ind] != '*': + list_kw['min_length'] = list_kw['max_length'] = shape[ind] + return List(*args, **list_kw) + + return _get_list_prop(kwargs.copy()) + + +if not vmath: + + def Vector3(*args, **kwargs): #pylint: disable=invalid-name,function-redefined + """If vmath not available, Vector3 is replaced with Array""" + kwargs.pop('length', None) + kwargs['shape'] = (3,) + kwargs['dtype'] = (float,) + return Array(*args, **kwargs) + + def Vector2(*args, **kwargs): #pylint: disable=invalid-name,function-redefined + """If vmath not available, Vector2 is replaced with Array""" + kwargs.pop('length', None) + kwargs['shape'] = (2,) + kwargs['dtype'] = (float,) + return Array(*args, **kwargs) + + def Vector3Array(*args, **kwargs): #pylint: disable=invalid-name,function-redefined + """If vmath not available, Vector3Array is replaced with Array""" + kwargs.pop('length', None) + kwargs['shape'] = ('*', 3) + kwargs['dtype'] = (float,) + return Array(*args, **kwargs) + + def Vector2Array(*args, **kwargs): #pylint: disable=invalid-name,function-redefined + """If vmath not available, Vector2Array is replaced with Array""" + kwargs.pop('length', None) + kwargs['shape'] = ('*', 2) + kwargs['dtype'] = (float,) + return Array(*args, **kwargs)