Skip to content

Commit 9bdd9fc

Browse files
jbrockmendelluckyvs1
authored andcommitted
BUG: Series construction with mismatched dt64 data vs td64 dtype (pandas-dev#38764)
1 parent b52294e commit 9bdd9fc

File tree

3 files changed

+53
-14
lines changed

3 files changed

+53
-14
lines changed

doc/source/whatsnew/v1.3.0.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ Datetimelike
189189
^^^^^^^^^^^^
190190
- Bug in :class:`DataFrame` and :class:`Series` constructors sometimes dropping nanoseconds from :class:`Timestamp` (resp. :class:`Timedelta`) ``data``, with ``dtype=datetime64[ns]`` (resp. ``timedelta64[ns]``) (:issue:`38032`)
191191
- Bug in :meth:`DataFrame.first` and :meth:`Series.first` returning two months for offset one month when first day is last calendar day (:issue:`29623`)
192-
- Bug in constructing a :class:`DataFrame` or :class:`Series` with mismatched ``datetime64`` data and ``timedelta64`` dtype, or vice-versa, failing to raise ``TypeError`` (:issue:`38575`)
192+
- Bug in constructing a :class:`DataFrame` or :class:`Series` with mismatched ``datetime64`` data and ``timedelta64`` dtype, or vice-versa, failing to raise ``TypeError`` (:issue:`38575`, :issue:`38764`)
193193
- Bug in :meth:`DatetimeIndex.intersection`, :meth:`DatetimeIndex.symmetric_difference`, :meth:`PeriodIndex.intersection`, :meth:`PeriodIndex.symmetric_difference` always returning object-dtype when operating with :class:`CategoricalIndex` (:issue:`38741`)
194194
- Bug in :meth:`Series.where` incorrectly casting ``datetime64`` values to ``int64`` (:issue:`37682`)
195195
-

pandas/core/dtypes/cast.py

+17-6
Original file line numberDiff line numberDiff line change
@@ -166,14 +166,23 @@ def maybe_unbox_datetimelike(value: Scalar, dtype: DtypeObj) -> Scalar:
166166
elif isinstance(value, Timedelta):
167167
value = value.to_timedelta64()
168168

169-
if (isinstance(value, np.timedelta64) and dtype.kind == "M") or (
170-
isinstance(value, np.datetime64) and dtype.kind == "m"
169+
_disallow_mismatched_datetimelike(value, dtype)
170+
return value
171+
172+
173+
def _disallow_mismatched_datetimelike(value: DtypeObj, dtype: DtypeObj):
174+
"""
175+
numpy allows np.array(dt64values, dtype="timedelta64[ns]") and
176+
vice-versa, but we do not want to allow this, so we need to
177+
check explicitly
178+
"""
179+
vdtype = getattr(value, "dtype", None)
180+
if vdtype is None:
181+
return
182+
elif (vdtype.kind == "m" and dtype.kind == "M") or (
183+
vdtype.kind == "M" and dtype.kind == "m"
171184
):
172-
# numpy allows np.array(dt64values, dtype="timedelta64[ns]") and
173-
# vice-versa, but we do not want to allow this, so we need to
174-
# check explicitly
175185
raise TypeError(f"Cannot cast {repr(value)} to {dtype}")
176-
return value
177186

178187

179188
def maybe_downcast_to_dtype(result, dtype: Union[str, np.dtype]):
@@ -1715,6 +1724,8 @@ def construct_1d_ndarray_preserving_na(
17151724
if dtype is not None and dtype.kind == "U":
17161725
subarr = lib.ensure_string_array(values, convert_na_value=False, copy=copy)
17171726
else:
1727+
if dtype is not None:
1728+
_disallow_mismatched_datetimelike(values, dtype)
17181729
subarr = np.array(values, dtype=dtype, copy=copy)
17191730

17201731
return subarr

pandas/tests/frame/test_constructors.py

+35-7
Original file line numberDiff line numberDiff line change
@@ -2925,12 +2925,31 @@ def get1(obj):
29252925

29262926

29272927
class TestFromScalar:
2928-
@pytest.fixture
2929-
def constructor(self, frame_or_series):
2930-
if frame_or_series is Series:
2931-
return functools.partial(Series, index=range(2))
2928+
@pytest.fixture(params=[list, dict, None])
2929+
def constructor(self, request, frame_or_series):
2930+
box = request.param
2931+
2932+
extra = {"index": range(2)}
2933+
if frame_or_series is DataFrame:
2934+
extra["columns"] = ["A"]
2935+
2936+
if box is None:
2937+
return functools.partial(frame_or_series, **extra)
2938+
2939+
elif box is dict:
2940+
if frame_or_series is Series:
2941+
return lambda x, **kwargs: frame_or_series(
2942+
{0: x, 1: x}, **extra, **kwargs
2943+
)
2944+
else:
2945+
return lambda x, **kwargs: frame_or_series({"A": x}, **extra, **kwargs)
29322946
else:
2933-
return functools.partial(DataFrame, index=range(2), columns=range(2))
2947+
if frame_or_series is Series:
2948+
return lambda x, **kwargs: frame_or_series([x, x], **extra, **kwargs)
2949+
else:
2950+
return lambda x, **kwargs: frame_or_series(
2951+
{"A": [x, x]}, **extra, **kwargs
2952+
)
29342953

29352954
@pytest.mark.parametrize("dtype", ["M8[ns]", "m8[ns]"])
29362955
def test_from_nat_scalar(self, dtype, constructor):
@@ -2951,7 +2970,8 @@ def test_from_timestamp_scalar_preserves_nanos(self, constructor):
29512970
assert get1(obj) == ts
29522971

29532972
def test_from_timedelta64_scalar_object(self, constructor, request):
2954-
if constructor.func is DataFrame and _np_version_under1p20:
2973+
if getattr(constructor, "func", None) is DataFrame and _np_version_under1p20:
2974+
# getattr check means we only xfail when box is None
29552975
mark = pytest.mark.xfail(
29562976
reason="np.array(td64, dtype=object) converts to int"
29572977
)
@@ -2964,7 +2984,15 @@ def test_from_timedelta64_scalar_object(self, constructor, request):
29642984
assert isinstance(get1(obj), np.timedelta64)
29652985

29662986
@pytest.mark.parametrize("cls", [np.datetime64, np.timedelta64])
2967-
def test_from_scalar_datetimelike_mismatched(self, constructor, cls):
2987+
def test_from_scalar_datetimelike_mismatched(self, constructor, cls, request):
2988+
node = request.node
2989+
params = node.callspec.params
2990+
if params["frame_or_series"] is DataFrame and params["constructor"] is not None:
2991+
mark = pytest.mark.xfail(
2992+
reason="DataFrame incorrectly allows mismatched datetimelike"
2993+
)
2994+
node.add_marker(mark)
2995+
29682996
scalar = cls("NaT", "ns")
29692997
dtype = {np.datetime64: "m8[ns]", np.timedelta64: "M8[ns]"}[cls]
29702998

0 commit comments

Comments
 (0)