diff --git a/pandas/core/arrays/base.py b/pandas/core/arrays/base.py index 6340cc732d6c1..23c588bc4a108 100644 --- a/pandas/core/arrays/base.py +++ b/pandas/core/arrays/base.py @@ -830,6 +830,20 @@ def copy(self) -> ABCExtensionArray: """ raise AbstractMethodError(self) + def view(self, dtype=None) -> ABCExtensionArray: + """ + Return a view on the array. + + Returns + ------- + ExtensionArray + + Notes + ----- + This must return a *new* object, not self. + """ + raise AbstractMethodError(self) + # ------------------------------------------------------------------------ # Printing # ------------------------------------------------------------------------ @@ -897,6 +911,23 @@ def _formatting_values(self) -> np.ndarray: # Reshaping # ------------------------------------------------------------------------ + @property + def T(self) -> ABCExtensionArray: + """ + Return a transposed view on self. For 1-D arrays this is a no-op. + """ + if self.ndim != 1: + raise NotImplementedError + return self + + def ravel(self, order=None) -> ABCExtensionArray: + """ + Return a flattened view on self. For 1-D arrays this is a no-op. + """ + if self.ndim != 1: + raise NotImplementedError + return self + @classmethod def _concat_same_type( cls, diff --git a/pandas/core/arrays/categorical.py b/pandas/core/arrays/categorical.py index 3ef2f41f25338..fbffdd8d1e19d 100644 --- a/pandas/core/arrays/categorical.py +++ b/pandas/core/arrays/categorical.py @@ -1675,19 +1675,7 @@ def _values_for_rank(self): ) return values - def ravel(self, order='C'): - """ - Return a flattened (numpy) array. - - For internal compatibility with numpy arrays. - - Returns - ------- - numpy.array - """ - return np.array(self) - - def view(self): + def view(self, dtype=None): """ Return a view of myself. @@ -1698,7 +1686,11 @@ def view(self): view : Categorical Returns `self`! """ - return self + if dtype is not None: + return NotImplementedError(dtype) + return self._constructor(values=self._codes, + dtype=self.dtype, + fastpath=True) def to_dense(self): """ diff --git a/pandas/core/arrays/datetimelike.py b/pandas/core/arrays/datetimelike.py index 93166759d8dbd..e175fcfccdcb6 100644 --- a/pandas/core/arrays/datetimelike.py +++ b/pandas/core/arrays/datetimelike.py @@ -554,9 +554,11 @@ def view(self, dtype=None): Returns ------- - ndarray + ndarray or ExtensionArray With the specified `dtype`. """ + if dtype is None: + return type(self)(self._data, dtype=self.dtype, freq=self.freq) return self._data.view(dtype=dtype) # ------------------------------------------------------------------ diff --git a/pandas/core/arrays/integer.py b/pandas/core/arrays/integer.py index 88de497a3329f..7d445f8ab2f14 100644 --- a/pandas/core/arrays/integer.py +++ b/pandas/core/arrays/integer.py @@ -380,6 +380,11 @@ def copy(self): mask = mask.copy() return type(self)(data, mask, copy=False) + def view(self, dtype=None): + if dtype is not None: + raise NotImplementedError + return type(self)(self._data, self._mask, copy=False) + def __setitem__(self, key, value): _is_scalar = is_scalar(value) if _is_scalar: diff --git a/pandas/core/arrays/interval.py b/pandas/core/arrays/interval.py index aaa4124182598..67891a1d06ff0 100644 --- a/pandas/core/arrays/interval.py +++ b/pandas/core/arrays/interval.py @@ -694,6 +694,13 @@ def copy(self): # TODO: Could skip verify_integrity here. return type(self).from_arrays(left, right, closed=closed) + def view(self, dtype=None): + if dtype is not None: + raise NotImplementedError + return type(self)._simple_new(self._left, self._right, self.closed, + copy=False, dtype=None, + verify_integrity=False) + def isna(self): return isna(self.left) diff --git a/pandas/core/arrays/numpy_.py b/pandas/core/arrays/numpy_.py index 1c5dc7666c3a1..68fd63cfcb95c 100644 --- a/pandas/core/arrays/numpy_.py +++ b/pandas/core/arrays/numpy_.py @@ -299,6 +299,11 @@ def unique(self): return type(self)(unique(self._ndarray)) + def view(self, dtype=None): + if dtype is not None: + raise NotImplementedError(dtype) + return type(self)(self._ndarray) + # ------------------------------------------------------------------------ # Reductions diff --git a/pandas/core/arrays/sparse.py b/pandas/core/arrays/sparse.py index 3512d4e9e29db..fcc426a0c5232 100644 --- a/pandas/core/arrays/sparse.py +++ b/pandas/core/arrays/sparse.py @@ -1266,6 +1266,11 @@ def copy(self): values = self.sp_values.copy() return self._simple_new(values, self.sp_index, self.dtype) + def view(self, dtype=None): + if dtype is not None: + raise NotImplementedError + return self._simple_new(self.sp_values, self.sp_index, self.dtype) + @classmethod def _concat_same_type(cls, to_concat): fill_values = [x.fill_value for x in to_concat] diff --git a/pandas/tests/extension/arrow/bool.py b/pandas/tests/extension/arrow/bool.py index 0d6396033fac7..361ef4efba94f 100644 --- a/pandas/tests/extension/arrow/bool.py +++ b/pandas/tests/extension/arrow/bool.py @@ -49,6 +49,11 @@ def __init__(self, values): self._data = values self._dtype = ArrowBoolDtype() + def view(self, dtype=None): + if dtype is not None: + raise NotImplementedError + return type(self)(self._data) + def __repr__(self): return "ArrowBoolArray({})".format(repr(self._data)) diff --git a/pandas/tests/extension/arrow/test_bool.py b/pandas/tests/extension/arrow/test_bool.py index 21ce5e999334e..42944f31eea8a 100644 --- a/pandas/tests/extension/arrow/test_bool.py +++ b/pandas/tests/extension/arrow/test_bool.py @@ -41,6 +41,10 @@ def test_copy(self, data): # __setitem__ does not work, so we only have a smoke-test data.copy() + def test_view(self, data): + # __setitem__ does not work, so we only have a smoke-test + data.view() + class TestConstructors(BaseArrowTests, base.BaseConstructorsTests): def test_from_dtype(self, data): diff --git a/pandas/tests/extension/base/interface.py b/pandas/tests/extension/base/interface.py index fd47ae6f31290..7ecf5c1c25d3b 100644 --- a/pandas/tests/extension/base/interface.py +++ b/pandas/tests/extension/base/interface.py @@ -74,3 +74,14 @@ def test_copy(self, data): data[1] = data[0] assert result[1] != result[0] + + def test_view(self, data): + # view with no dtype should return a shallow copy, *not* the same + # object + assert data[1] != data[0] + + result = data.view() + assert result is not data + + result[1] = result[0] + assert data[1] == data[0] diff --git a/pandas/tests/extension/base/reshaping.py b/pandas/tests/extension/base/reshaping.py index ee22ffb3ccf97..f5b2cd52f8015 100644 --- a/pandas/tests/extension/base/reshaping.py +++ b/pandas/tests/extension/base/reshaping.py @@ -269,3 +269,15 @@ def test_unstack(self, data, index, obj): result = result.astype(object) self.assert_frame_equal(result, expected) + + def test_ravel(self, data): + # Test is invalid if data.ndim > 1 + assert data.ndim == 1 + result = data.ravel() + assert result is data + + def test_transpose(self, data): + # Test is invalid if data.ndim > 1 + assert data.ndim == 1 + result = data.T + assert result is data diff --git a/pandas/tests/extension/decimal/array.py b/pandas/tests/extension/decimal/array.py index 2b1bb53e962be..990023899431e 100644 --- a/pandas/tests/extension/decimal/array.py +++ b/pandas/tests/extension/decimal/array.py @@ -104,6 +104,11 @@ def take(self, indexer, allow_fill=False, fill_value=None): def copy(self): return type(self)(self._data.copy()) + def view(self, dtype=None): + if dtype is not None: + raise NotImplementedError + return type(self)(self._data) + def astype(self, dtype, copy=True): if isinstance(dtype, type(self.dtype)): return type(self)(self._data, context=dtype.context) diff --git a/pandas/tests/extension/json/array.py b/pandas/tests/extension/json/array.py index 1b5009830303b..6ea90a9ab7c5a 100644 --- a/pandas/tests/extension/json/array.py +++ b/pandas/tests/extension/json/array.py @@ -146,6 +146,11 @@ def take(self, indexer, allow_fill=False, fill_value=None): def copy(self): return type(self)(self.data[:]) + def view(self, dtype=None): + if dtype is not None: + raise NotImplementedError + return type(self)(self.data) + def astype(self, dtype, copy=True): # NumPy has issues when all the dicts are the same length. # np.array([UserDict(...), UserDict(...)]) fails, diff --git a/pandas/tests/extension/test_interval.py b/pandas/tests/extension/test_interval.py index f1f90b298ffe2..a25e23dae4d3a 100644 --- a/pandas/tests/extension/test_interval.py +++ b/pandas/tests/extension/test_interval.py @@ -35,12 +35,17 @@ def dtype(): return IntervalDtype() -@pytest.fixture def data(): """Length-100 PeriodArray for semantics test.""" return IntervalArray(make_data()) +@pytest.fixture(name="data") +def data_fixture(): + """Length-100 PeriodArray for semantics test.""" + return IntervalArray(make_data()) + + @pytest.fixture def data_missing(): """Length 2 array with [NA, Valid]""" @@ -95,7 +100,11 @@ class TestGrouping(BaseInterval, base.BaseGroupbyTests): class TestInterface(BaseInterval, base.BaseInterfaceTests): - pass + + def test_view(self, data): + # __setitem__ incorrectly makes a copy (GH#27147), so we only + # have a smoke-test + data.view() class TestReduce(base.BaseNoReduceTests): diff --git a/pandas/tests/extension/test_sparse.py b/pandas/tests/extension/test_sparse.py index 86ca3e230ddd5..96ceaf5b5d940 100644 --- a/pandas/tests/extension/test_sparse.py +++ b/pandas/tests/extension/test_sparse.py @@ -102,6 +102,10 @@ def test_copy(self, data): # __setitem__ does not work, so we only have a smoke-test data.copy() + def test_view(self, data): + # __setitem__ does not work, so we only have a smoke-test + data.view() + class TestConstructors(BaseSparseTests, base.BaseConstructorsTests): pass