from rpython.rlib import jit
from pypy.interpreter.baseobjspace import W_Root
from pypy.interpreter.typedef import TypeDef, GetSetProperty
from pypy.interpreter.gateway import interp2app, unwrap_spec, WrappedDefault
from pypy.interpreter.error import OperationError, oefmt
from pypy.module.micronumpy import support, concrete
from pypy.module.micronumpy.base import W_NDimArray, convert_to_array
from pypy.module.micronumpy.descriptor import decode_w_dtype
from pypy.module.micronumpy.iterators import ArrayIter
from pypy.module.micronumpy.strides import (calculate_broadcast_strides,
                                            shape_agreement, shape_agreement_multiple)


def parse_op_arg(space, name, w_op_flags, n, parse_one_arg):
    if space.is_w(w_op_flags, space.w_None):
        w_op_flags = space.newtuple([space.wrap('readonly')])
    if not space.isinstance_w(w_op_flags, space.w_tuple) and not \
            space.isinstance_w(w_op_flags, space.w_list):
        raise oefmt(space.w_ValueError,
                    '%s must be a tuple or array of per-op flag-tuples',
                    name)
    ret = []
    w_lst = space.listview(w_op_flags)
    if space.isinstance_w(w_lst[0], space.w_tuple) or \
       space.isinstance_w(w_lst[0], space.w_list):
        if len(w_lst) != n:
            raise oefmt(space.w_ValueError,
                        '%s must be a tuple or array of per-op flag-tuples',
                        name)
        for item in w_lst:
            ret.append(parse_one_arg(space, space.listview(item)))
    else:
        op_flag = parse_one_arg(space, w_lst)
        for i in range(n):
            ret.append(op_flag)
    return ret


class OpFlag(object):
    def __init__(self):
        self.rw = ''
        self.broadcast = True
        self.force_contig = False
        self.force_align = False
        self.native_byte_order = False
        self.tmp_copy = ''
        self.allocate = False

def parse_op_flag(space, lst):
    op_flag = OpFlag()
    for w_item in lst:
        item = space.str_w(w_item)
        if item == 'readonly':
            op_flag.rw = 'r'
        elif item == 'readwrite':
            op_flag.rw = 'rw'
        elif item == 'writeonly':
            op_flag.rw = 'w'
        elif item == 'no_broadcast':
            op_flag.broadcast = False
        elif item == 'contig':
            op_flag.force_contig = True
        elif item == 'aligned':
            op_flag.force_align = True
        elif item == 'nbo':
            op_flag.native_byte_order = True
        elif item == 'copy':
            op_flag.tmp_copy = 'r'
        elif item == 'updateifcopy':
            op_flag.tmp_copy = 'rw'
        elif item == 'allocate':
            op_flag.allocate = True
        elif item == 'no_subtype':
            raise oefmt(space.w_NotImplementedError,
                '"no_subtype" op_flag not implemented yet')
        elif item == 'arraymask':
            raise oefmt(space.w_NotImplementedError,
                '"arraymask" op_flag not implemented yet')
        elif item == 'writemask':
            raise oefmt(space.w_NotImplementedError,
                '"writemask" op_flag not implemented yet')
        else:
            raise oefmt(space.w_ValueError,
                'op_flags must be a tuple or array of per-op flag-tuples')
    if op_flag.rw == '':
        raise oefmt(space.w_ValueError,
                    "None of the iterator flags READWRITE, READONLY, or "
                    "WRITEONLY were specified for an operand")
    return op_flag


