Skip to content

Commit 22a038e

Browse files
authored
ENH: Dtype._is_immutable (#54421)
1 parent 6704f6b commit 22a038e

File tree

7 files changed

+47
-18
lines changed

7 files changed

+47
-18
lines changed

pandas/core/dtypes/base.py

+10
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,16 @@ def _can_hold_na(self) -> bool:
396396
"""
397397
return True
398398

399+
@property
400+
def _is_immutable(self) -> bool:
401+
"""
402+
Can arrays with this dtype be modified with __setitem__? If not, return
403+
True.
404+
405+
Immutable arrays are expected to raise TypeError on __setitem__ calls.
406+
"""
407+
return False
408+
399409

400410
class StorageExtensionDtype(ExtensionDtype):
401411
"""ExtensionDtype that may be backed by more than one implementation."""

pandas/core/dtypes/dtypes.py

+2
Original file line numberDiff line numberDiff line change
@@ -1608,6 +1608,8 @@ class SparseDtype(ExtensionDtype):
16081608
0.3333333333333333
16091609
"""
16101610

1611+
_is_immutable = True
1612+
16111613
# We include `_is_na_fill_value` in the metadata to avoid hash collisions
16121614
# between SparseDtype(float, 0.0) and SparseDtype(float, nan).
16131615
# Without is_na_fill_value in the comparison, those would be equal since

pandas/core/internals/managers.py

+1-2
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,6 @@
3939
from pandas.core.dtypes.dtypes import (
4040
DatetimeTZDtype,
4141
ExtensionDtype,
42-
SparseDtype,
4342
)
4443
from pandas.core.dtypes.generic import (
4544
ABCDataFrame,
@@ -943,7 +942,7 @@ def fast_xs(self, loc: int) -> SingleBlockManager:
943942
n = len(self)
944943

945944
# GH#46406
946-
immutable_ea = isinstance(dtype, SparseDtype)
945+
immutable_ea = isinstance(dtype, ExtensionDtype) and dtype._is_immutable
947946

948947
if isinstance(dtype, ExtensionDtype) and not immutable_ea:
949948
cls = dtype.construct_array_type()

pandas/tests/extension/base/interface.py

+7
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import numpy as np
2+
import pytest
23

34
from pandas.core.dtypes.common import is_extension_array_dtype
45
from pandas.core.dtypes.dtypes import ExtensionDtype
@@ -102,6 +103,9 @@ def test_copy(self, data):
102103
assert data[0] != data[1]
103104
result = data.copy()
104105

106+
if data.dtype._is_immutable:
107+
pytest.skip("test_copy assumes mutability")
108+
105109
data[1] = data[0]
106110
assert result[1] != result[0]
107111

@@ -114,6 +118,9 @@ def test_view(self, data):
114118
assert result is not data
115119
assert type(result) == type(data)
116120

121+
if data.dtype._is_immutable:
122+
pytest.skip("test_view assumes mutability")
123+
117124
result[1] = result[0]
118125
assert data[1] == data[0]
119126

pandas/tests/extension/base/reshaping.py

+6
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,9 @@ def test_ravel(self, data):
334334
result = data.ravel()
335335
assert type(result) == type(data)
336336

337+
if data.dtype._is_immutable:
338+
pytest.skip("test_ravel assumes mutability")
339+
337340
# Check that we have a view, not a copy
338341
result[0] = result[1]
339342
assert data[0] == data[1]
@@ -348,6 +351,9 @@ def test_transpose(self, data):
348351
# If we ever _did_ support 2D, shape should be reversed
349352
assert result.shape == data.shape[::-1]
350353

354+
if data.dtype._is_immutable:
355+
pytest.skip("test_transpose assumes mutability")
356+
351357
# Check that we have a view, not a copy
352358
result[0] = result[1]
353359
assert data[0] == data[1]

pandas/tests/extension/base/setitem.py

+18
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,24 @@ def full_indexer(self, request):
3636
"""
3737
return request.param
3838

39+
@pytest.fixture(autouse=True)
40+
def skip_if_immutable(self, dtype, request):
41+
if dtype._is_immutable:
42+
node = request.node
43+
if node.name.split("[")[0] == "test_is_immutable":
44+
# This fixture is auto-used, but we want to not-skip
45+
# test_is_immutable.
46+
return
47+
pytest.skip("__setitem__ test not applicable with immutable dtype")
48+
49+
def test_is_immutable(self, data):
50+
if data.dtype._is_immutable:
51+
with pytest.raises(TypeError):
52+
data[0] = data[0]
53+
else:
54+
data[0] = data[1]
55+
assert data[0] == data[1]
56+
3957
def test_setitem_scalar_series(self, data, box_in_series):
4058
if box_in_series:
4159
data = pd.Series(data)

pandas/tests/extension/test_sparse.py

+3-16
Original file line numberDiff line numberDiff line change
@@ -103,24 +103,14 @@ def _check_unsupported(self, data):
103103
if data.dtype == SparseDtype(int, 0):
104104
pytest.skip("Can't store nan in int array.")
105105

106-
@pytest.mark.xfail(reason="SparseArray does not support setitem")
107-
def test_ravel(self, data):
108-
super().test_ravel(data)
109-
110106

111107
class TestDtype(BaseSparseTests, base.BaseDtypeTests):
112108
def test_array_type_with_arg(self, data, dtype):
113109
assert dtype.construct_array_type() is SparseArray
114110

115111

116112
class TestInterface(BaseSparseTests, base.BaseInterfaceTests):
117-
def test_copy(self, data):
118-
# __setitem__ does not work, so we only have a smoke-test
119-
data.copy()
120-
121-
def test_view(self, data):
122-
# __setitem__ does not work, so we only have a smoke-test
123-
data.view()
113+
pass
124114

125115

126116
class TestConstructors(BaseSparseTests, base.BaseConstructorsTests):
@@ -180,10 +170,6 @@ def test_merge(self, data, na_value):
180170
self._check_unsupported(data)
181171
super().test_merge(data, na_value)
182172

183-
@pytest.mark.xfail(reason="SparseArray does not support setitem")
184-
def test_transpose(self, data):
185-
super().test_transpose(data)
186-
187173

188174
class TestGetitem(BaseSparseTests, base.BaseGetitemTests):
189175
def test_get(self, data):
@@ -199,7 +185,8 @@ def test_reindex(self, data, na_value):
199185
super().test_reindex(data, na_value)
200186

201187

202-
# Skipping TestSetitem, since we don't implement it.
188+
class TestSetitem(BaseSparseTests, base.BaseSetitemTests):
189+
pass
203190

204191

205192
class TestIndex(base.BaseIndexTests):

0 commit comments

Comments
 (0)