Skip to content

Commit 84e83c7

Browse files
let getitem propagate readonly property
1 parent d5d4db4 commit 84e83c7

File tree

9 files changed

+57
-8
lines changed

9 files changed

+57
-8
lines changed

pandas/core/arrays/_mixins.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -284,7 +284,10 @@ def __getitem__(
284284
result = self._ndarray[key]
285285
if self.ndim == 1:
286286
return self._box_func(result)
287-
return self._from_backing_data(result)
287+
result = self._from_backing_data(result)
288+
if self._getitem_returns_view(key):
289+
result._readonly = self._readonly
290+
return result
288291

289292
# error: Incompatible types in assignment (expression has type "ExtensionArray",
290293
# variable has type "Union[int, slice, ndarray]")
@@ -295,6 +298,8 @@ def __getitem__(
295298
return self._box_func(result)
296299

297300
result = self._from_backing_data(result)
301+
if self._getitem_returns_view(key):
302+
result._readonly = self._readonly
298303
return result
299304

300305
def _pad_or_backfill(

pandas/core/arrays/arrow/array.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -730,7 +730,10 @@ def __getitem__(self, item: PositionalIndexer):
730730

731731
value = self._pa_array[item]
732732
if isinstance(value, pa.ChunkedArray):
733-
return self._from_pyarrow_array(value)
733+
result = self._from_pyarrow_array(value)
734+
if self._getitem_returns_view(item):
735+
result._readonly = self._readonly
736+
return result
734737
else:
735738
pa_type = self._pa_array.type
736739
scalar = value.as_py()

pandas/core/arrays/base.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,22 @@ def __getitem__(self, item: PositionalIndexer) -> Self | Any:
433433
"""
434434
raise AbstractMethodError(self)
435435

436+
def _getitem_returns_view(self, key) -> bool:
437+
if not isinstance(key, tuple):
438+
key = (key,)
439+
440+
# filter out Ellipsis and np.newaxis
441+
key = tuple(k for k in key if k is not Ellipsis and k is not np.newaxis)
442+
if not key:
443+
return True
444+
# single integer gives view if selecting subset of 2D array
445+
if self.ndim == 2 and lib.is_integer(key[0]):
446+
return True
447+
# slices always give views
448+
if all(isinstance(k, slice) for k in key):
449+
return True
450+
return False
451+
436452
def __setitem__(self, key, value) -> None:
437453
"""
438454
Set one or more values inplace.

pandas/core/arrays/interval.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -724,7 +724,10 @@ def __getitem__(self, key: PositionalIndexer) -> Self | IntervalOrNA:
724724
# "Union[Period, Timestamp, Timedelta, NaTType, DatetimeArray, TimedeltaArray,
725725
# ndarray[Any, Any]]"; expected "Union[Union[DatetimeArray, TimedeltaArray],
726726
# ndarray[Any, Any]]"
727-
return self._simple_new(left, right, dtype=self.dtype) # type: ignore[arg-type]
727+
result = self._simple_new(left, right, dtype=self.dtype)
728+
if self._getitem_returns_view(key):
729+
result._readonly = self._readonly
730+
return result # type: ignore[arg-type]
728731

729732
def __setitem__(self, key, value) -> None:
730733
if self._readonly:

pandas/core/arrays/masked.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,7 +200,10 @@ def __getitem__(self, item: PositionalIndexer) -> Self | Any:
200200
return self.dtype.na_value
201201
return self._data[item]
202202

203-
return self._simple_new(self._data[item], newmask)
203+
result = self._simple_new(self._data[item], newmask)
204+
if self._getitem_returns_view(item):
205+
result._readonly = self._readonly
206+
return result
204207

205208
def _pad_or_backfill(
206209
self,

pandas/core/arrays/sparse/array.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1008,7 +1008,11 @@ def __getitem__(
10081008
elif isinstance(key, slice):
10091009
if key == slice(None):
10101010
# to ensure arr[:] (used by view()) does not make a copy
1011-
return type(self)._simple_new(self.sp_values, self.sp_index, self.dtype)
1011+
result = type(self)._simple_new(
1012+
self.sp_values, self.sp_index, self.dtype
1013+
)
1014+
result._readonly = self._readonly
1015+
return result
10121016
# Avoid densifying when handling contiguous slices
10131017
if key.step is None or key.step == 1:
10141018
start = 0 if key.start is None else key.start

pandas/tests/extension/base/getitem.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,3 +467,10 @@ def test_item(self, data):
467467

468468
with pytest.raises(ValueError, match=msg):
469469
s.item()
470+
471+
def test_getitem_propagates_readonly_property(self, data):
472+
# ensure read-only propagates if getitem returns view
473+
data._readonly = True
474+
475+
result = data[:]
476+
assert result._readonly

pandas/tests/extension/decimal/array.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,10 @@ def __getitem__(self, item):
177177
else:
178178
# array, slice.
179179
item = pd.api.indexers.check_array_indexer(self, item)
180-
return type(self)(self._data[item])
180+
result = type(self)(self._data[item])
181+
if self._getitem_returns_view(item):
182+
result._readonly = self._readonly
183+
return result
181184

182185
def take(self, indexer, allow_fill=False, fill_value=None):
183186
from pandas.api.extensions import take

pandas/tests/extension/json/array.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,15 @@ def __getitem__(self, item):
105105
return self.data[item]
106106
elif isinstance(item, slice) and item == slice(None):
107107
# Make sure we get a view
108-
return type(self)(self.data)
108+
result = type(self)(self.data)
109+
result._readonly = self._readonly
110+
return result
109111
elif isinstance(item, slice):
110112
# slice
111-
return type(self)(self.data[item])
113+
result = type(self)(self.data[item])
114+
if self._getitem_returns_view(item):
115+
result._readonly = self._readonly
116+
return result
112117
elif not is_list_like(item):
113118
# e.g. "foo" or 2.5
114119
# exception message copied from numpy

0 commit comments

Comments
 (0)