Skip to content

Commit df22f17

Browse files
TomAugspurgerPingviinituutti
authored andcommitted
Datetimelike __setitem__ (pandas-dev#24477)
1 parent b369e5d commit df22f17

File tree

10 files changed

+110
-42
lines changed

10 files changed

+110
-42
lines changed

pandas/core/arrays/datetimelike.py

+50
Original file line numberDiff line numberDiff line change
@@ -478,6 +478,56 @@ def __getitem__(self, key):
478478

479479
return self._simple_new(result, **attribs)
480480

481+
def __setitem__(
482+
self,
483+
key, # type: Union[int, Sequence[int], Sequence[bool], slice]
484+
value, # type: Union[NaTType, Scalar, Sequence[Scalar]]
485+
):
486+
# type: (...) -> None
487+
# I'm fudging the types a bit here. The "Scalar" above really depends
488+
# on type(self). For PeriodArray, it's Period (or stuff coercible
489+
# to a period in from_sequence). For DatetimeArray, it's Timestamp...
490+
# I don't know if mypy can do that, possibly with Generics.
491+
# https://mypy.readthedocs.io/en/latest/generics.html
492+
493+
if is_list_like(value):
494+
is_slice = isinstance(key, slice)
495+
496+
if lib.is_scalar(key):
497+
raise ValueError("setting an array element with a sequence.")
498+
499+
if (not is_slice
500+
and len(key) != len(value)
501+
and not com.is_bool_indexer(key)):
502+
msg = ("shape mismatch: value array of length '{}' does not "
503+
"match indexing result of length '{}'.")
504+
raise ValueError(msg.format(len(key), len(value)))
505+
if not is_slice and len(key) == 0:
506+
return
507+
508+
value = type(self)._from_sequence(value, dtype=self.dtype)
509+
self._check_compatible_with(value)
510+
value = value.asi8
511+
elif isinstance(value, self._scalar_type):
512+
self._check_compatible_with(value)
513+
value = self._unbox_scalar(value)
514+
elif isna(value) or value == iNaT:
515+
value = iNaT
516+
else:
517+
msg = (
518+
"'value' should be a '{scalar}', 'NaT', or array of those. "
519+
"Got '{typ}' instead."
520+
)
521+
raise TypeError(msg.format(scalar=self._scalar_type.__name__,
522+
typ=type(value).__name__))
523+
self._data[key] = value
524+
self._maybe_clear_freq()
525+
526+
def _maybe_clear_freq(self):
527+
# inplace operations like __setitem__ may invalidate the freq of
528+
# DatetimeArray and TimedeltaArray
529+
pass
530+
481531
def astype(self, dtype, copy=True):
482532
# Some notes on cases we don't have to handle here in the base class:
483533
# 1. PeriodArray.astype handles period -> period

pandas/core/arrays/datetimes.py

+3
Original file line numberDiff line numberDiff line change
@@ -368,6 +368,9 @@ def _check_compatible_with(self, other):
368368
raise ValueError("Timezones don't match. '{own} != {other}'"
369369
.format(own=self.tz, other=other.tz))
370370

371+
def _maybe_clear_freq(self):
372+
self._freq = None
373+
371374
# -----------------------------------------------------------------
372375
# Descriptive Properties
373376

pandas/core/arrays/period.py

-42
Original file line numberDiff line numberDiff line change
@@ -371,48 +371,6 @@ def _formatter(self, boxed=False):
371371
return str
372372
return "'{}'".format
373373

374-
def __setitem__(
375-
self,
376-
key, # type: Union[int, Sequence[int], Sequence[bool], slice]
377-
value # type: Union[NaTType, Period, Sequence[Period]]
378-
):
379-
# type: (...) -> None
380-
# n.b. the type on `value` is a bit too restrictive.
381-
# we also accept a sequence of stuff coercible to a PeriodArray
382-
# by period_array, which includes things like ndarray[object],
383-
# ndarray[datetime64ns]. I think ndarray[int] / ndarray[str] won't
384-
# work, since the freq can't be inferred.
385-
if is_list_like(value):
386-
is_slice = isinstance(key, slice)
387-
if (not is_slice
388-
and len(key) != len(value)
389-
and not com.is_bool_indexer(key)):
390-
msg = ("shape mismatch: value array of length '{}' does not "
391-
"match indexing result of length '{}'.")
392-
raise ValueError(msg.format(len(key), len(value)))
393-
if not is_slice and len(key) == 0:
394-
return
395-
396-
value = period_array(value)
397-
398-
if self.freqstr != value.freqstr:
399-
_raise_on_incompatible(self, value)
400-
401-
value = value.asi8
402-
elif isinstance(value, Period):
403-
404-
if self.freqstr != value.freqstr:
405-
_raise_on_incompatible(self, value)
406-
407-
value = value.ordinal
408-
elif isna(value):
409-
value = iNaT
410-
else:
411-
msg = ("'value' should be a 'Period', 'NaT', or array of those. "
412-
"Got '{}' instead.".format(type(value).__name__))
413-
raise TypeError(msg)
414-
self._data[key] = value
415-
416374
@Appender(dtl.DatetimeLikeArrayMixin._validate_fill_value.__doc__)
417375
def _validate_fill_value(self, fill_value):
418376
if isna(fill_value):

pandas/core/arrays/timedeltas.py

+3
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,9 @@ def _check_compatible_with(self, other):
238238
# we don't have anything to validate.
239239
pass
240240

241+
def _maybe_clear_freq(self):
242+
self._freq = None
243+
241244
# ----------------------------------------------------------------
242245
# Array-Like / EA-Interface Methods
243246

pandas/core/indexes/datetimelike.py

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ class DatetimeIndexOpsMixin(DatetimeLikeArrayMixin):
3838
# override DatetimeLikeArrayMixin method
3939
copy = Index.copy
4040
view = Index.view
41+
__setitem__ = Index.__setitem__
4142

4243
# DatetimeLikeArrayMixin assumes subclasses are mutable, so these are
4344
# properties there. They can be made into cache_readonly for Index

pandas/tests/arrays/test_datetimelike.py

+25
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,31 @@ def test_searchsorted(self):
182182
result = arr.searchsorted(pd.NaT)
183183
assert result == 0
184184

185+
def test_setitem(self):
186+
data = np.arange(10, dtype='i8') * 24 * 3600 * 10**9
187+
arr = self.array_cls(data, freq='D')
188+
189+
arr[0] = arr[1]
190+
expected = np.arange(10, dtype='i8') * 24 * 3600 * 10**9
191+
expected[0] = expected[1]
192+
193+
tm.assert_numpy_array_equal(arr.asi8, expected)
194+
195+
arr[:2] = arr[-2:]
196+
expected[:2] = expected[-2:]
197+
tm.assert_numpy_array_equal(arr.asi8, expected)
198+
199+
def test_setitem_raises(self):
200+
data = np.arange(10, dtype='i8') * 24 * 3600 * 10**9
201+
arr = self.array_cls(data, freq='D')
202+
val = arr[0]
203+
204+
with pytest.raises(IndexError, match="index 12 is out of bounds"):
205+
arr[12] = val
206+
207+
with pytest.raises(TypeError, match="'value' should be a.* 'object'"):
208+
arr[0] = object()
209+
185210

186211
class TestDatetimeArray(SharedTests):
187212
index_cls = pd.DatetimeIndex

pandas/tests/arrays/test_datetimes.py

+16
Original file line numberDiff line numberDiff line change
@@ -74,3 +74,19 @@ def test_tz_setter_raises(self):
7474
arr = DatetimeArray._from_sequence(['2000'], tz='US/Central')
7575
with pytest.raises(AttributeError, match='tz_localize'):
7676
arr.tz = 'UTC'
77+
78+
def test_setitem_different_tz_raises(self):
79+
data = np.array([1, 2, 3], dtype='M8[ns]')
80+
arr = DatetimeArray(data, copy=False,
81+
dtype=DatetimeTZDtype(tz="US/Central"))
82+
with pytest.raises(ValueError, match="None"):
83+
arr[0] = pd.Timestamp('2000')
84+
85+
with pytest.raises(ValueError, match="US/Central"):
86+
arr[0] = pd.Timestamp('2000', tz="US/Eastern")
87+
88+
def test_setitem_clears_freq(self):
89+
a = DatetimeArray(pd.date_range('2000', periods=2, freq='D',
90+
tz='US/Central'))
91+
a[0] = pd.Timestamp("2000", tz="US/Central")
92+
assert a.freq is None

pandas/tests/arrays/test_timedeltas.py

+5
Original file line numberDiff line numberDiff line change
@@ -72,3 +72,8 @@ def test_astype_int(self, dtype):
7272

7373
assert result.dtype == expected_dtype
7474
tm.assert_numpy_array_equal(result, expected)
75+
76+
def test_setitem_clears_freq(self):
77+
a = TimedeltaArray(pd.timedelta_range('1H', periods=2, freq='H'))
78+
a[0] = pd.Timedelta("1H")
79+
assert a.freq is None

pandas/tests/extension/base/setitem.py

+5
Original file line numberDiff line numberDiff line change
@@ -182,3 +182,8 @@ def test_setitem_slice_array(self, data):
182182
arr = data[:5].copy()
183183
arr[:5] = data[-5:]
184184
self.assert_extension_array_equal(arr, data[-5:])
185+
186+
def test_setitem_scalar_key_sequence_raise(self, data):
187+
arr = data[:5].copy()
188+
with pytest.raises(ValueError):
189+
arr[0] = arr[[0, 1]]

pandas/tests/extension/decimal/array.py

+2
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@ def astype(self, dtype, copy=True):
108108

109109
def __setitem__(self, key, value):
110110
if pd.api.types.is_list_like(value):
111+
if pd.api.types.is_scalar(key):
112+
raise ValueError("setting an array element with a sequence.")
111113
value = [decimal.Decimal(v) for v in value]
112114
else:
113115
value = decimal.Decimal(value)

0 commit comments

Comments
 (0)