Source code for nani

#                      __
#   .-----.---.-.-----|__|
#   |     |  _  |     |  |
#   |__|__|___._|__|__|__|
#

"""Alternative approach to defining and viewing NumPy's arrays."""

__all__ = ['Bool', 'Object', 'Number', 'String', 'Unicode', 'Array',
           'Structure', 'Bytes', 'Str', 'Field', 'READ_ONLY', 'Nani',
           'validate', 'resolve', 'update', 'get_data', 'get_element_view']

__title__ = 'nani'
__version__ = '0.2.0'
__summary__ = "Alternative approach to defining and viewing NumPy's arrays"
__url__ = 'https://github.com/christophercrouzet/nani'
__author__ = "Christopher Crouzet"
__contact__ = 'christopher.crouzet@gmail.com'
__license__ = "MIT"

import collections
import copy
import numbers
import sys
import types

import numpy


_PY2 = sys.version_info[0] == 2


if _PY2:
    _CLASS_TYPES = (type, types.ClassType)
    _STRING_TYPES = (basestring,)
    _BuiltinString = str
    _BuiltinUnicode = unicode
    _BUILTIN_MODULE = '__builtin__'

    _range = xrange
else:
    _CLASS_TYPES = (type,)
    _STRING_TYPES = (str,)
    _BuiltinString = bytes
    _BuiltinUnicode = str
    _BUILTIN_MODULE = 'builtins'

    _range = range

_NUMBER_TYPES = (numbers.Number, numpy.number)
_SEQUENCE_TYPES = (list, tuple)


_Bool = collections.namedtuple(
    'Bool', (
        'default',
        'view',
    ))
_Bool.__new__.__defaults__ = (False, None)