def parse_func_flags(space, nditer, w_flags):
    if space.is_w(w_flags, space.w_None):
        return
    elif not space.isinstance_w(w_flags, space.w_tuple) and not \
            space.isinstance_w(w_flags, space.w_list):
        raise oefmt(space.w_ValueError,
            'Iter global flags must be a list or tuple of strings')
    lst = space.listview(w_flags)
    for w_item in lst:
        if not space.isinstance_w(w_item, space.w_str) and not \
                space.isinstance_w(w_item, space.w_unicode):
            raise oefmt(space.w_TypeError,
                        "expected string or Unicode object, %T found",
                        w_item)
        item = space.str_w(w_item)
        if item == 'external_loop':
            nditer.external_loop = True
        elif item == 'buffered':
            raise oefmt(space.w_NotImplementedError,
                'nditer buffered not implemented yet')
            # For numpy compatability
            nditer.buffered = True
        elif item == 'c_index':
            nditer.tracked_index = 'C'
        elif item == 'f_index':
            nditer.tracked_index = 'F'
        elif item == 'multi_index':
            nditer.tracked_index = 'multi'
        elif item == 'common_dtype':
            nditer.common_dtype = True
        elif item == 'delay_bufalloc':
            nditer.delay_bufalloc = True
        elif item == 'grow_inner':
            nditer.grow_inner = True
        elif item == 'ranged':
            nditer.ranged = True
        elif item == 'refs_ok':
            nditer.refs_ok = True
        elif item == 'reduce_ok':
            raise oefmt(space.w_NotImplementedError,
                'nditer reduce_ok not implemented yet')
            nditer.reduce_ok = True
        elif item == 'zerosize_ok':
            nditer.zerosize_ok = True
        else:
            raise oefmt(space.w_ValueError,
                        'Unexpected iterator global flag "%s"',
                        item)
    if nditer.tracked_index and nditer.external_loop:
        raise oefmt(space.w_ValueError,
            'Iterator flag EXTERNAL_LOOP cannot be used if an index or '
            'multi-index is being tracked')


def is_backward(imp, order):
    if order == 'K' or (order == 'C' and imp.order == 'C'):
        return False
    elif order == 'F' and imp.order == 'C':
        return True
    else:
        raise NotImplementedError('not implemented yet')


class OperandIter(ArrayIter):
    _immutable_fields_ = ['slice_shape', 'slice_stride', 'slice_backstride',
                          'operand_type', 'base']

    def getitem(self, state):
        # cannot be called - must return a boxed value
        assert False

    def getitem_bool(self, state):
        # cannot be called - must return a boxed value
        assert False

    def setitem(self, state, elem):
        # cannot be called - must return a boxed value
        assert False


class ConcreteIter(OperandIter):
    def __init__(self, array, size, shape, strides, backstrides,
                 op_flags, base):
        OperandIter.__init__(self, array, size, shape, strides, backstrides)
        self.slice_shape =[] 
        self.slice_stride = []
        self.slice_backstride = []
        if op_flags.rw == 'r':
            self.operand_type = concrete.ConcreteNonWritableArrayWithBase
        else:
            self.operand_type = concrete.ConcreteArrayWithBase
        self.base = base

    def getoperand(self, state):
        assert state.iterator is self
        impl = self.operand_type
        res = impl([], self.array.dtype, self.array.order, [], [],
                   self.array.storage, self.base)
        res.start = state.offset
        return res


class SliceIter(OperandIter):
    def __init__(self, array, size, shape, strides, backstrides, slice_shape,
                 slice_stride, slice_backstride, op_flags, base):
        OperandIter.__init__(self, array, size, shape, strides, backstrides)
        self.slice_shape = slice_shape
        self.slice_stride = slice_stride
        self.slice_backstride = slice_backstride
        if op_flags.rw == 'r':
            self.operand_type = concrete.NonWritableSliceArray
        else:
            self.operand_type = concrete.SliceArray
        self.base = base

    def getoperand(self, state):
        assert state.iterator is self
        impl = self.operand_type
        arr = impl(state.offset, self.slice_stride, self.slice_backstride,
                   self.slice_shape, self.array, self.base)
        return arr


def get_iter(space, order, arr, shape, dtype, op_flags, base):
    imp = arr.implementation
    backward = is_backward(imp, order)
    if arr.is_scalar():
        return ConcreteIter(imp, 1, [], [], [], op_flags, base)
    if (imp.strides[0] < imp.strides[-1] and not backward) or \
       (imp.strides[0] > imp.strides[-1] and backward):
        # flip the strides. Is this always true for multidimension?
        strides = imp.strides[:]
        backstrides = imp.backstrides[:]
        shape = imp.shape[:]
        strides.reverse()
        backstrides.reverse()
        shape.reverse()
    else:
        strides = imp.strides
        backstrides = imp.backstrides
    r = calculate_broadcast_strides(strides, backstrides, imp.shape,
                                    shape, backward)
    if len(shape) != len(r[0]):
        # shape can be shorter when using an external loop, just return a view
        return ConcreteIter(imp, imp.get_size(), imp.shape, r[0], r[1], op_flags, base)
    return ConcreteIter(imp, imp.get_size(), shape, r[0], r[1], op_flags, base)

def calculate_ndim(op_in, oa_ndim):
    if oa_ndim >=0:
        return oa_ndim
    else:
        ndim = 0
        for op in op_in:
            if op is None:
                continue
            assert isinstance(op, W_NDimArray)
            ndim = max(ndim, op.ndims())
    return ndim

