Module mpython.core.wrapped_types

Classes

class AnyWrappedArray
Expand source code
class AnyWrappedArray(AnyMatlabArray):
    """Base class for wrapped numpy/scipy arrays."""

    @classmethod
    def _parse_args(cls, *args, **kwargs):
        """
        This function is used in the `__new__` constructor of
        Array/Cell/Struct.

        It does some preliminary preprocesing to reduces the number of
        cases that must be handled by `__new__`.

        In particular:

        * It converts multiple integer arguments to a single list[int]
        * It extracts the shape or object to copy, if there is one.
        * It convert positional dtype/order into keywords.

        Returns
        -------
        mode : {"shape", "obj"}
        arg : array-like | list[int]
        kwargs : dict
        """
        __has_dtype = kwargs.pop("__has_dtype", True)
        __has_order = kwargs.pop("__has_order", True)

        # Detect integer arguments
        args, shape, obj = list(args), [], None
        while args and isinstance(args[0], int):
            shape.append(args.pop(0))

        # If no integer arguments, the first argument (if it exists)
        # must be a shape or an array-like object to convert.
        if not shape:
            # Catch case where no size/array is passed and the first
            # argument is a data type.
            if args and not isinstance(args[0], (str, np.dtype, type)):
                obj = args.pop(0)

        # If there are positional arguments remaining, they are:
        # 1. dtype
        if args and __has_dtype:
            if "dtype" in kwargs:
                raise TypeError(
                    f"{cls.__name__}() got multiple values for argument "
                    f"'dtype'"
                )
            kwargs["dtype"] = args.pop(0)
        # 2. order {"C", "F"}
        if args and __has_order:
            if "order" in kwargs:
                raise TypeError(
                    f"{cls.__name__}() got multiple values for argument "
                    f"'order'"
                )
            kwargs["order"] = args.pop(0)
        # 3. no other positionals allowed -> raise
        if args:
            raise TypeError(
                f"{cls.__name__}() takes from 1 to 3 positional "
                "arguments but more were given"
            )

        # If we found an object and it is a generator
        # (= an iterable that has no `len`), copy its values into a list.
        if hasattr(obj, "__iter__") and not hasattr(obj, "__len__"):
            # save iterator values in a list
            obj = list(obj)

        # If obj is a list[int] -> it is a shape
        if (
            not shape
            and isinstance(obj, (list, tuple))
            and all(isinstance(x, int) for x in obj)
        ):
            shape, obj = obj, None

        mode = "obj" if obj is not None else "shape"
        arg = obj if obj is not None else shape
        return mode, arg, kwargs

Base class for wrapped numpy/scipy arrays.

Ancestors

Subclasses

Inherited members

