Skip to content

Commit e8be9fa

Browse files
authored
REF: use DatetimeArray._validate_setitem_value for DatetimeBlock._can_hold_element (#38651)
1 parent 019d053 commit e8be9fa

File tree

3 files changed

+65
-29
lines changed

3 files changed

+65
-29
lines changed

pandas/core/arrays/datetimelike.py

+4
Original file line numberDiff line numberDiff line change
@@ -601,6 +601,10 @@ def _validate_listlike(self, value, allow_object: bool = False):
601601
if isinstance(value, type(self)):
602602
return value
603603

604+
if isinstance(value, list) and len(value) == 0:
605+
# We treat empty list as our own dtype.
606+
return type(self)._from_sequence([], dtype=self.dtype)
607+
604608
# Do type inference if necessary up front
605609
# e.g. we passed PeriodIndex.values and got an ndarray of Periods
606610
value = array(value)

pandas/core/internals/blocks.py

+10-28
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from datetime import datetime, timedelta
1+
from datetime import timedelta
22
import inspect
33
import re
44
from typing import TYPE_CHECKING, Any, List, Optional, Type, Union, cast
@@ -18,7 +18,6 @@
1818
)
1919
from pandas._libs.internals import BlockPlacement
2020
from pandas._libs.tslibs import conversion
21-
from pandas._libs.tslibs.timezones import tz_compare
2221
from pandas._typing import ArrayLike, DtypeObj, Scalar, Shape
2322
from pandas.util._validators import validate_bool_kwarg
2423

@@ -2190,6 +2189,15 @@ def to_native_types(self, na_rep="NaT", **kwargs):
21902189
result = arr._format_native_types(na_rep=na_rep, **kwargs)
21912190
return self.make_block(result)
21922191

2192+
def _can_hold_element(self, element: Any) -> bool:
2193+
arr = self.array_values()
2194+
2195+
try:
2196+
arr._validate_setitem_value(element)
2197+
return True
2198+
except (TypeError, ValueError):
2199+
return False
2200+
21932201

21942202
class DatetimeBlock(DatetimeLikeBlockMixin):
21952203
__slots__ = ()
@@ -2222,32 +2230,6 @@ def _maybe_coerce_values(self, values):
22222230
assert isinstance(values, np.ndarray), type(values)
22232231
return values
22242232

2225-
def _can_hold_element(self, element: Any) -> bool:
2226-
tipo = maybe_infer_dtype_type(element)
2227-
if tipo is not None:
2228-
if isinstance(element, list) and len(element) == 0:
2229-
# Following DatetimeArray._validate_setitem_value
2230-
# convention, we treat this as object-dtype
2231-
# (even though tipo is float64)
2232-
return True
2233-
2234-
elif self.is_datetimetz:
2235-
# require exact match, since non-nano does not exist
2236-
return is_dtype_equal(tipo, self.dtype) or is_valid_nat_for_dtype(
2237-
element, self.dtype
2238-
)
2239-
2240-
# GH#27419 if we get a non-nano datetime64 object
2241-
return is_datetime64_dtype(tipo)
2242-
elif element is NaT:
2243-
return True
2244-
elif isinstance(element, datetime):
2245-
if self.is_datetimetz:
2246-
return tz_compare(element.tzinfo, self.dtype.tz)
2247-
return element.tzinfo is None
2248-
2249-
return is_valid_nat_for_dtype(element, self.dtype)
2250-
22512233
def set_inplace(self, locs, values):
22522234
"""
22532235
See Block.set.__doc__

pandas/tests/indexing/test_indexing.py

+51-1
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from pandas.core.dtypes.common import is_float_dtype, is_integer_dtype
1111

1212
import pandas as pd
13-
from pandas import DataFrame, Index, NaT, Series
13+
from pandas import DataFrame, Index, NaT, Series, date_range
1414
import pandas._testing as tm
1515
from pandas.core.indexing import maybe_numeric_slice, non_reducing_slice
1616
from pandas.tests.indexing.common import _mklbl
@@ -966,6 +966,56 @@ def test_none_coercion_mixed_dtypes(self):
966966
tm.assert_frame_equal(start_dataframe, exp)
967967

968968

969+
class TestDatetimelikeCoercion:
970+
@pytest.mark.parametrize("indexer", [setitem, loc, iloc])
971+
def test_setitem_dt64_string_scalar(self, tz_naive_fixture, indexer):
972+
# dispatching _can_hold_element to underling DatetimeArray
973+
# TODO(EA2D) use tz_naive_fixture once DatetimeBlock is backed by DTA
974+
tz = tz_naive_fixture
975+
976+
dti = date_range("2016-01-01", periods=3, tz=tz)
977+
ser = Series(dti)
978+
979+
values = ser._values
980+
981+
indexer(ser)[0] = "2018-01-01"
982+
983+
if tz is None:
984+
# TODO(EA2D): we can make this no-copy in tz-naive case too
985+
assert ser.dtype == dti.dtype
986+
else:
987+
assert ser._values is values
988+
989+
@pytest.mark.parametrize("box", [list, np.array, pd.array])
990+
@pytest.mark.parametrize(
991+
"key", [[0, 1], slice(0, 2), np.array([True, True, False])]
992+
)
993+
@pytest.mark.parametrize("indexer", [setitem, loc, iloc])
994+
def test_setitem_dt64_string_values(self, tz_naive_fixture, indexer, key, box):
995+
# dispatching _can_hold_element to underling DatetimeArray
996+
# TODO(EA2D) use tz_naive_fixture once DatetimeBlock is backed by DTA
997+
tz = tz_naive_fixture
998+
999+
if isinstance(key, slice) and indexer is loc:
1000+
key = slice(0, 1)
1001+
1002+
dti = date_range("2016-01-01", periods=3, tz=tz)
1003+
ser = Series(dti)
1004+
1005+
values = ser._values
1006+
1007+
newvals = box(["2019-01-01", "2010-01-02"])
1008+
values._validate_setitem_value(newvals)
1009+
1010+
indexer(ser)[key] = newvals
1011+
1012+
if tz is None:
1013+
# TODO(EA2D): we can make this no-copy in tz-naive case too
1014+
assert ser.dtype == dti.dtype
1015+
else:
1016+
assert ser._values is values
1017+
1018+
9691019
def test_extension_array_cross_section():
9701020
# A cross-section of a homogeneous EA should be an EA
9711021
df = DataFrame(

0 commit comments

Comments
 (0)