def coalesce_axes(it, space):
    # Copy logic from npyiter_coalesce_axes, used in ufunc iterators
    # and in nditer's with 'external_loop' flag
    can_coalesce = True
    for idim in range(it.ndim - 1):
        for op_it, _ in it.iters:
            if op_it is None:
                continue
            assert isinstance(op_it, ArrayIter)
            indx = len(op_it.strides)
            if it.order == 'F':
                indx = len(op_it.array.strides) - indx
                assert indx >=0
                astrides = op_it.array.strides[indx:]
            else:
                astrides = op_it.array.strides[:indx]
            # does op_it iters over array "naturally"
            if astrides != op_it.strides:
                can_coalesce = False
                break
        if can_coalesce:
            for i in range(len(it.iters)):
                new_iter = coalesce_iter(it.iters[i][0], it.op_flags[i], it,
                                         it.order)
                it.iters[i] = (new_iter, new_iter.reset())
            if len(it.shape) > 1:
                if it.order == 'F':
                    it.shape = it.shape[1:]
                else:
                    it.shape = it.shape[:-1]
            else:
                it.shape = [1]

        else:
            break
    # Always coalesce at least one
    for i in range(len(it.iters)):
        new_iter = coalesce_iter(it.iters[i][0], it.op_flags[i], it, 'C')
        it.iters[i] = (new_iter, new_iter.reset())
    if len(it.shape) > 1:
        if it.order == 'F':
            it.shape = it.shape[1:]
        else:
            it.shape = it.shape[:-1]
    else:
        it.shape = [1]


def coalesce_iter(old_iter, op_flags, it, order, flat=True):
    '''
    We usually iterate through an array one value at a time.
    But after coalesce(), getoperand() will return a slice by removing
    the fastest varying dimension(s) from the beginning or end of the shape.
    If flat is true, then the slice will be 1d, otherwise stack up the shape of
    the fastest varying dimension in the slice, so an iterator of a  'C' array 
    of shape (2,4,3) after two calls to coalesce will iterate 2 times over a slice
    of shape (4,3) by setting the offset to the beginning of the data at each iteration
    '''
    shape = [s+1 for s in old_iter.shape_m1]
    strides = old_iter.strides
    backstrides = old_iter.backstrides
    if order == 'F':
        new_shape = shape[1:]
        new_strides = strides[1:]
        new_backstrides = backstrides[1:]
        _stride = old_iter.slice_stride + [strides[0]]
        _shape =  old_iter.slice_shape + [shape[0]]
        _backstride = old_iter.slice_backstride + [strides[0] * (shape[0] - 1)]
        fastest = shape[0]
    else:
        new_shape = shape[:-1]
        new_strides = strides[:-1]
        new_backstrides = backstrides[:-1]
        # use the operand's iterator's rightmost stride,
        # even if it is not the fastest (for 'F' or swapped axis)
        _stride = [strides[-1]] + old_iter.slice_stride
        _shape = [shape[-1]]  + old_iter.slice_shape
        _backstride = [(shape[-1] - 1) * strides[-1]] + old_iter.slice_backstride
        fastest = shape[-1]
    if flat:
        _shape = [support.product(_shape)]
        if len(_stride) > 1:
            _stride = [min(_stride[0], _stride[1])]
        _backstride = [(shape[0] - 1) * _stride[0]]
    return SliceIter(old_iter.array, old_iter.size / fastest,
                new_shape, new_strides, new_backstrides,
                _shape, _stride, _backstride, op_flags, it)

class IndexIterator(object):
    def __init__(self, shape, backward=False):
        self.shape = shape
        self.index = [0] * len(shape)
        self.backward = backward

    @jit.unroll_safe
    def next(self):
        for i in range(len(self.shape) - 1, -1, -1):
            if self.index[i] < self.shape[i] - 1:
                self.index[i] += 1
                break
            else:
                self.index[i] = 0

    def getvalue(self):
        if not self.backward:
            ret = self.index[-1]
            for i in range(len(self.shape) - 2, -1, -1):
                ret += self.index[i] * self.shape[i - 1]
        else:
            ret = self.index[0]
            for i in range(1, len(self.shape)):
                ret += self.index[i] * self.shape[i - 1]
        return ret