class WrappedArray (...)
Expand source code
class WrappedArray(np.ndarray, AnyWrappedArray):
    """
    Base class for "arrays of things" (`Array`, `Cell`, `Struct`)
    """

    # Value used to initalize empty arrays
    @classmethod
    def _DEFAULT(cls, shape: list = ()):
        raise NotImplementedError

    def __str__(self):
        fmt = {"all": str}  # use str instead of repr for items
        return np.array2string(self, separator=", ", formatter=fmt)

    def __repr__(self):
        # close to np.array_repr, but hides dtype.
        pre = type(self).__name__ + "("
        suf = ")"
        arr = np.array2string(self, prefix=pre, suffix=suf, separator=", ")
        return pre + arr + suf

    def __bool__(self):
        # NumPy arrays do not lower to True/False in a boolean context.
        # We do lower our matlab equivalent using all()
        return np.ndarray.view(np.all(self), np.ndarray).item()

    def __iter__(self):
        # FIXME:
        #   ndarray.__iter__ seems to call __getattr__, which leads
        #   to infinite resizing.
        #   This overload seems to fix it, but may not be computationally
        #   optimal.
        for i in range(len(self)):
            yield self[i]

    def __getitem__(self, index):
        """Resize array if needed, then fallback to `np.ndarray` indexing."""
        try:
            return super().__getitem__(index)
        except IndexError:
            # We return a delayed version of the current type, with the
            # same shape as the requested view. Its elements will only
            # be inserted into the original object (self) is the view
            # is properly finalized by an eventual call to __setitem__
            # or __setattr__.
            return self._return_delayed(index)

    def __setitem__(self, index, value):
        """Resize array if needed, then fallback to `np.ndarray` indexing."""
        value = MatlabType.from_any(value)
        try:
            return super().__setitem__(index, value)
        except (IndexError, ValueError):
            self._resize_for_index(index)
            return super().__setitem__(index, value)

    def __delitem__(self, index):
        if isinstance(index, tuple):
            raise TypeError(
                "Multidimensional indices are not supported in `del`."
            )

        # --- list: delete sequentially, from tail to head -------------
        if hasattr(index, "__iter__"):
            index = (len(self) + i if i < 0 else i for i in index)
            index = sorted(index, reverse=True)
            for i in index:
                del self[i]

        # --- slice: skip the entire slice, if possible ----------------
        elif isinstance(index, slice):
            start, stop, step = index.start, index.stop, index.step

            # --- let's make the slice parameters a bit more useful ---
            step = step or 1
            # compute true start
            if start is None:
                if step < 0:
                    start = len(self) - 1
                else:
                    start = 0
            if start < 0:
                start = len(self) + start
            # compute stop in terms of "positive indices"
            # (where -1 really means -1, and not n-1)
            if stop is not None:
                if stop < 0:
                    stop = len(self) + stop
            else:
                stop = len(self) if step > 0 else -1
            stop = min(stop, len(self)) if step > 0 else max(stop, -1)
            # align stop with steps
            stop = start + int(np.ceil(abs(stop - start) / abs(step))) * step
            # compute true inclusive stop
            stop_inclusive = stop - step
            # ensure step is positive
            if step < 0:
                start, stop_inclusive, step = stop_inclusive, start, abs(step)

            # --- if non consecutive, fallback to sequential ---
            if step != 1:
                index = range(start, stop + 1, step)
                del self[index]

            # --- otherwise, skip the entire slice ---
            else:
                nb_del = 1 + stop_inclusive - start
                new_shape = list(np.shape(self))
                new_shape[0] -= nb_del
                self[start:-nb_del] = self[stop_inclusive:]
                np.ndarray.resize(self, new_shape, refcheck=False)

        # --- int: skip a single element -------------------------------
        else:
            index = int(index)
            if index < 0:
                index = len(self) + index
            new_shape = list(np.shape(self))
            new_shape[0] -= 1
            self[index:-1] = self[index + 1:]
            np.ndarray.resize(self, new_shape, refcheck=False)

    def _resize_for_index(self, index, set_default=True):
        """
        Resize the array so that the (multidimensional) index is not OOB.

        We only support a restricted number of cases:

        * Index should only contain integers and slices
          (no smart indexing, no new axis, no ellipsis)
        * Only integer indices are used to compute the new size.
          This is to be consistent with numpy, where slice-indexing never
          raises `IndexError` (but instead returns the overlap between
          the array and the slice -- eventually empty).

        Other cases could be handled but require much more complicated logic.
        """
        input_shape = self.shape
        if not isinstance(index, tuple):
            index = (index,)
        index, new_index = list(index), []
        shape, new_shape = list(np.shape(self)), []
        axis = -1
        while index:
            next_index = index.pop(0)
            if shape:
                next_shape = shape.pop(0)
            else:
                next_shape = 1
            axis += 1
            if isinstance(next_index, int):
                if next_index < 0:
                    next_index = next_shape + next_index
                if next_index >= next_shape:
                    next_shape = next_index + 1
            elif isinstance(next_index, slice):
                # FIXME: this is not exactly right when abs(step) != 1
                step = next_index.step or 1
                start = next_index.start
                stop = next_index.stop
                if start is not None:
                    start = next_shape + start if start < 0 else start
                if stop is not None:
                    stop = next_shape + stop if stop < 0 else stop
                if step < 0:
                    max_index = start
                else:
                    max_index = stop
                if max_index is None:
                    max_index = next_shape
                if max_index > next_shape:
                    next_shape = max_index
            elif not isinstance(next_index, slice):
                raise TypeError(
                    "Can only automatically resize cell if simple "
                    "indexing (int, slice) is used."
                )
            new_index.append(next_index)
            new_shape.append(next_shape)
        new_shape = new_shape + shape
        if not input_shape:
            # We risk erasing the original scalar whn setting the
            # defaults, so we save it and reinsert it at the end.
            scalar = np.ndarray.view(self, np.ndarray).item()
        np.ndarray.resize(self, new_shape, refcheck=False)
        if set_default:
            arr = np.ndarray.view(self, np.ndarray)
            view_index = tuple(slice(x, None) for x in input_shape)
            view_shape = arr[view_index].shape
            new_data = self._DEFAULT(view_shape)
            arr[view_index] = new_data
            if not input_shape:
                # Insert back scalar in the first position.
                scalar_index = (0,) * arr.ndim
                arr[scalar_index] = scalar

    def _return_delayed(self, index):
        Cell = _imports.Cell
        Struct = _imports.Struct

        if not isinstance(index, tuple):
            index = (index,)

        #   Resize as if we were already performing a valid __setitem__.
        #   This helps us guess the shape of the view.
        #   Also, we'll hopefully be able to use the allocated space
        #   later if the caller did not mess up their syntax, so there's
        #   not much wasted performance.
        shape = self.shape
        self._resize_for_index(index, set_default=False)

        #   Ensure that the indexed view is an array, not a single item.
        index_for_view = index
        if ... not in index_for_view:
            index_for_view = index_for_view + (...,)

        sub_shape = np.ndarray.view(self, np.ndarray)[index_for_view].shape

        #   Now, undo resize so that if the caller's syntax is wrong and
        #   an exception is raised (and caught), it's as if nothing ever
        #   happened.
        np.ndarray.resize(self, shape, refcheck=False)

        #   If self is wrapped in a DelayedCell/DelayedStruct,
        #   reference wrapper instead of self.
        parent = getattr(self, "_delayed_wrapper", self)

        if isinstance(self, Cell):
            if sub_shape == ():
                return AnyDelayedArray(parent, index)
            else:
                return DelayedCell(sub_shape, parent, index)

        elif isinstance(self, Struct):
            return DelayedStruct(sub_shape, parent, index)

        else:
            #   In numeric arrays, only seeting OOB items is allowed.
            #   Getting OOB items should raise an error, which this
            #   call to the ndarray accessor will do.
            return super().__getitem__(index)

Base class for "arrays of things" (Array, Cell, Struct)

Ancestors

Subclasses

Inherited members