diff --git a/doc/source/whatsnew/v1.0.0.rst b/doc/source/whatsnew/v1.0.0.rst index 71374a3bff692..9998a9a847643 100644 --- a/doc/source/whatsnew/v1.0.0.rst +++ b/doc/source/whatsnew/v1.0.0.rst @@ -119,6 +119,8 @@ Datetimelike - Bug in :meth:`pandas.core.groupby.SeriesGroupBy.nunique` where ``NaT`` values were interfering with the count of unique values (:issue:`27951`) - Bug in :class:`Timestamp` subtraction when subtracting a :class:`Timestamp` from a ``np.datetime64`` object incorrectly raising ``TypeError`` (:issue:`28286`) - Addition and subtraction of integer or integer-dtype arrays with :class:`Timestamp` will now raise ``NullFrequencyError`` instead of ``ValueError`` (:issue:`28268`) +- Bug in :class:`Series` and :class:`DataFrame` with integer dtype failing to raise ``TypeError`` when adding or subtracting a ``np.datetime64`` object (:issue:`28080`) +- Timedelta diff --git a/pandas/_libs/tslibs/nattype.pyx b/pandas/_libs/tslibs/nattype.pyx index 020d1acf0b4ce..fcf75968c06da 100644 --- a/pandas/_libs/tslibs/nattype.pyx +++ b/pandas/_libs/tslibs/nattype.pyx @@ -150,6 +150,8 @@ cdef class _NaT(datetime): result = np.empty(other.shape, dtype="datetime64[ns]") result.fill("NaT") return result + raise TypeError("Cannot add NaT to ndarray with dtype {dtype}" + .format(dtype=other.dtype)) return NotImplemented @@ -201,6 +203,10 @@ cdef class _NaT(datetime): result.fill("NaT") return result + raise TypeError( + "Cannot subtract NaT from ndarray with dtype {dtype}" + .format(dtype=other.dtype)) + return NotImplemented def __pos__(self): diff --git a/pandas/core/ops/__init__.py b/pandas/core/ops/__init__.py index 0e258496abc24..016feff7e3beb 100644 --- a/pandas/core/ops/__init__.py +++ b/pandas/core/ops/__init__.py @@ -9,7 +9,7 @@ import numpy as np -from pandas._libs import Timedelta, lib, ops as libops +from pandas._libs import Timedelta, Timestamp, lib, ops as libops from pandas.errors import NullFrequencyError from pandas.util._decorators import Appender @@ -148,13 +148,24 @@ def maybe_upcast_for_op(obj, shape: Tuple[int, ...]): Be careful to call this *after* determining the `name` attribute to be attached to the result of the arithmetic operation. """ - from pandas.core.arrays import TimedeltaArray + from pandas.core.arrays import DatetimeArray, TimedeltaArray if type(obj) is datetime.timedelta: # GH#22390 cast up to Timedelta to rely on Timedelta # implementation; otherwise operation against numeric-dtype # raises TypeError return Timedelta(obj) + elif isinstance(obj, np.datetime64): + # GH#28080 numpy casts integer-dtype to datetime64 when doing + # array[int] + datetime64, which we do not allow + if isna(obj): + # Avoid possible ambiguities with pd.NaT + obj = obj.astype("datetime64[ns]") + right = np.broadcast_to(obj, shape) + return DatetimeArray(right) + + return Timestamp(obj) + elif isinstance(obj, np.timedelta64): if isna(obj): # wrapping timedelta64("NaT") in Timedelta returns NaT, @@ -624,7 +635,13 @@ def wrapper(left, right): keep_null_freq = isinstance( right, - (ABCDatetimeIndex, ABCDatetimeArray, ABCTimedeltaIndex, ABCTimedeltaArray), + ( + ABCDatetimeIndex, + ABCDatetimeArray, + ABCTimedeltaIndex, + ABCTimedeltaArray, + Timestamp, + ), ) left, right = _align_method_SERIES(left, right) @@ -635,13 +652,9 @@ def wrapper(left, right): rvalues = maybe_upcast_for_op(rvalues, lvalues.shape) - if should_extension_dispatch(lvalues, rvalues): - result = dispatch_to_extension_op(op, lvalues, rvalues, keep_null_freq) - - elif is_timedelta64_dtype(rvalues) or isinstance(rvalues, ABCDatetimeArray): - # We should only get here with td64 rvalues with non-scalar values - # for rvalues upcast by maybe_upcast_for_op - assert not isinstance(rvalues, (np.timedelta64, np.ndarray)) + if should_extension_dispatch(left, rvalues) or isinstance( + rvalues, (ABCTimedeltaArray, ABCDatetimeArray, Timestamp) + ): result = dispatch_to_extension_op(op, lvalues, rvalues, keep_null_freq) else: diff --git a/pandas/tests/arithmetic/test_numeric.py b/pandas/tests/arithmetic/test_numeric.py index 8e7e72fcdc580..584e22f8488f5 100644 --- a/pandas/tests/arithmetic/test_numeric.py +++ b/pandas/tests/arithmetic/test_numeric.py @@ -73,10 +73,10 @@ def test_compare_invalid(self): # ------------------------------------------------------------------ -# Numeric dtypes Arithmetic with Timedelta Scalar +# Numeric dtypes Arithmetic with Datetime/Timedelta Scalar -class TestNumericArraylikeArithmeticWithTimedeltaLike: +class TestNumericArraylikeArithmeticWithDatetimeLike: # TODO: also check name retentention @pytest.mark.parametrize("box_cls", [np.array, pd.Index, pd.Series]) @@ -235,6 +235,30 @@ def test_add_sub_timedeltalike_invalid(self, numeric_idx, other, box): with pytest.raises(TypeError): other - left + @pytest.mark.parametrize( + "other", + [ + pd.Timestamp.now().to_pydatetime(), + pd.Timestamp.now(tz="UTC").to_pydatetime(), + pd.Timestamp.now().to_datetime64(), + pd.NaT, + ], + ) + @pytest.mark.filterwarnings("ignore:elementwise comp:DeprecationWarning") + def test_add_sub_datetimelike_invalid(self, numeric_idx, other, box): + # GH#28080 numeric+datetime64 should raise; Timestamp raises + # NullFrequencyError instead of TypeError so is excluded. + left = tm.box_expected(numeric_idx, box) + + with pytest.raises(TypeError): + left + other + with pytest.raises(TypeError): + other + left + with pytest.raises(TypeError): + left - other + with pytest.raises(TypeError): + other - left + # ------------------------------------------------------------------ # Arithmetic