class W_NDIter(W_Root):
    _immutable_fields_ = ['ndim', ]
    def __init__(self, space, w_seq, w_flags, w_op_flags, w_op_dtypes,
                 w_casting, w_op_axes, w_itershape, buffersize=0, order='K'):
        from pypy.module.micronumpy.ufuncs import find_binop_result_dtype
        
        self.order = order
        self.external_loop = False
        self.buffered = False
        self.tracked_index = ''
        self.common_dtype = False
        self.delay_bufalloc = False
        self.grow_inner = False
        self.ranged = False
        self.refs_ok = False
        self.reduce_ok = False
        self.zerosize_ok = False
        self.index_iter = None
        self.done = False
        self.first_next = True
        self.op_axes = []
        # convert w_seq operands to a list of W_NDimArray
        if space.isinstance_w(w_seq, space.w_tuple) or \
           space.isinstance_w(w_seq, space.w_list):
            w_seq_as_list = space.listview(w_seq)
            self.seq = [convert_to_array(space, w_elem)
                        if not space.is_none(w_elem) else None
                        for w_elem in w_seq_as_list]
        else:
            self.seq = [convert_to_array(space, w_seq)]

        parse_func_flags(space, self, w_flags)
        self.op_flags = parse_op_arg(space, 'op_flags', w_op_flags,
                                     len(self.seq), parse_op_flag)
        # handle w_op_axes
        oa_ndim = -1
        if not space.is_none(w_op_axes):
            oa_ndim = self.set_op_axes(space, w_op_axes)
        self.ndim = calculate_ndim(self.seq, oa_ndim)

        # handle w_op_dtypes part 1: creating self.dtypes list from input
        if not space.is_none(w_op_dtypes):
            w_seq_as_list = space.listview(w_op_dtypes)
            self.dtypes = [decode_w_dtype(space, w_elem) for w_elem in w_seq_as_list]
            if len(self.dtypes) != len(self.seq):
                raise oefmt(space.w_ValueError,
                    "op_dtypes must be a tuple/list matching the number of ops")
        else:
            self.dtypes = []

        # handle None or writable operands, calculate my shape
        outargs = [i for i in range(len(self.seq))
                   if self.seq[i] is None or self.op_flags[i].rw == 'w']
        if len(outargs) > 0:
            out_shape = shape_agreement_multiple(space, [self.seq[i] for i in outargs])
        else:
            out_shape = None
        if space.isinstance_w(w_itershape, space.w_tuple) or \
           space.isinstance_w(w_itershape, space.w_list):
            self.shape = [space.int_w(i) for i in space.listview(w_itershape)]
        else:
            self.shape = shape_agreement_multiple(space, self.seq,
                                                           shape=out_shape)
        if len(outargs) > 0:
            # Make None operands writeonly and flagged for allocation
            if len(self.dtypes) > 0:
                out_dtype = self.dtypes[outargs[0]]
            else:
                out_dtype = None
                for i in range(len(self.seq)):
                    if self.seq[i] is None:
                        self.op_flags[i].allocate = True
                        continue
                    if self.op_flags[i].rw == 'w':
                        continue
                    out_dtype = find_binop_result_dtype(
                        space, self.seq[i].get_dtype(), out_dtype)
            for i in outargs:
                if self.seq[i] is None:
                    # XXX can we postpone allocation to later?
                    self.seq[i] = W_NDimArray.from_shape(space, self.shape, out_dtype)
                else:
                    if not self.op_flags[i].broadcast:
                        # Raises if ooutput cannot be broadcast
                        shape_agreement(space, self.shape, self.seq[i], False)

        if self.tracked_index != "":
            if self.order == "K":
                self.order = self.seq[0].implementation.order
            if self.tracked_index == "multi":
                backward = False
            else:
                backward = self.order != self.tracked_index
            self.index_iter = IndexIterator(self.shape, backward=backward)

        # handle w_op_dtypes part 2: copy where needed if possible
        if len(self.dtypes) > 0:
            for i in range(len(self.seq)):
                selfd = self.dtypes[i]
                seq_d = self.seq[i].get_dtype()
                if not selfd:
                    self.dtypes[i] = seq_d
                elif selfd != seq_d:
                    if not 'r' in self.op_flags[i].tmp_copy:
                        raise oefmt(space.w_TypeError,
                                    "Iterator operand required copying or "
                                    "buffering for operand %d", i)
                    impl = self.seq[i].implementation
                    new_impl = impl.astype(space, selfd)
                    self.seq[i] = W_NDimArray(new_impl)
        else:
            #copy them from seq
            self.dtypes = [s.get_dtype() for s in self.seq]

        # create an iterator for each operand
        self.iters = []
        for i in range(len(self.seq)):
            it = get_iter(space, self.order, self.seq[i], self.shape,
                          self.dtypes[i], self.op_flags[i], self)
            it.contiguous = False
            self.iters.append((it, it.reset()))

        if self.external_loop:
            coalesce_axes(self, space)

    def set_op_axes(self, space, w_op_axes):
        if space.len_w(w_op_axes) != len(self.seq):
            raise oefmt(space.w_ValueError,
                        "op_axes must be a tuple/list matching the number of ops")
        op_axes = space.listview(w_op_axes)
        oa_ndim = -1
        for w_axis in op_axes:
            if not space.is_none(w_axis):
                axis_len = space.len_w(w_axis)
                if oa_ndim == -1:
                    oa_ndim = axis_len
                elif axis_len != oa_ndim:
                    raise oefmt(space.w_ValueError,
                                "Each entry of op_axes must have the same size")
                self.op_axes.append([space.int_w(x) if not space.is_none(x) else -1
                                     for x in space.listview(w_axis)])
        if oa_ndim == -1:
            raise oefmt(space.w_ValueError,
                        "If op_axes is provided, at least one list of axes "
                        "must be contained within it")
        raise oefmt(space.w_NotImplementedError, "op_axis not finished yet")
        # Check that values make sense:
        # - in bounds for each operand
        # ValueError: Iterator input op_axes[0][3] (==3) is not a valid axis of op[0], which has 2 dimensions
        # - no repeat axis
        # ValueError: The 'op_axes' provided to the iterator constructor for operand 1 contained duplicate value 0
        return oa_ndim

    def descr_iter(self, space):
        return space.wrap(self)

    def getitem(self, it, st):
        res = it.getoperand(st)
        return W_NDimArray(res)

    def descr_getitem(self, space, w_idx):
        idx = space.int_w(w_idx)
        try:
            it, st = self.iters[idx]
        except IndexError:
            raise oefmt(space.w_IndexError,
                        "Iterator operand index %d is out of bounds", idx)
        return self.getitem(it, st)

    def descr_setitem(self, space, w_idx, w_value):
        raise oefmt(space.w_NotImplementedError, "not implemented yet")

    def descr_len(self, space):
        space.wrap(len(self.iters))

    @jit.unroll_safe
    def descr_next(self, space):
        for it, st in self.iters:
            if not it.done(st):
                break
        else:
            self.done = True
            raise OperationError(space.w_StopIteration, space.w_None)
        res = []
        if self.index_iter:
            if not self.first_next:
                self.index_iter.next()
            else:
                self.first_next = False
        for i, (it, st) in enumerate(self.iters):
            res.append(self.getitem(it, st))
            self.iters[i] = (it, it.next(st))
        if len(res) < 2:
            return res[0]
        return space.newtuple(res)

    def iternext(self):
        if self.index_iter:
            self.index_iter.next()
        for i, (it, st) in enumerate(self.iters):
            self.iters[i] = (it, it.next(st))
        for it, st in self.iters:
            if not it.done(st):
                break
        else:
            self.done = True
            return self.done
        return self.done

    def descr_iternext(self, space):
        return space.wrap(self.iternext())

    def descr_copy(self, space):
        raise oefmt(space.w_NotImplementedError, "not implemented yet")

    def descr_debug_print(self, space):
        raise oefmt(space.w_NotImplementedError, "not implemented yet")

    def descr_enable_external_loop(self, space):
        raise oefmt(space.w_NotImplementedError, "not implemented yet")

    @unwrap_spec(axis=int)
    def descr_remove_axis(self, space, axis):
        raise oefmt(space.w_NotImplementedError, "not implemented yet")

    def descr_remove_multi_index(self, space, w_multi_index):
        raise oefmt(space.w_NotImplementedError, "not implemented yet")

    def descr_reset(self, space):
        raise oefmt(space.w_NotImplementedError, "not implemented yet")

    def descr_get_operands(self, space):
        l_w = []
        for op in self.seq:
            l_w.append(op.descr_view(space))
        return space.newlist(l_w)

    def descr_get_dtypes(self, space):
        res = [None] * len(self.seq)
        for i in range(len(self.seq)):
            res[i] = self.seq[i].descr_get_dtype(space)
        return space.newtuple(res)

    def descr_get_finished(self, space):
        return space.wrap(self.done)

    def descr_get_has_delayed_bufalloc(self, space):
        raise oefmt(space.w_NotImplementedError, "not implemented yet")

    def descr_get_has_index(self, space):
        return space.wrap(self.tracked_index in ["C", "F"])

    def descr_get_index(self, space):
        if not self.tracked_index in ["C", "F"]:
            raise oefmt(space.w_ValueError, "Iterator does not have an index")
        if self.done:
            raise oefmt(space.w_ValueError, "Iterator is past the end")
        return space.wrap(self.index_iter.getvalue())

    def descr_get_has_multi_index(self, space):
        return space.wrap(self.tracked_index == "multi")

    def descr_get_multi_index(self, space):
        if not self.tracked_index == "multi":
            raise oefmt(space.w_ValueError, "Iterator is not tracking a multi-index")
        if self.done:
            raise oefmt(space.w_ValueError, "Iterator is past the end")
        return space.newtuple([space.wrap(x) for x in self.index_iter.index])

    def descr_get_iterationneedsapi(self, space):
        raise oefmt(space.w_NotImplementedError, "not implemented yet")

    def descr_get_iterindex(self, space):
        raise oefmt(space.w_NotImplementedError, "not implemented yet")

    def descr_get_itersize(self, space):
        return space.wrap(support.product(self.shape))

    def descr_get_itviews(self, space):
        raise oefmt(space.w_NotImplementedError, "not implemented yet")

    def descr_get_ndim(self, space):
        return space.wrap(self.ndim)

    def descr_get_nop(self, space):
        raise oefmt(space.w_NotImplementedError, "not implemented yet")

    def descr_get_shape(self, space):
        raise oefmt(space.w_NotImplementedError, "not implemented yet")

    def descr_get_value(self, space):
        raise oefmt(space.w_NotImplementedError, "not implemented yet")


