Skip to content

Commit 0f8234c

Browse files
authored
BUG: incorrectly accepting datetime64(nat) for dt64tz (#39769)
1 parent 86ad5c3 commit 0f8234c

File tree

9 files changed

+66
-9
lines changed

9 files changed

+66
-9
lines changed

doc/source/whatsnew/v1.3.0.rst

+2
Original file line numberDiff line numberDiff line change
@@ -346,7 +346,9 @@ Indexing
346346
- Bug in setting ``timedelta64`` or ``datetime64`` values into numeric :class:`Series` failing to cast to object dtype (:issue:`39086`, issue:`39619`)
347347
- Bug in setting :class:`Interval` values into a :class:`Series` or :class:`DataFrame` with mismatched :class:`IntervalDtype` incorrectly casting the new values to the existing dtype (:issue:`39120`)
348348
- Bug in setting ``datetime64`` values into a :class:`Series` with integer-dtype incorrect casting the datetime64 values to integers (:issue:`39266`)
349+
- Bug in setting ``np.datetime64("NaT")`` into a :class:`Series` with :class:`Datetime64TZDtype` incorrectly treating the timezone-naive value as timezone-aware (:issue:`39769`)
349350
- Bug in :meth:`Index.get_loc` not raising ``KeyError`` when method is specified for ``NaN`` value when ``NaN`` is not in :class:`Index` (:issue:`39382`)
351+
- Bug in :meth:`DatetimeIndex.insert` when inserting ``np.datetime64("NaT")`` into a timezone-aware index incorrectly treating the timezone-naive value as timezone-aware (:issue:`39769`)
350352
- Bug in incorrectly raising in :meth:`Index.insert`, when setting a new column that cannot be held in the existing ``frame.columns``, or in :meth:`Series.reset_index` or :meth:`DataFrame.reset_index` instead of casting to a compatible dtype (:issue:`39068`)
351353
- Bug in :meth:`RangeIndex.append` where a single object of length 1 was concatenated incorrectly (:issue:`39401`)
352354
- Bug in setting ``numpy.timedelta64`` values into an object-dtype :class:`Series` using a boolean indexer (:issue:`39488`)

pandas/core/arrays/datetimelike.py

+6
Original file line numberDiff line numberDiff line change
@@ -559,6 +559,12 @@ def _validate_scalar(
559559
# GH#18295
560560
value = NaT
561561

562+
elif isna(value):
563+
# if we are dt64tz and value is dt64("NaT"), dont cast to NaT,
564+
# or else we'll fail to raise in _unbox_scalar
565+
msg = self._validation_error_message(value, allow_listlike)
566+
raise TypeError(msg)
567+
562568
elif isinstance(value, self._recognized_scalars):
563569
# error: Too many arguments for "object"
564570
value = self._scalar_type(value) # type: ignore[call-arg]

pandas/core/arrays/datetimes.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -464,10 +464,8 @@ def _generate_range(
464464
def _unbox_scalar(self, value, setitem: bool = False) -> np.datetime64:
465465
if not isinstance(value, self._scalar_type) and value is not NaT:
466466
raise ValueError("'value' should be a Timestamp.")
467-
if not isna(value):
468-
self._check_compatible_with(value, setitem=setitem)
469-
return value.asm8
470-
return np.datetime64(value.value, "ns")
467+
self._check_compatible_with(value, setitem=setitem)
468+
return value.asm8
471469

472470
def _scalar_from_string(self, value):
473471
return Timestamp(value, tz=self.tz)

pandas/core/arrays/interval.py

+7-2
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
from pandas._config import get_option
1111

12+
from pandas._libs import NaT
1213
from pandas._libs.interval import (
1314
VALID_CLOSED,
1415
Interval,
@@ -23,7 +24,8 @@
2324
from pandas.core.dtypes.cast import maybe_convert_platform
2425
from pandas.core.dtypes.common import (
2526
is_categorical_dtype,
26-
is_datetime64_any_dtype,
27+
is_datetime64_dtype,
28+
is_datetime64tz_dtype,
2729
is_dtype_equal,
2830
is_float_dtype,
2931
is_integer_dtype,
@@ -999,9 +1001,12 @@ def _validate_setitem_value(self, value):
9991001
if is_integer_dtype(self.dtype.subtype):
10001002
# can't set NaN on a numpy integer array
10011003
needs_float_conversion = True
1002-
elif is_datetime64_any_dtype(self.dtype.subtype):
1004+
elif is_datetime64_dtype(self.dtype.subtype):
10031005
# need proper NaT to set directly on the numpy array
10041006
value = np.datetime64("NaT")
1007+
elif is_datetime64tz_dtype(self.dtype.subtype):
1008+
# need proper NaT to set directly on the DatetimeArray array
1009+
value = NaT
10051010
elif is_timedelta64_dtype(self.dtype.subtype):
10061011
# need proper NaT to set directly on the numpy array
10071012
value = np.timedelta64("NaT")

pandas/core/dtypes/missing.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -604,7 +604,11 @@ def is_valid_na_for_dtype(obj, dtype: DtypeObj) -> bool:
604604
if not lib.is_scalar(obj) or not isna(obj):
605605
return False
606606
if dtype.kind == "M":
607-
return not isinstance(obj, np.timedelta64)
607+
if isinstance(dtype, np.dtype):
608+
# i.e. not tzaware
609+
return not isinstance(obj, np.timedelta64)
610+
# we have to rule out tznaive dt64("NaT")
611+
return not isinstance(obj, (np.timedelta64, np.datetime64))
608612
if dtype.kind == "m":
609613
return not isinstance(obj, np.datetime64)
610614
if dtype.kind in ["i", "u", "f", "c"]:

pandas/core/internals/blocks.py

+2-1
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88

99
from pandas._libs import (
1010
Interval,
11+
NaT,
1112
Period,
1213
Timestamp,
1314
algos as libalgos,
@@ -2097,7 +2098,7 @@ class DatetimeTZBlock(ExtensionBlock, DatetimeBlock):
20972098
_can_hold_element = DatetimeBlock._can_hold_element
20982099
to_native_types = DatetimeBlock.to_native_types
20992100
diff = DatetimeBlock.diff
2100-
fill_value = np.datetime64("NaT", "ns")
2101+
fill_value = NaT
21012102
where = DatetimeBlock.where
21022103
putmask = DatetimeLikeBlockMixin.putmask
21032104

pandas/tests/indexes/datetimes/test_insert.py

+4
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,12 @@ class TestInsert:
1313
@pytest.mark.parametrize("tz", [None, "UTC", "US/Eastern"])
1414
def test_insert_nat(self, tz, null):
1515
# GH#16537, GH#18295 (test missing)
16+
1617
idx = DatetimeIndex(["2017-01-01"], tz=tz)
1718
expected = DatetimeIndex(["NaT", "2017-01-01"], tz=tz)
19+
if tz is not None and isinstance(null, np.datetime64):
20+
expected = Index([null, idx[0]], dtype=object)
21+
1822
res = idx.insert(0, null)
1923
tm.assert_index_equal(res, expected)
2024

pandas/tests/series/indexing/test_indexing.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -559,7 +559,7 @@ def test_dt64_series_assign_nat(nat_val, tz, indexer_sli):
559559
base = Series(dti)
560560
expected = Series([pd.NaT] + list(dti[1:]), dtype=dti.dtype)
561561

562-
should_cast = nat_val is pd.NaT or base.dtype.kind == nat_val.dtype.kind
562+
should_cast = nat_val is pd.NaT or base.dtype == nat_val.dtype
563563
if not should_cast:
564564
expected = expected.astype(object)
565565

pandas/tests/series/indexing/test_setitem.py

+37
Original file line numberDiff line numberDiff line change
@@ -671,6 +671,43 @@ def key(self):
671671
return 0
672672

673673

674+
class TestSetitemNADatetime64Dtype(SetitemCastingEquivalents):
675+
# some nat-like values should be cast to datetime64 when inserting
676+
# into a datetime64 series. Others should coerce to object
677+
# and retain their dtypes.
678+
679+
@pytest.fixture(params=[None, "UTC", "US/Central"])
680+
def obj(self, request):
681+
tz = request.param
682+
dti = date_range("2016-01-01", periods=3, tz=tz)
683+
return Series(dti)
684+
685+
@pytest.fixture(
686+
params=[NaT, np.timedelta64("NaT", "ns"), np.datetime64("NaT", "ns")]
687+
)
688+
def val(self, request):
689+
return request.param
690+
691+
@pytest.fixture
692+
def is_inplace(self, val, obj):
693+
if obj._values.tz is None:
694+
# cast to object iff val is timedelta64("NaT")
695+
return val is NaT or val.dtype.kind == "M"
696+
697+
# otherwise we have to exclude tznaive dt64("NaT")
698+
return val is NaT
699+
700+
@pytest.fixture
701+
def expected(self, obj, val, is_inplace):
702+
dtype = obj.dtype if is_inplace else object
703+
expected = Series([val] + list(obj[1:]), dtype=dtype)
704+
return expected
705+
706+
@pytest.fixture
707+
def key(self):
708+
return 0
709+
710+
674711
class TestSetitemMismatchedTZCastsToObject(SetitemCastingEquivalents):
675712
# GH#24024
676713
@pytest.fixture

0 commit comments

Comments
 (0)