diff --git a/doc/source/whatsnew/v0.25.0.rst b/doc/source/whatsnew/v0.25.0.rst index fe1e2d7826d62..d17f1caa45bcd 100644 --- a/doc/source/whatsnew/v0.25.0.rst +++ b/doc/source/whatsnew/v0.25.0.rst @@ -888,6 +888,7 @@ Other API changes - :meth:`ExtensionArray.argsort` places NA values at the end of the sorted array. (:issue:`21801`) - :meth:`DataFrame.to_hdf` and :meth:`Series.to_hdf` will now raise a ``NotImplementedError`` when saving a :class:`MultiIndex` with extention data types for a ``fixed`` format. (:issue:`7775`) - Passing duplicate ``names`` in :meth:`read_csv` will now raise a ``ValueError`` (:issue:`17346`) +- :meth:`Categorical.ravel` will now return a :class:`Categorical` instead of a NumPy array. (:issue:`27153`) .. _whatsnew_0250.deprecations: diff --git a/pandas/core/arrays/__init__.py b/pandas/core/arrays/__init__.py index 5c83ed8cf5e24..31d67531795f9 100644 --- a/pandas/core/arrays/__init__.py +++ b/pandas/core/arrays/__init__.py @@ -1,3 +1,4 @@ +from ._reshaping import implement_2d # noqa:F401 from .base import ( # noqa: F401 ExtensionArray, ExtensionOpsMixin, diff --git a/pandas/core/arrays/_reshaping.py b/pandas/core/arrays/_reshaping.py new file mode 100644 index 0000000000000..931e59678db7e --- /dev/null +++ b/pandas/core/arrays/_reshaping.py @@ -0,0 +1,240 @@ +""" +Utilities for implementing 2D compatibility for 1D ExtensionArrays. +""" +from functools import wraps +from typing import Tuple + +import numpy as np + +from pandas._libs.lib import is_integer + + +def implement_2d(cls): + """ + A decorator to take a 1-dimension-only ExtensionArray subclass and make + it support limited 2-dimensional operations. + """ + from pandas.core.arrays import ExtensionArray + + # For backwards-compatibility, if an EA author implemented __len__ + # but not size, we use that __len__ method to get an array's size. + has_size = cls.size is not ExtensionArray.size + has_shape = cls.shape is not ExtensionArray.shape + has_len = cls.__len__ is not ExtensionArray.__len__ + + if not has_size and has_len: + cls.size = property(cls.__len__) + cls.__len__ = ExtensionArray.__len__ + + elif not has_size and has_shape: + + @property + def size(self) -> int: + return np.prod(self.shape) + + cls.size = size + + orig_copy = cls.copy + + @wraps(orig_copy) + def copy(self): + result = orig_copy(self) + result._shape = self._shape + return result + + cls.copy = copy + + orig_getitem = cls.__getitem__ + + def __getitem__(self, key): + if self.ndim == 1: + return orig_getitem(self, key) + + key = expand_key(key, self.shape) + if is_integer(key[0]): + assert key[0] in [0, -1] + result = orig_getitem(self, key[1]) + return result + + if isinstance(key[0], slice): + if slice_contains_zero(key[0]): + result = orig_getitem(self, key[1]) + result._shape = (1, result.size) + return result + + raise NotImplementedError(key) + # TODO: ellipses? + raise NotImplementedError(key) + + cls.__getitem__ = __getitem__ + + orig_take = cls.take + + # kwargs for compat with Interval + # allow_fill=None instead of False is for compat with Categorical + def take(self, indices, allow_fill=None, fill_value=None, axis=0, **kwargs): + if self.ndim == 1 and axis == 0: + return orig_take( + self, indices, allow_fill=allow_fill, fill_value=fill_value, **kwargs + ) + + if self.ndim != 2 or self.shape[0] != 1: + raise NotImplementedError + if axis not in [0, 1]: + raise ValueError(axis) + if kwargs: + raise ValueError( + "kwargs should not be passed in the 2D case, " + "are only included for compat with Interval" + ) + + if axis == 1: + result = orig_take( + self, indices, allow_fill=allow_fill, fill_value=fill_value + ) + result._shape = (1, result.size) + return result + + # For axis == 0, because we only support shape (1, N) + # there are only limited indices we can accept + if len(indices) != 1: + # TODO: we could probably support zero-len here + raise NotImplementedError + + def take_item(n): + if n == -1: + seq = [fill_value] * self.shape[1] + return type(self)._from_sequence(seq) + else: + return self[n, :] + + arrs = [take_item(n) for n in indices] + result = type(self)._concat_same_type(arrs) + result.shape = (len(indices), self.shape[1]) + return result + + cls.take = take + + orig_iter = cls.__iter__ + + def __iter__(self): + if self.ndim == 1: + for obj in orig_iter(self): + yield obj + else: + for n in range(self.shape[0]): + yield self[n] + + cls.__iter__ = __iter__ + + return cls + + +def slice_contains_zero(slc: slice) -> bool: + if slc == slice(None): + return True + if slc == slice(0, None): + return True + if slc == slice(0, 1): + return True + if slc.start == slc.stop: + # Note: order matters here, since we _dont_ want this to catch + # the slice(None) case. + return False + raise NotImplementedError(slc) + + +def expand_key(key, shape): + ndim = len(shape) + if ndim != 2 or shape[0] != 1: + raise NotImplementedError + if not isinstance(key, tuple): + key = (key, slice(None)) + if len(key) != 2: + raise ValueError(key) + + if is_integer(key[0]) and key[0] not in [0, -1]: + raise ValueError(key) + + return key + + +def can_safe_ravel(shape: Tuple[int, ...]) -> bool: + """ + Check if an array with the given shape can be ravelled unambiguously + regardless of column/row order. + + Parameters + ---------- + shape : tuple[int] + + Returns + ------- + bool + """ + if len(shape) == 1: + return True + if len(shape) > 2: + raise NotImplementedError(shape) + if shape[0] == 1 or shape[1] == 1: + # column-like or row-like + return True + return False + + +def tuplify_shape(size: int, shape, restrict=True) -> Tuple[int, ...]: + """ + Convert a passed shape into a valid tuple. + Following ndarray.reshape, we accept either `reshape(a, b)` or + `reshape((a, b))`, the latter being canonical. + + Parameters + ---------- + size : int + shape : tuple + restrict : bool, default True + Whether to restrict to shapes (N), (1,N), and (N,1) + + Returns + ------- + tuple[int, ...] + """ + if len(shape) == 0: + raise ValueError("shape must be a non-empty tuple of integers", shape) + + if len(shape) == 1: + if is_integer(shape[0]): + pass + else: + shape = shape[0] + if not isinstance(shape, tuple): + raise ValueError("shape must be a non-empty tuple of integers", shape) + + if not all(is_integer(x) for x in shape): + raise ValueError("shape must be a non-empty tuple of integers", shape) + + if any(x < -1 for x in shape): + raise ValueError("Invalid shape {shape}".format(shape=shape)) + + if -1 in shape: + if shape.count(-1) != 1: + raise ValueError("Invalid shape {shape}".format(shape=shape)) + idx = shape.index(-1) + others = [n for n in shape if n != -1] + prod = np.prod(others) + dim = size // prod + shape = shape[:idx] + (dim,) + shape[idx + 1 :] + + if np.prod(shape) != size: + raise ValueError( + "Product of shape ({shape}) must match " + "size ({size})".format(shape=shape, size=size) + ) + + num_gt1 = len([x for x in shape if x > 1]) + if num_gt1 > 1 and restrict: + raise ValueError( + "The default `reshape` implementation is limited to " + "shapes (N,), (N,1), and (1,N), not {shape}".format(shape=shape) + ) + return shape diff --git a/pandas/core/arrays/base.py b/pandas/core/arrays/base.py index 5c121172d0e4f..3d190800765be 100644 --- a/pandas/core/arrays/base.py +++ b/pandas/core/arrays/base.py @@ -24,6 +24,7 @@ from pandas._typing import ArrayLike from pandas.core import ops from pandas.core.algorithms import _factorize_array, unique +from pandas.core.arrays._reshaping import can_safe_ravel, tuplify_shape from pandas.core.missing import backfill_1d, pad_1d from pandas.core.sorting import nargsort @@ -83,7 +84,7 @@ class ExtensionArray: * _from_sequence * _from_factorized * __getitem__ - * __len__ + * __len__ *or* size * dtype * nbytes * isna @@ -160,6 +161,12 @@ class ExtensionArray: # Don't override this. _typ = "extension" + # Whether this class supports 2D arrays natively. If so, set _allows_2d + # to True and override reshape, ravel, and T. Otherwise, apply the + # `implement_2d` decorator to use default implementations of limited + # 2D functionality. + _allows_2d = False + # ------------------------------------------------------------------------ # Constructors # ------------------------------------------------------------------------ @@ -319,7 +326,7 @@ def __len__(self) -> int: ------- length : int """ - raise AbstractMethodError(self) + return self.shape[0] def __iter__(self): """ @@ -334,6 +341,7 @@ def __iter__(self): # ------------------------------------------------------------------------ # Required attributes # ------------------------------------------------------------------------ + _shape = None @property def dtype(self) -> ExtensionDtype: @@ -347,14 +355,33 @@ def shape(self) -> Tuple[int, ...]: """ Return a tuple of the array dimensions. """ - return (len(self),) + if self._shape is not None: + return self._shape + + # Default to 1D + length = self.size + return (length,) + + @shape.setter + def shape(self, value): + size = np.prod(value) + if size != self.size: + raise ValueError("Implied size must match actual size.") + self._shape = value @property def ndim(self) -> int: """ Extension Arrays are only allowed to be 1-dimensional. """ - return 1 + return len(self.shape) + + @property + def size(self) -> int: + """ + The number of elements in this array. + """ + raise AbstractMethodError(self) @property def nbytes(self) -> int: @@ -933,6 +960,26 @@ def _formatter(self, boxed: bool = False) -> Callable[[Any], Optional[str]]: # Reshaping # ------------------------------------------------------------------------ + def reshape(self, *shape): + """ + Return a view on this array with the given shape. + """ + # numpy accepts either a single tuple or an expanded tuple + shape = tuplify_shape(self.size, shape) + result = self.view() + result._shape = shape + return result + + @property + def T(self) -> ABCExtensionArray: + """ + Return a transposed view on self. + """ + if not can_safe_ravel(self.shape): + raise NotImplementedError + shape = self.shape[::-1] + return self.reshape(shape) + def ravel(self, order="C") -> ABCExtensionArray: """ Return a flattened view on this array. @@ -947,10 +994,12 @@ def ravel(self, order="C") -> ABCExtensionArray: Notes ----- - - Because ExtensionArrays are 1D-only, this is a no-op. - The "order" argument is ignored, is for compatibility with NumPy. """ - return self + if not can_safe_ravel(self.shape): + raise NotImplementedError + shape = (self.size,) + return self.reshape(shape) @classmethod def _concat_same_type( diff --git a/pandas/core/arrays/categorical.py b/pandas/core/arrays/categorical.py index 9862b4b530424..8afd275a0f02e 100644 --- a/pandas/core/arrays/categorical.py +++ b/pandas/core/arrays/categorical.py @@ -62,6 +62,7 @@ from pandas.io.formats import console +from ._reshaping import implement_2d from .base import ExtensionArray, _extension_array_shared_docs _take_msg = textwrap.dedent( @@ -225,6 +226,7 @@ def contains(cat, key, container): """ +@implement_2d class Categorical(ExtensionArray, PandasObject): """ Represent a categorical variable in classic R / S-plus fashion. @@ -1248,19 +1250,6 @@ def map(self, mapper): __ge__ = _cat_compare_op("__ge__") # for Series/ndarray like compat - @property - def shape(self): - """ - Shape of the Categorical. - - For internal compatibility with numpy arrays. - - Returns - ------- - shape : tuple - """ - - return tuple([len(self._codes)]) def shift(self, periods, fill_value=None): """ @@ -1935,7 +1924,7 @@ def take_nd(self, indexer, allow_fill=None, fill_value=None): indexer = np.asarray(indexer, dtype=np.intp) if allow_fill is None: if (indexer < 0).any(): - warn(_take_msg, FutureWarning, stacklevel=2) + warn(_take_msg, FutureWarning, stacklevel=3) allow_fill = True dtype = self.dtype @@ -1956,6 +1945,23 @@ def take_nd(self, indexer, allow_fill=None, fill_value=None): take = take_nd + def _slice(self, slicer): + """ + Return a slice of myself. + + For internal compatibility with numpy arrays. + """ + + # only allow 1 dimensional slicing, but can + # in a 2-d case be passd (slice(None),....) + if isinstance(slicer, tuple) and len(slicer) == 2: + if not com.is_null_slice(slicer[0]): + raise AssertionError("invalid slicing for a 1-ndim " "categorical") + slicer = slicer[1] + + codes = self._codes[slicer] + return self._constructor(values=codes, dtype=self.dtype, fastpath=True) + def __len__(self): """ The length of this Categorical. diff --git a/pandas/core/arrays/datetimelike.py b/pandas/core/arrays/datetimelike.py index 0372b8f0c080a..3387187d7670a 100644 --- a/pandas/core/arrays/datetimelike.py +++ b/pandas/core/arrays/datetimelike.py @@ -52,6 +52,7 @@ from pandas.tseries import frequencies from pandas.tseries.offsets import DateOffset, Tick +from ._reshaping import implement_2d from .base import ExtensionArray, ExtensionOpsMixin @@ -317,6 +318,7 @@ def ceil(self, freq, ambiguous="raise", nonexistent="raise"): return self._round(freq, RoundTo.PLUS_INFTY, ambiguous, nonexistent) +@implement_2d class DatetimeLikeArrayMixin(ExtensionOpsMixin, AttributesMixin, ExtensionArray): """ Shared Base/Mixin class for DatetimeArray, TimedeltaArray, PeriodArray @@ -394,11 +396,7 @@ def __array__(self, dtype=None): @property def size(self) -> int: - """The number of elements in this array.""" - return np.prod(self.shape) - - def __len__(self): - return len(self._data) + return self._data.size def __getitem__(self, key): """ diff --git a/pandas/core/arrays/integer.py b/pandas/core/arrays/integer.py index 1f14bd169a228..065ca056c005d 100644 --- a/pandas/core/arrays/integer.py +++ b/pandas/core/arrays/integer.py @@ -26,7 +26,7 @@ from pandas.core import nanops, ops from pandas.core.algorithms import take -from pandas.core.arrays import ExtensionArray, ExtensionOpsMixin +from pandas.core.arrays import ExtensionArray, ExtensionOpsMixin, implement_2d from pandas.core.tools.numeric import to_numeric @@ -232,6 +232,7 @@ def coerce_to_array(values, dtype, mask=None, copy=False): return values, mask +@implement_2d class IntegerArray(ExtensionArray, ExtensionOpsMixin): """ Array of integer (optional missing) values. @@ -461,8 +462,9 @@ def __setitem__(self, key, value): self._data[key] = value self._mask[key] = mask - def __len__(self): - return len(self._data) + @property + def size(self) -> int: + return self._data.size @property def nbytes(self): diff --git a/pandas/core/arrays/interval.py b/pandas/core/arrays/interval.py index 9cb2721b33634..39947a25401ec 100644 --- a/pandas/core/arrays/interval.py +++ b/pandas/core/arrays/interval.py @@ -34,6 +34,7 @@ from pandas.core.dtypes.missing import isna, notna from pandas.core.algorithms import take, value_counts +from pandas.core.arrays._reshaping import implement_2d from pandas.core.arrays.base import ExtensionArray, _extension_array_shared_docs from pandas.core.arrays.categorical import Categorical import pandas.core.common as com @@ -140,6 +141,7 @@ ), ) ) +@implement_2d class IntervalArray(IntervalMixin, ExtensionArray): ndim = 1 can_hold_na = True @@ -460,9 +462,6 @@ def _validate(self): def __iter__(self): return iter(np.asarray(self)) - def __len__(self): - return len(self.left) - def __getitem__(self, value): left = self.left[value] right = self.right[value] diff --git a/pandas/core/arrays/numpy_.py b/pandas/core/arrays/numpy_.py index 4e2e37d88eb9a..9235295933e8a 100644 --- a/pandas/core/arrays/numpy_.py +++ b/pandas/core/arrays/numpy_.py @@ -19,6 +19,7 @@ from pandas.core.construction import extract_array from pandas.core.missing import backfill_1d, pad_1d +from ._reshaping import implement_2d from .base import ExtensionArray, ExtensionOpsMixin @@ -86,6 +87,7 @@ def itemsize(self): return self._dtype.itemsize +@implement_2d class PandasArray(ExtensionArray, ExtensionOpsMixin, NDArrayOperatorsMixin): """ A pandas ExtensionArray for NumPy data. @@ -245,8 +247,9 @@ def __setitem__(self, key, value): else: self._ndarray[key] = value - def __len__(self) -> int: - return len(self._ndarray) + @property + def size(self) -> int: + return self._ndarray.size @property def nbytes(self) -> int: diff --git a/pandas/core/arrays/sparse.py b/pandas/core/arrays/sparse.py index 8aa83c3fbc37d..6c2a0586534a8 100644 --- a/pandas/core/arrays/sparse.py +++ b/pandas/core/arrays/sparse.py @@ -49,7 +49,7 @@ from pandas._typing import Dtype from pandas.core.accessor import PandasDelegate, delegate_names import pandas.core.algorithms as algos -from pandas.core.arrays import ExtensionArray, ExtensionOpsMixin +from pandas.core.arrays import ExtensionArray, ExtensionOpsMixin, implement_2d from pandas.core.base import PandasObject import pandas.core.common as com from pandas.core.construction import sanitize_array @@ -524,6 +524,7 @@ def _wrap_result(name, data, sparse_index, fill_value, dtype=None): ) +@implement_2d class SparseArray(PandasObject, ExtensionArray, ExtensionOpsMixin): """ An ExtensionArray for storing sparse data. @@ -854,7 +855,8 @@ def _valid_sp_values(self): mask = notna(sp_vals) return sp_vals[mask] - def __len__(self) -> int: + @property + def size(self) -> int: return self.sp_index.length @property diff --git a/pandas/tests/arrays/test_reshaping.py b/pandas/tests/arrays/test_reshaping.py new file mode 100644 index 0000000000000..6bf3d41ce32d3 --- /dev/null +++ b/pandas/tests/arrays/test_reshaping.py @@ -0,0 +1,58 @@ +""" +Tests for reshaping utilities. +""" +import pytest + +from pandas.core.arrays._reshaping import tuplify_shape + + +class TestTuplify: + def test_tuplify_single_arg(self): + # Single-tuple cases, i.e. + # arr.reshape((x, y)) + shape = tuplify_shape(3, ((3,),)) + assert shape == (3,) + + shape = tuplify_shape(3, ((1, 3),)) + assert shape == (1, 3) + + shape = tuplify_shape(3, ((3, 1),)) + assert shape == (3, 1) + + def test_tuplify_multi_arg(self): + # Multi-arg cases, i.e. + # arr.reshape(x, y) + shape = tuplify_shape(3, (3,)) + assert shape == (3,) + + shape = tuplify_shape(3, (3, 1)) + assert shape == (3, 1) + + shape = tuplify_shape(3, (1, 3)) + assert shape == (1, 3) + + def test_tuplify_minus_one(self): + shape = tuplify_shape(4, (1, -1)) + assert shape == (1, 4) + + shape = tuplify_shape(4, (-1, 1)) + assert shape == (4, 1) + + def test_tuplify_minus_one_factors(self): + shape = tuplify_shape(4, (1, -1, 2), restrict=False) + assert shape == (1, 2, 2) + + def test_tuplify_multiple_minus_ones(self): + # No more than 1 "-1" + with pytest.raises(ValueError, match="Invalid shape"): + tuplify_shape(99, (-1, -1)) + + def test_tuplify_negative(self): + # Nothing less than -1 in a shape + with pytest.raises(ValueError, match="Invalid shape"): + tuplify_shape(99, (-2, 3)) + + def test_tuplify_size_match(self): + # must match original size + with pytest.raises(ValueError, match="Product of shape"): + tuplify_shape(3, (2, 2)) diff --git a/pandas/tests/extension/arrow/arrays.py b/pandas/tests/extension/arrow/arrays.py index 6a28f76e474cc..3cbf1862d55ab 100644 --- a/pandas/tests/extension/arrow/arrays.py +++ b/pandas/tests/extension/arrow/arrays.py @@ -18,6 +18,7 @@ register_extension_dtype, take, ) +from pandas.core.arrays import implement_2d @register_extension_dtype @@ -63,6 +64,7 @@ def construct_array_type(cls): return ArrowStringArray +@implement_2d class ArrowExtensionArray(ExtensionArray): @classmethod def from_scalars(cls, values): @@ -88,7 +90,8 @@ def __getitem__(self, item): vals = self._data.to_pandas()[item] return type(self).from_scalars(vals) - def __len__(self): + @property + def size(self) -> int: return len(self._data) def astype(self, dtype, copy=True): diff --git a/pandas/tests/extension/arrow/test_bool.py b/pandas/tests/extension/arrow/test_bool.py index 9c53210b75d6b..8faad4d7281af 100644 --- a/pandas/tests/extension/arrow/test_bool.py +++ b/pandas/tests/extension/arrow/test_bool.py @@ -15,13 +15,17 @@ def dtype(): return ArrowBoolDtype() -@pytest.fixture def data(): values = np.random.randint(0, 2, size=100, dtype=bool) values[1] = ~values[0] return ArrowBoolArray.from_scalars(values) +@pytest.fixture(name="data") +def data_fixture(): + return data() + + @pytest.fixture def data_missing(): return ArrowBoolArray.from_scalars([None, True]) diff --git a/pandas/tests/extension/base/interface.py b/pandas/tests/extension/base/interface.py index a29f6deeffae6..d5fc4b134ad85 100644 --- a/pandas/tests/extension/base/interface.py +++ b/pandas/tests/extension/base/interface.py @@ -76,6 +76,13 @@ def test_copy(self, data): data[1] = data[0] assert result[1] != result[0] + def test_copy_preserves_shape(self, data): + data_2d = data.reshape(1, -1) + assert data_2d.shape == (1, len(data)), data_2d.shape + + copied = data_2d.copy() + assert copied.shape == data_2d.shape, (copied.shape, data_2d.shape) + def test_view(self, data): # view with no dtype should return a shallow copy, *not* the same # object diff --git a/pandas/tests/extension/decimal/array.py b/pandas/tests/extension/decimal/array.py index a1988744d76a1..be91c3fd5d07a 100644 --- a/pandas/tests/extension/decimal/array.py +++ b/pandas/tests/extension/decimal/array.py @@ -9,7 +9,7 @@ import pandas as pd from pandas.api.extensions import register_extension_dtype -from pandas.core.arrays import ExtensionArray, ExtensionScalarOpsMixin +from pandas.core.arrays import ExtensionArray, ExtensionScalarOpsMixin, implement_2d @register_extension_dtype @@ -47,6 +47,7 @@ def _is_numeric(self): return True +@implement_2d class DecimalArray(ExtensionArray, ExtensionScalarOpsMixin): __array_priority__ = 1000 @@ -137,8 +138,9 @@ def __setitem__(self, key, value): value = decimal.Decimal(value) self._data[key] = value - def __len__(self) -> int: - return len(self._data) + @property + def size(self) -> int: + return self._data.size @property def nbytes(self) -> int: diff --git a/pandas/tests/extension/json/array.py b/pandas/tests/extension/json/array.py index b64ddbd6ac84d..07fb0cbb672c4 100644 --- a/pandas/tests/extension/json/array.py +++ b/pandas/tests/extension/json/array.py @@ -21,7 +21,7 @@ from pandas.core.dtypes.base import ExtensionDtype -from pandas.core.arrays import ExtensionArray +from pandas.core.arrays import ExtensionArray, implement_2d class JSONDtype(ExtensionDtype): @@ -47,6 +47,7 @@ def construct_from_string(cls, string): raise TypeError("Cannot construct a '{}' from '{}'".format(cls, string)) +@implement_2d class JSONArray(ExtensionArray): dtype = JSONDtype() __array_priority__ = 1000 @@ -106,7 +107,8 @@ def __setitem__(self, key, value): assert isinstance(v, self.dtype.type) self.data[k] = v - def __len__(self) -> int: + @property + def size(self) -> int: return len(self.data) @property diff --git a/pandas/tests/extension/test_interval.py b/pandas/tests/extension/test_interval.py index 4fdcf930d224f..505b81e88b88e 100644 --- a/pandas/tests/extension/test_interval.py +++ b/pandas/tests/extension/test_interval.py @@ -143,7 +143,9 @@ def test_non_scalar_raises(self, data_missing): class TestReshaping(BaseInterval, base.BaseReshapingTests): - pass + @pytest.mark.xfail(reason="setitem incorrectly makes copy, see GH#27147") + def test_ravel(self, data): + super().test_ravel(data) class TestSetitem(BaseInterval, base.BaseSetitemTests):