[docs]class Bool(_Bool): """Type corresponding to ``numpy.bool_``. Attributes ---------- default : bool Default value. view : type or None If ``None``, the owning array returns a direct reference to this boolean value, otherwise it is expected to be a class object wrapping it and accepting 2 parameters: ``data``, the NumPy array owning the boolean value, and ``index``, its position in the array. """ __slots__ = ()
_Object = collections.namedtuple( 'Object', ( 'default', 'view', )) _Object.__new__.__defaults__ = (None, None)
[docs]class Object(_Object): """Type corresponding to ``numpy.object_``. Attributes ---------- default : object Default value. view : type or None If ``None``, the owning array returns a direct reference to this Python object, otherwise it is expected to be a class object wrapping it and accepting 2 parameters: ``data``, the NumPy array owning the Python object, and ``index``, its position in the array. """ __slots__ = ()
_Number = collections.namedtuple( 'Number', ( 'type', 'default', 'view', )) _Number.__new__.__defaults__ = (numpy.float_, 0, None)
[docs]class Number(_Number): """Type corresponding to ``numpy.number``. Attributes ---------- type : type Type of the number. Either one inheriting from ``numbers.Number`` or ``numpy.number``. default : numbers.Number or numpy.number Default value. view : type or None If ``None``, the owning array returns a direct reference to this numeric value, otherwise it is expected to be a class object wrapping it and accepting 2 parameters: ``data``, the NumPy array owning the numeric value, and ``index``, its position in the array. """ __slots__ = ()
_String = collections.namedtuple( 'String', ( 'length', 'default', 'view', )) _String.__new__.__defaults__ = (_BuiltinString(), None)
[docs]class String(_String): """Type corresponding to ``numpy.string_``. Attributes ---------- length : int Number of characters. default : str on PY2 or bytes on PY3 Default value. view : type or None If ``None``, the owning array returns a direct reference to this string value, otherwise it is expected to be a class object wrapping it and accepting 2 parameters: ``data``, the NumPy array owning the string value, and ``index``, its position in the array. """ __slots__ = ()
_Unicode = collections.namedtuple( 'Unicode', ( 'length', 'default', 'view', )) _Unicode.__new__.__defaults__ = (_BuiltinUnicode(), None)
[docs]class Unicode(_Unicode): """Type corresponding to ``numpy.unicode_``. Attributes ---------- length : int Number of characters. default : unicode on PY2 or str on PY3 Default value. view : type or None If ``None``, the owning array returns a direct reference to this unicode value, otherwise it is expected to be a class object wrapping it and accepting 2 parameters: ``data``, the NumPy array owning the unicode value, and ``index``, its position in the array. """ __slots__ = ()
_Array = collections.namedtuple( 'Array', ( 'element_type', 'shape', 'name', 'view', )) _Array.__new__.__defaults__ = (None, None)
[docs]class Array(_Array): """Type corresponding to a NumPy (sub)array. Attributes ---------- element_type : nani type Type of each element. shape : int or tuple of int Shape of the array. Passing an int defines a 1D array. name : str or None Name for the view type if `view` is ``None``. view : type or None If ``None``, a view for this array is dynamically generated by Nani, otherwise it is expected to be a class object wrapping it and accepting 1 parameter: ``data``, the corresponding NumPy array. """ __slots__ = ()
_Structure = collections.namedtuple( 'Structure', ( 'fields', 'name', 'view', )) _Structure.__new__.__defaults__ = (None, None)
[docs]class Structure(_Structure): """Type corresponding to a NumPy structured array. Attributes ---------- fields : tuple of nani.Field or compatible tuple Fields defining the structure. name : str or None Name for the view type if `view` is ``None``. view : type or None If ``None``, a view for this structured array is dynamically generated by Nani, otherwise it is expected to be a class object wrapping it and accepting 1 parameter: ``data``, the corresponding NumPy structured array. """ __slots__ = ()
# Aliases. Bytes = String if _PY2: Str = String else: Str = Unicode _PREDEFINED_ATOMIC_NUMPY_TYPES = { Bool: numpy.bool_, Object: numpy.object_, String: numpy.string_, Unicode: numpy.unicode_, } _FIXED_ATOMIC = (Bool, Object, Number) _FLEXIBLE_ATOMIC = (String, Unicode) _ATOMIC = _FIXED_ATOMIC + _FLEXIBLE_ATOMIC _ALL = _ATOMIC + (Array, Structure) _TYPE_COUNT = len(_ALL) assert(all(hasattr(type_, 'type') or type_ in _PREDEFINED_ATOMIC_NUMPY_TYPES for type_ in _ATOMIC)) _Field = collections.namedtuple( 'Field', ( 'name', 'type', 'read_only', )) _Field.__new__.__defaults__ = (False,)
[docs]class Field(_Field): """Describe a field of a structured array. Attributes ---------- name : str Name of the field. type : nani data type Type of the field. read_only : bool ``True`` to not define a setter property in the structured array view if it is set to be dynamically generated by Nani. """ __slots__ = ()
#: Constant to use for the :attr:`Field.read_only` attribute's value. #: To use for readability reasons when the ``read_only`` keyword is not #: explicitely written. READ_ONLY = True _FIELD_NAME_IDX = Field._fields.index('name') _FIELD_TYPE_IDX = Field._fields.index('type') _FIELD_ATTR_COUNT = len(Field._fields) _FIELD_REQUIRED_ARG_RANGE = _range( _FIELD_ATTR_COUNT - len(Field.__new__.__defaults__), _FIELD_ATTR_COUNT + 1) _FieldInstanceCheck = collections.namedtuple( '_FieldInstanceCheck', ( 'name', 'type', 'allow_none', )) _FieldInstanceCheck.__new__.__defaults__ = (False,) _FieldSubclassCheck = collections.namedtuple( '_FieldSubclassCheck', ( 'name', 'type', 'allow_none', )) _FieldSubclassCheck.__new__.__defaults__ = (False,) _TYPE_ATTR_CHECKS = { Bool: ( _FieldInstanceCheck(name='default', type=bool), _FieldSubclassCheck(name='view', type=object, allow_none=True), ), Object: ( _FieldInstanceCheck(name='default', type=object, allow_none=True), _FieldSubclassCheck(name='view', type=object, allow_none=True), ), Number: ( _FieldSubclassCheck(name='type', type=_NUMBER_TYPES), _FieldInstanceCheck(name='default', type=_NUMBER_TYPES), _FieldSubclassCheck(name='view', type=object, allow_none=True), ), String: ( _FieldInstanceCheck(name='length', type=int), _FieldInstanceCheck(name='default', type=_BuiltinString), _FieldSubclassCheck(name='view', type=object, allow_none=True), ), Unicode: ( _FieldInstanceCheck(name='length', type=int), _FieldInstanceCheck(name='default', type=_BuiltinUnicode), _FieldSubclassCheck(name='view', type=object, allow_none=True), ), Array: ( _FieldInstanceCheck(name='element_type', type=_ALL), _FieldInstanceCheck(name='shape', type=_SEQUENCE_TYPES + (int,)), _FieldInstanceCheck(name='name', type=_STRING_TYPES, allow_none=True), _FieldSubclassCheck(name='view', type=object, allow_none=True), ), Structure: ( _FieldInstanceCheck(name='fields', type=_SEQUENCE_TYPES), _FieldInstanceCheck(name='name', type=_STRING_TYPES, allow_none=True), _FieldSubclassCheck(name='view', type=object, allow_none=True), ), } _MIXIN_ATTRIBUTES = {} class _DirectArrayViewMixin(object): """Mixin for array views with no element view provided. Element are directly accessed and set. """ __slots__ = ('_data',) def __init__(self, data): self._data = data def __repr__(self): return "[%s]" % (', '.join(str(item) for item in self._data)) def __eq__(self, other): if isinstance(other, type(self)): return numpy.array_equal(self._data, other._data) return NotImplemented def __ne__(self, other): is_equal = self.__eq__(other) return is_equal if is_equal is NotImplemented else not is_equal def __getitem__(self, index): return self._data[index] def __setitem__(self, index, value): self._data[index] = value def __iter__(self): return iter(self._data) def __len__(self): return self._data.__len__() def __contains__(self, item): return item in self._data _MIXIN_ATTRIBUTES[_DirectArrayViewMixin] = ( '__slots__', '__init__', '__repr__', '__eq__', '__ne__', '__getitem__', '__setitem__', '__iter__', '__len__', '__contains__', ) class _IndirectAtomicArrayViewMixin(object): """Mixin for atomic array views with an element view provided. Atomic elements are not directly accessed and set. Instead they are first wrapped into the element view type then returned. """ __slots__ = ('_data',) def __init__(self, data): self._data = data def __repr__(self): return "[%s]" % (', '.join(str(self._element_view(self._data, i)) for i in _range(len(self._data)))) def __eq__(self, other): if isinstance(other, type(self)): return numpy.array_equal(self._data, other._data) return NotImplemented def __ne__(self, other): is_equal = self.__eq__(other) return is_equal if is_equal is NotImplemented else not is_equal def __getitem__(self, index): return self._element_view(self._data, index) def __setitem__(self, index, value): self._data[index] = value def __iter__(self): return (self._element_view(self._data, i) for i in _range(len(self._data))) def __len__(self): return len(self._data) def __contains__(self, item): return item in self._data _MIXIN_ATTRIBUTES[_IndirectAtomicArrayViewMixin] = ( '__slots__', '__init__', '__repr__', '__eq__', '__ne__', '__getitem__', '__setitem__', '__iter__', '__len__', '__contains__', ) class _IndirectCompositeArrayViewMixin(object): """Mixin for composite array views with an element view provided. Composite elements are first wrapped into the element view type then returned. The element views are to be initialized with 1 parameter: ``data``, the NumPy array representing the element. """ __slots__ = ('_data',) def __init__(self, data): self._data = data def __repr__(self): return "[%s]" % (', '.join(str(self._element_view(item)) for item in self._data)) def __eq__(self, other): if isinstance(other, type(self)): return numpy.array_equal(self._data, other._data) return NotImplemented def __ne__(self, other): is_equal = self.__eq__(other) return is_equal if is_equal is NotImplemented else not is_equal def __getitem__(self, index): return self._element_view(self._data[index]) def __setitem__(self, index, value): self._data[index] = value def __iter__(self): return (self._element_view(item) for item in self._data) def __len__(self): return self._data.__len__() def __contains__(self, item): return item in self._data _MIXIN_ATTRIBUTES[_IndirectCompositeArrayViewMixin] = ( '__slots__', '__init__', '__repr__', '__eq__', '__ne__', '__getitem__', '__setitem__', '__iter__', '__len__', '__contains__', ) class _StructuredViewMixin(object): """Mixin for structured array views. Properties are to be dynamically generated for each field. """ __slots__ = ('_data',) def __init__(self, data): self._data = data def __repr__(self): fields_and_values = ("%s=%r" % (field, getattr(self, field)) for field in self._fields) return "%s(%s)" % (type(self).__name__, ', '.join(fields_and_values)) def __eq__(self, other): if isinstance(other, type(self)): return numpy.array_equal(self._data, other._data) return NotImplemented def __ne__(self, other): is_equal = self.__eq__(other) return is_equal if is_equal is NotImplemented else not is_equal _MIXIN_ATTRIBUTES[_StructuredViewMixin] = ( '__slots__', '__init__', '__repr__', '__eq__', '__ne__', ) _Nani = collections.namedtuple( 'Nani', ( 'dtype', 'default', 'view', ))
[docs]class Nani(_Nani): """Output structure of the function `resolve`. Attributes ---------- dtype : numpy.dtype NumPy's ``dtype``, that is the data type of the array elements. default : object Default value(s) for a single array element. view : type A class to use as a wrapper around the NumPy array. """ __slots__ = ()
[docs]def validate(data_type): """Check if a data type is well-formed. Parameters ---------- data_type : nani data type Data type. Returns ------- bool ``True`` if the data type is well-formed. Raises ------ TypeError or ValueError The data type isn't well-formed. """ return _validate(data_type, '')
[docs]def resolve(data_type, name=None, listify_default=False): """Retrieve the properties for a given data type. This is the main routine where most of the work is done. It converts Nani's data types into properties that can be used to define a new NumPy array and to wrap it into a view object. Use :func:`validate` to check if the input data type is well-formed. Parameters ---------- data_type : nani data type Type of the array elements. name : str Name for the view to be generated for the array. listify_default : bool ``True`` to output the default values with lists in place of tuples. This might cause the output to be incompatible with array creation routines such as ``numpy.array`` but it should still work for element assignment. Returns ------- nani.Nani The properties to use to initalize a NumPy array around the data type. Examples -------- Create a NumPy array where each element represents a color: >>> import numpy >>> import nani >>> color_type = nani.Array( ... element_type=nani.Number(type=numpy.uint8, default=255), ... shape=3, ... view=None) >>> dtype, default, view = nani.resolve(color_type, name='Color') >>> a = numpy.array([default] * element_count, dtype=dtype) >>> v = view(a) >>> type(v) <class 'nani.Color'> >>> for color in v: ... color [255, 255, 255] [255, 255, 255] """ data_type = _consolidate(data_type) return Nani( dtype=numpy.dtype(_resolve_dtype(data_type)), default=_resolve_default(data_type, listify=listify_default), view=_resolve_view(Array(element_type=data_type, shape=-1, name=name)))
[docs]def update(data_type, **kwargs): """Update a data type with different values. The operation is not made in place, instead a copy is returned. Parameters ---------- data_type : nani data type Data type. kwargs Keyword arguments to update. Returns ------- nani data type The updated version of the data type. Examples -------- Update the shape of an array data type and the default value of its elements: >>> import nani >>> data_type = nani.Array( ... element_type=nani.Number(), ... shape=2) >>> new_data_type = nani.update( ... data_type, ... element_type=nani.update(data_type.element_type, default=123), ... shape=3) """ return data_type._replace(**kwargs)
[docs]def get_data(view): """Retrieve the NumPy data from an array view generated by Nani. Parameters ---------- view : nani.AtomicArrayView or nani.CompositeArrayView Array view. Returns ------- type The NumPy array, ``None`` otherwise. Examples -------- >>> import numpy >>> import nani >>> data_type = nani.Number(type=numpy.int32) >>> dtype, _, view = nani.resolve(data_type) >>> a = numpy.a_range(10, dtype=dtype) >>> v = view(a) >>> nani.get_data(v) is a True """ return getattr(view, '_data', None)
[docs]def get_element_view(view): """Retrieve the element view from an array view generated by Nani. Parameters ---------- view : nani.AtomicArrayView or nani.CompositeArrayView Array view. Returns ------- type The element view, ``None`` otherwise. Examples -------- >>> import numpy >>> import nani >>> vector2_type = nani.Array( ... element_type=nani.Number(), ... shape=2, ... name='Vector2') >>> dtype, default, view = nani.resolve(vector2_type, name='Positions') >>> a = numpy.zeros(3, dtype=dtype) >>> v = view(a) >>> type(v) <class 'nani.Positions'> >>> nani.get_element_view(v) <class 'nani.Vector2'> """ return getattr(view, '_element_view', None)
def _validate(data_type, parent_path): """Implementation for the `validate` function.""" if isinstance(data_type, _CLASS_TYPES): raise TypeError( "The data type is expected to be an instance object, but got the " "type '%s' instead." % (_format_type(data_type),)) base = _find_base_type(data_type) if not base: raise TypeError( "Objects of type '%s' aren't supported as data types. Use any " "type from %s instead." % (_format_type(type(data_type)), _join_types(_ALL, "or "))) name = getattr(data_type, 'name', None) if not name: name = type(data_type).__name__ full_path = '%s.%s' % (parent_path, name) if parent_path else name # Generic checks for each attribute. for check in _TYPE_ATTR_CHECKS[base]: attribute = getattr(data_type, check.name) if attribute is None: if check.allow_none: continue else: raise TypeError("The attribute '%s.%s' cannot be 'None'." % (full_path, check.name)) if isinstance(check, _FieldInstanceCheck): check_function = isinstance elif isinstance(check, _FieldSubclassCheck): if not isinstance(attribute, _CLASS_TYPES): raise TypeError( "The attribute '%s.%s' is expected to be a type " "object%s." % (full_path, check.name, " or 'None'" if check.allow_none else '')) check_function = issubclass if not check_function(attribute, check.type): if isinstance(check, _FieldInstanceCheck): glue_1 = "an instance object of type" glue_2 = "not" glue_3 = "" type_name = _format_type(type(attribute)) elif isinstance(check, _FieldSubclassCheck): glue_1 = "a subclass of" glue_2 = "but got" glue_3 = " instead" type_name = _format_type(attribute) raise TypeError( "The attribute '%s.%s' is expected to be %s %s, %s '%s'%s." % (full_path, check.name, glue_1, _join_types(check.type, "or "), glue_2, type_name, glue_3)) # Additional and/or recursive checks for specific attributes. if isinstance(data_type, Array): _validate(data_type.element_type, full_path) elif isinstance(data_type, Structure): for field in data_type.fields: if not isinstance(field, _SEQUENCE_TYPES): raise TypeError( "Each field from the attribute '%s.fields' is expected " "to be an instance object of type %s, not '%s'." % (full_path, _join_types(_SEQUENCE_TYPES + (Field,), "or "), _format_type(type(field)))) if len(field) not in _FIELD_REQUIRED_ARG_RANGE: raise TypeError( "Each field from the attribute '%s.fields' is expected " "to be an instance object of type %s, and compatible with " "the '%s' structure, but got %r instead." % (full_path, _join_types(_SEQUENCE_TYPES + (Field,), "or "), _format_type(Field), field)) field = Field(*field) if not isinstance(field.name, _STRING_TYPES): raise TypeError( "The first element of each field from the attribute " "'%s.fields', that is the 'name' attribute, is expected " "to be an instance object of type %s, not '%s'." % (full_path, _join_types(_STRING_TYPES, "or "), _format_type(type(field.name)))) if not isinstance(field.type, _ALL): raise TypeError( "The second element of each field from the attribute " "'%s.fields', that is the 'type' attribute, is expected " "to be an instance object of type %s, not '%s'." % (full_path, _join_types(_ALL, "or "), _format_type(type(field.type)))) if not isinstance(field.read_only, bool): raise TypeError( "The third element of each field from the attribute " "'%s.fields', that is the 'read_only' attribute, is " "expected to be an instance object of type 'bool', " "not '%s'." % (full_path, _format_type(type(field.read_only)))) field_path = '%s.%s' % (full_path, field.name) _validate(field.type, field_path) fields = [field[_FIELD_NAME_IDX] for field in data_type.fields] duplicates = _find_duplicates(fields) if duplicates: if len(duplicates) > 1: raise ValueError( "The structure fields %s, were provided multiple times." % (_join_sequence(duplicates, "and ")),) else: raise ValueError( "The structure field '%s' was provided multiple times." % (duplicates[0]),) return True def _consolidate(data_type): """Enforce the structure of the data type. Specifically, ensure that if a field is defined as a generic tuple, then it will be converted into an instance of `Field`. """ if isinstance(data_type, _ATOMIC): out = data_type elif isinstance(data_type, Array): element_type = _consolidate(data_type.element_type) out = data_type._replace(element_type=element_type) elif isinstance(data_type, Structure): fields = tuple( Field(*(_consolidate(field[i]) if i == _FIELD_TYPE_IDX else field[i] for i in _range(len(field)))) for field in data_type.fields) out = data_type._replace(fields=fields) return out def _resolve_dtype(data_type): """Retrieve the corresponding NumPy's `dtype` for a given data type.""" if isinstance(data_type, _FIXED_ATOMIC): out = _get_atomic_dtype(data_type) elif isinstance(data_type, _FLEXIBLE_ATOMIC): out = (_get_atomic_dtype(data_type), data_type.length) elif isinstance(data_type, Array): shape = data_type.shape if isinstance(shape, _SEQUENCE_TYPES) and len(shape) == 1: # Workaround the exception `ValueError: invalid itemsize in # generic type tuple` when an `Array` of shape 0 or (0,) is nested # within another `Array`. shape = shape[0] out = (_resolve_dtype(data_type.element_type), shape) elif isinstance(data_type, Structure): out = [(field.name, _resolve_dtype(field.type)) for field in data_type.fields] return out def _resolve_default(data_type, listify=False): """Retrieve the default value for a given data type.""" if isinstance(data_type, _ATOMIC): # A Python's object type needs to be left as is instead of being # wrapped into a NumPy type. out = (data_type.default if isinstance(data_type, Object) else _get_atomic_dtype(data_type)(data_type.default)) elif isinstance(data_type, Array): element_default = _resolve_default(data_type.element_type, listify=listify) Sequence = list if listify else tuple shape = ((data_type.shape,) if isinstance(data_type.shape, int) else data_type.shape) out = element_default for dimension in shape: out = Sequence(copy.deepcopy(out) for _ in _range(dimension)) elif isinstance(data_type, Structure): if listify: out = [_resolve_default(field.type, listify=listify) for field in data_type.fields] else: field_defaults = collections.OrderedDict( (field.name, _resolve_default(field.type, listify=listify)) for field in data_type.fields) name = ('StructureDefault_%s' % (data_type.name,) if data_type.name else 'StructureDefault') struct = collections.namedtuple(name, field_defaults.keys()) out = struct(**field_defaults) return out def _resolve_view(data_type): """Retrieve the view for a given data type. Only one view class is returned, that is the one representing the root data type, but more class objects might be dynamically created if the input data type has nested elements, such as for the `Array` and `Structure` types. The default behaviour of dynamically and recursively creating a new view class can be overriden by setting the `view` attribute of a data type. """ view = getattr(data_type, 'view', None) if view is not None: return view if isinstance(data_type, _ATOMIC): out = None elif isinstance(data_type, Array): out = _define_array_view(data_type) elif isinstance(data_type, Structure): out = _define_structure_view(data_type) return out def _define_array_view(data_type): """Define a new view object for a `Array` type.""" element_type = data_type.element_type element_view = _resolve_view(element_type) if element_view is None: mixins = (_DirectArrayViewMixin,) attributes = _get_mixin_attributes(mixins) elif isinstance(element_type, _ATOMIC): mixins = (_IndirectAtomicArrayViewMixin,) attributes = _get_mixin_attributes(mixins) attributes.update({ '_element_view': element_view, }) else: mixins = (_IndirectCompositeArrayViewMixin,) attributes = _get_mixin_attributes(mixins) attributes.update({ '_element_view': element_view, }) name = data_type.name if data_type.name else 'ArrayView' return type(name, (), attributes) def _define_structure_view(data_type): """Define a new view object for a `Structure` type.""" def define_getter(field_index, field_type, field_view): if field_view is None: def getter(self): return self._data[field_index] elif isinstance(field_type, _ATOMIC): def getter(self): return field_view(self._data, field_index) else: def getter(self): return field_view(self._data[field_index]) return getter def define_setter(field_index, read_only): def setter(self, value): self._data[field_index] = value return None if read_only else setter field_views = [_resolve_view(field.type) for field in data_type.fields] mixins = (_StructuredViewMixin,) attributes = _get_mixin_attributes(mixins) attributes.update({ '_fields': tuple(field.name for field in data_type.fields), }) attributes.update({ field.name: property( fget=define_getter(i, field.type, field_view), fset=define_setter(i, field.read_only), fdel=None) for i, (field, field_view) in enumerate(zip(data_type.fields, field_views))}) name = data_type.name if data_type.name else 'StructureView' return type(name, (), attributes) def _get_mixin_attributes(mixins): """Retrieve the attributes for a given set of mixin classes. The attributes of each mixin class are being merged into a single dictionary. """ return {attribute: mixin.__dict__[attribute] for mixin in mixins for attribute in _MIXIN_ATTRIBUTES[mixin]} def _get_atomic_dtype(data_type): """Retrieve the NumPy's `dtype` for a given atomic data type.""" atomic_type = getattr(data_type, 'type', None) if atomic_type is not None: return atomic_type return _PREDEFINED_ATOMIC_NUMPY_TYPES[_find_base_type(data_type)] def _find_base_type(data_type): """Find the Nani's base type for a given data type. This is useful when Nani's data types were subclassed and the original type is required. """ bases = type(data_type).__mro__ for base in bases: if base in _ALL: return base return None def _find_duplicates(seq): """Find the duplicate elements from a sequence.""" seen = set() return [element for element in seq if seq.count(element) > 1 and element not in seen and seen.add(element) is None] def _format_type(cls): """Format a type name for printing.""" if cls.__module__ == _BUILTIN_MODULE: return cls.__name__ else: return '%s.%s' % (cls.__module__, cls.__name__) def _format_element(element, count, index, last_separator): """Format an element from a sequence. This only prepends a separator for the last element and wraps each element with single quotes. """ return ("%s'%s'" % (last_separator, element) if count > 1 and index == count - 1 else "'%s'" % (element,)) def _join_sequence(seq, last_separator=''): """Join a sequence into a string.""" count = len(seq) return ', '.join(_format_element(element, count, i, last_separator) for i, element in enumerate(seq)) def _join_types(seq, last_separator=''): """Join class object names into a string.""" class_names = [_format_type(cls) for cls in seq] return _join_sequence(class_names, last_separator)