@unwrap_spec(w_flags=WrappedDefault(None), w_op_flags=WrappedDefault(None),
             w_op_dtypes=WrappedDefault(None), order=str,
             w_casting=WrappedDefault(None), w_op_axes=WrappedDefault(None),
             w_itershape=WrappedDefault(None), buffersize=int)
def descr_new_nditer(space, w_subtype, w_seq, w_flags, w_op_flags, w_op_dtypes,
                 w_casting, w_op_axes, w_itershape, buffersize=0, order='K'):
    return W_NDIter(space, w_seq, w_flags, w_op_flags, w_op_dtypes, w_casting, w_op_axes,
                    w_itershape, buffersize, order)

W_NDIter.typedef = TypeDef('numpy.nditer',
    __new__ = interp2app(descr_new_nditer),

    __iter__ = interp2app(W_NDIter.descr_iter),
    __getitem__ = interp2app(W_NDIter.descr_getitem),
    __setitem__ = interp2app(W_NDIter.descr_setitem),
    __len__ = interp2app(W_NDIter.descr_len),

    next = interp2app(W_NDIter.descr_next),
    iternext = interp2app(W_NDIter.descr_iternext),
    copy = interp2app(W_NDIter.descr_copy),
    debug_print = interp2app(W_NDIter.descr_debug_print),
    enable_external_loop = interp2app(W_NDIter.descr_enable_external_loop),
    remove_axis = interp2app(W_NDIter.descr_remove_axis),
    remove_multi_index = interp2app(W_NDIter.descr_remove_multi_index),
    reset = interp2app(W_NDIter.descr_reset),

    operands = GetSetProperty(W_NDIter.descr_get_operands),
    dtypes = GetSetProperty(W_NDIter.descr_get_dtypes),
    finished = GetSetProperty(W_NDIter.descr_get_finished),
    has_delayed_bufalloc = GetSetProperty(W_NDIter.descr_get_has_delayed_bufalloc),
    has_index = GetSetProperty(W_NDIter.descr_get_has_index),
    index = GetSetProperty(W_NDIter.descr_get_index),
    has_multi_index = GetSetProperty(W_NDIter.descr_get_has_multi_index),
    multi_index = GetSetProperty(W_NDIter.descr_get_multi_index),
    iterationneedsapi = GetSetProperty(W_NDIter.descr_get_iterationneedsapi),
    iterindex = GetSetProperty(W_NDIter.descr_get_iterindex),
    itersize = GetSetProperty(W_NDIter.descr_get_itersize),
    itviews = GetSetProperty(W_NDIter.descr_get_itviews),
    ndim = GetSetProperty(W_NDIter.descr_get_ndim),
    nop = GetSetProperty(W_NDIter.descr_get_nop),
    shape = GetSetProperty(W_NDIter.descr_get_shape),
    value = GetSetProperty(W_NDIter.descr_get_value),
)
W_NDIter.typedef.acceptable_as_base_class = False
