Skip to content

Commit 61b8fc3

Browse files
authored
DEPR: casting date to dt64 in maybe_promote (#39767)
1 parent 4861e40 commit 61b8fc3

File tree

6 files changed

+77
-16
lines changed

6 files changed

+77
-16
lines changed

doc/source/whatsnew/v1.3.0.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ Deprecations
239239
- Deprecated :attr:`Rolling.is_datetimelike` (:issue:`38963`)
240240
- Deprecated :meth:`core.window.ewm.ExponentialMovingWindow.vol` (:issue:`39220`)
241241
- Using ``.astype`` to convert between ``datetime64[ns]`` dtype and :class:`DatetimeTZDtype` is deprecated and will raise in a future version, use ``obj.tz_localize`` or ``obj.dt.tz_localize`` instead (:issue:`38622`)
242-
-
242+
- Deprecated casting ``datetime.date`` objects to ``datetime64`` when used as ``fill_value`` in :meth:`DataFrame.unstack`, :meth:`DataFrame.shift`, :meth:`Series.shift`, and :meth:`DataFrame.reindex`, pass ``pd.Timestamp(dateobj)`` instead (:issue:`39767`)
243243

244244
.. ---------------------------------------------------------------------------
245245

pandas/core/dtypes/cast.py

+41-11
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from __future__ import annotations
66

77
from contextlib import suppress
8-
from datetime import datetime, timedelta
8+
from datetime import date, datetime, timedelta
99
from typing import (
1010
TYPE_CHECKING,
1111
Any,
@@ -549,16 +549,46 @@ def maybe_promote(dtype: np.dtype, fill_value=np.nan):
549549

550550
# returns tuple of (dtype, fill_value)
551551
if issubclass(dtype.type, np.datetime64):
552-
if isinstance(fill_value, datetime) and fill_value.tzinfo is not None:
553-
# Trying to insert tzaware into tznaive, have to cast to object
554-
dtype = np.dtype(np.object_)
555-
elif is_integer(fill_value) or is_float(fill_value):
556-
dtype = np.dtype(np.object_)
557-
else:
552+
inferred, fv = infer_dtype_from_scalar(fill_value, pandas_dtype=True)
553+
if inferred == dtype:
554+
return dtype, fv
555+
556+
# TODO(2.0): once this deprecation is enforced, this whole case
557+
# becomes equivalent to:
558+
# dta = DatetimeArray._from_sequence([], dtype="M8[ns]")
559+
# try:
560+
# fv = dta._validate_setitem_value(fill_value)
561+
# return dta.dtype, fv
562+
# except (ValueError, TypeError):
563+
# return np.dtype(object), fill_value
564+
if isinstance(fill_value, date) and not isinstance(fill_value, datetime):
565+
# deprecate casting of date object to match infer_dtype_from_scalar
566+
# and DatetimeArray._validate_setitem_value
558567
try:
559-
fill_value = Timestamp(fill_value).to_datetime64()
560-
except (TypeError, ValueError):
561-
dtype = np.dtype(np.object_)
568+
fv = Timestamp(fill_value).to_datetime64()
569+
except OutOfBoundsDatetime:
570+
pass
571+
else:
572+
warnings.warn(
573+
"Using a `date` object for fill_value with `datetime64[ns]` "
574+
"dtype is deprecated. In a future version, this will be cast "
575+
"to object dtype. Pass `fill_value=Timestamp(date_obj)` instead.",
576+
FutureWarning,
577+
stacklevel=7,
578+
)
579+
return dtype, fv
580+
elif isinstance(fill_value, str):
581+
try:
582+
# explicitly wrap in str to convert np.str_
583+
fv = Timestamp(str(fill_value))
584+
except (ValueError, TypeError):
585+
pass
586+
else:
587+
if fv.tz is None:
588+
return dtype, fv.asm8
589+
590+
return np.dtype(object), fill_value
591+
562592
elif issubclass(dtype.type, np.timedelta64):
563593
if (
564594
is_integer(fill_value)
@@ -723,13 +753,13 @@ def infer_dtype_from_scalar(val, pandas_dtype: bool = False) -> Tuple[DtypeObj,
723753

724754
if val is NaT or val.tz is None:
725755
dtype = np.dtype("M8[ns]")
756+
val = val.to_datetime64()
726757
else:
727758
if pandas_dtype:
728759
dtype = DatetimeTZDtype(unit="ns", tz=val.tz)
729760
else:
730761
# return datetimetz as object
731762
return np.dtype(object), val
732-
val = val.value
733763

734764
elif isinstance(val, (np.timedelta64, timedelta)):
735765
try:

pandas/core/indexes/interval.py

+4
Original file line numberDiff line numberDiff line change
@@ -533,6 +533,10 @@ def _maybe_convert_i8(self, key):
533533
key_dtype, key_i8 = infer_dtype_from_scalar(key, pandas_dtype=True)
534534
if lib.is_period(key):
535535
key_i8 = key.ordinal
536+
elif isinstance(key_i8, Timestamp):
537+
key_i8 = key_i8.value
538+
elif isinstance(key_i8, (np.datetime64, np.timedelta64)):
539+
key_i8 = key_i8.view("i8")
536540
else:
537541
# DatetimeIndex/TimedeltaIndex
538542
key_dtype, key_i8 = key.dtype, Index(key.asi8)

pandas/tests/dtypes/cast/test_infer_dtype.py

+1-3
Original file line numberDiff line numberDiff line change
@@ -105,13 +105,11 @@ def test_infer_from_scalar_tz(tz, pandas_dtype):
105105

106106
if pandas_dtype:
107107
exp_dtype = f"datetime64[ns, {tz}]"
108-
exp_val = dt.value
109108
else:
110109
exp_dtype = np.object_
111-
exp_val = dt
112110

113111
assert dtype == exp_dtype
114-
assert val == exp_val
112+
assert val == dt
115113

116114

117115
@pytest.mark.parametrize(

pandas/tests/dtypes/cast/test_promote.py

+8-1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
from pandas.core.dtypes.missing import isna
2525

2626
import pandas as pd
27+
import pandas._testing as tm
2728

2829

2930
@pytest.fixture(
@@ -403,7 +404,13 @@ def test_maybe_promote_any_with_datetime64(
403404
expected_dtype = np.dtype(object)
404405
exp_val_for_scalar = fill_value
405406

406-
_check_promote(dtype, fill_value, expected_dtype, exp_val_for_scalar)
407+
warn = None
408+
if type(fill_value) is datetime.date and dtype.kind == "M":
409+
# Casting date to dt64 is deprecated
410+
warn = FutureWarning
411+
412+
with tm.assert_produces_warning(warn, check_stacklevel=False):
413+
_check_promote(dtype, fill_value, expected_dtype, exp_val_for_scalar)
407414

408415

409416
@pytest.mark.parametrize(

pandas/tests/frame/methods/test_reindex.py

+22
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,28 @@ class TestDataFrameSelectReindex:
6262
# These are specific reindex-based tests; other indexing tests should go in
6363
# test_indexing
6464

65+
def test_reindex_date_fill_value(self):
66+
# passing date to dt64 is deprecated
67+
arr = date_range("2016-01-01", periods=6).values.reshape(3, 2)
68+
df = DataFrame(arr, columns=["A", "B"], index=range(3))
69+
70+
ts = df.iloc[0, 0]
71+
fv = ts.date()
72+
73+
with tm.assert_produces_warning(FutureWarning):
74+
res = df.reindex(index=range(4), columns=["A", "B", "C"], fill_value=fv)
75+
76+
expected = DataFrame(
77+
{"A": df["A"].tolist() + [ts], "B": df["B"].tolist() + [ts], "C": [ts] * 4}
78+
)
79+
tm.assert_frame_equal(res, expected)
80+
81+
# same with a datetime-castable str
82+
res = df.reindex(
83+
index=range(4), columns=["A", "B", "C"], fill_value="2016-01-01"
84+
)
85+
tm.assert_frame_equal(res, expected)
86+
6587
def test_reindex_with_multi_index(self):
6688
# https://github.com/pandas-dev/pandas/issues/29896
6789
# tests for reindexing a multi-indexed DataFrame with a new MultiIndex

0 commit comments

Comments
 (0)