diff --git a/doc/source/whatsnew/v1.0.0.rst b/doc/source/whatsnew/v1.0.0.rst index 29139a0a14991..ef514e7e89b57 100644 --- a/doc/source/whatsnew/v1.0.0.rst +++ b/doc/source/whatsnew/v1.0.0.rst @@ -547,6 +547,7 @@ or ``matplotlib.Axes.plot``. See :ref:`plotting.formatters` for more. - Removed support for nested renaming in :meth:`DataFrame.aggregate`, :meth:`Series.aggregate`, :meth:`DataFrameGroupBy.aggregate`, :meth:`SeriesGroupBy.aggregate`, :meth:`Rolling.aggregate` (:issue:`18529`) - Passing ``datetime64`` data to :class:`TimedeltaIndex` or ``timedelta64`` data to ``DatetimeIndex`` now raises ``TypeError`` (:issue:`23539`, :issue:`23937`) - A tuple passed to :meth:`DataFrame.groupby` is now exclusively treated as a single key (:issue:`18314`) +- Addition and subtraction of ``int`` or integer-arrays is no longer allowed in :class:`Timestamp`, :class:`DatetimeIndex`, :class:`TimedeltaIndex`, use ``obj + n * obj.freq`` instead of ``obj + n`` (:issue:`22535`) - Removed :meth:`Series.from_array` (:issue:`18258`) - Removed :meth:`DataFrame.from_items` (:issue:`18458`) - Removed :meth:`DataFrame.as_matrix`, :meth:`Series.as_matrix` (:issue:`18458`) diff --git a/pandas/_libs/tslibs/c_timestamp.pyx b/pandas/_libs/tslibs/c_timestamp.pyx index 02e252219453b..6e6b809b9b5a6 100644 --- a/pandas/_libs/tslibs/c_timestamp.pyx +++ b/pandas/_libs/tslibs/c_timestamp.pyx @@ -51,15 +51,18 @@ class NullFrequencyError(ValueError): pass -def maybe_integer_op_deprecated(obj): - # GH#22535 add/sub of integers and int-arrays is deprecated - if obj.freq is not None: - warnings.warn("Addition/subtraction of integers and integer-arrays " - f"to {type(obj).__name__} is deprecated, " - "will be removed in a future " - "version. Instead of adding/subtracting `n`, use " - "`n * self.freq`" - , FutureWarning) +def integer_op_not_supported(obj): + # GH#22535 add/sub of integers and int-arrays is no longer allowed + # Note we return rather than raise the exception so we can raise in + # the caller; mypy finds this more palatable. + cls = type(obj).__name__ + + int_addsub_msg = ( + f"Addition/subtraction of integers and integer-arrays with {cls} is " + "no longer supported. Instead of adding/subtracting `n`, " + "use `n * obj.freq`" + ) + return TypeError(int_addsub_msg) cdef class _Timestamp(datetime): @@ -229,15 +232,7 @@ cdef class _Timestamp(datetime): return type(self)(self.value + other_int, tz=self.tzinfo, freq=self.freq) elif is_integer_object(other): - maybe_integer_op_deprecated(self) - - if self is NaT: - # to be compat with Period - return NaT - elif self.freq is None: - raise NullFrequencyError( - "Cannot add integral value to Timestamp without freq.") - return type(self)((self.freq * other).apply(self), freq=self.freq) + raise integer_op_not_supported(self) elif PyDelta_Check(other) or hasattr(other, 'delta'): # delta --> offsets.Tick @@ -256,12 +251,7 @@ cdef class _Timestamp(datetime): elif is_array(other): if other.dtype.kind in ['i', 'u']: - maybe_integer_op_deprecated(self) - if self.freq is None: - raise NullFrequencyError( - "Cannot add integer-dtype array " - "to Timestamp without freq.") - return self.freq * other + self + raise integer_op_not_supported(self) # index/series like elif hasattr(other, '_typ'): @@ -283,12 +273,7 @@ cdef class _Timestamp(datetime): elif is_array(other): if other.dtype.kind in ['i', 'u']: - maybe_integer_op_deprecated(self) - if self.freq is None: - raise NullFrequencyError( - "Cannot subtract integer-dtype array " - "from Timestamp without freq.") - return self - self.freq * other + raise integer_op_not_supported(self) typ = getattr(other, '_typ', None) if typ is not None: diff --git a/pandas/core/arrays/datetimelike.py b/pandas/core/arrays/datetimelike.py index dc3c49b7e06a9..ed2d8ff8320a8 100644 --- a/pandas/core/arrays/datetimelike.py +++ b/pandas/core/arrays/datetimelike.py @@ -6,7 +6,7 @@ import numpy as np from pandas._libs import NaT, NaTType, Timestamp, algos, iNaT, lib -from pandas._libs.tslibs.c_timestamp import maybe_integer_op_deprecated +from pandas._libs.tslibs.c_timestamp import integer_op_not_supported from pandas._libs.tslibs.period import DIFFERENT_FREQ, IncompatibleFrequency, Period from pandas._libs.tslibs.timedeltas import Timedelta, delta_to_nanoseconds from pandas._libs.tslibs.timestamps import RoundTo, round_nsint64 @@ -1207,7 +1207,7 @@ def __add__(self, other): # This check must come after the check for np.timedelta64 # as is_integer returns True for these if not is_period_dtype(self): - maybe_integer_op_deprecated(self) + raise integer_op_not_supported(self) result = self._time_shift(other) # array-like others @@ -1222,7 +1222,7 @@ def __add__(self, other): return self._add_datetime_arraylike(other) elif is_integer_dtype(other): if not is_period_dtype(self): - maybe_integer_op_deprecated(self) + raise integer_op_not_supported(self) result = self._addsub_int_array(other, operator.add) else: # Includes Categorical, other ExtensionArrays @@ -1259,7 +1259,7 @@ def __sub__(self, other): # This check must come after the check for np.timedelta64 # as is_integer returns True for these if not is_period_dtype(self): - maybe_integer_op_deprecated(self) + raise integer_op_not_supported(self) result = self._time_shift(-other) elif isinstance(other, Period): @@ -1280,7 +1280,7 @@ def __sub__(self, other): result = self._sub_period_array(other) elif is_integer_dtype(other): if not is_period_dtype(self): - maybe_integer_op_deprecated(self) + raise integer_op_not_supported(self) result = self._addsub_int_array(other, operator.sub) else: # Includes ExtensionArrays, float_dtype diff --git a/pandas/tests/arithmetic/test_datetime64.py b/pandas/tests/arithmetic/test_datetime64.py index 29d425039551a..90a41d43a2a88 100644 --- a/pandas/tests/arithmetic/test_datetime64.py +++ b/pandas/tests/arithmetic/test_datetime64.py @@ -13,7 +13,7 @@ from pandas._libs.tslibs.conversion import localize_pydatetime from pandas._libs.tslibs.offsets import shift_months from pandas.compat.numpy import np_datetime64_compat -from pandas.errors import NullFrequencyError, PerformanceWarning +from pandas.errors import PerformanceWarning import pandas as pd from pandas import ( @@ -1856,10 +1856,8 @@ def test_dt64_series_add_intlike(self, tz, op): method = getattr(ser, op) msg = "|".join( [ - "incompatible type for a .* operation", - "cannot evaluate a numeric op", - "ufunc .* cannot use operands", - "cannot (add|subtract)", + "Addition/subtraction of integers and integer-arrays", + "cannot subtract .* from ndarray", ] ) with pytest.raises(TypeError, match=msg): @@ -1941,38 +1939,23 @@ class TestDatetimeIndexArithmetic: # ------------------------------------------------------------- # Binary operations DatetimeIndex and int - def test_dti_add_int(self, tz_naive_fixture, one): + def test_dti_addsub_int(self, tz_naive_fixture, one): # Variants of `one` for #19012 tz = tz_naive_fixture rng = pd.date_range("2000-01-01 09:00", freq="H", periods=10, tz=tz) - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - result = rng + one - expected = pd.date_range("2000-01-01 10:00", freq="H", periods=10, tz=tz) - tm.assert_index_equal(result, expected) + msg = "Addition/subtraction of integers" - def test_dti_iadd_int(self, tz_naive_fixture, one): - tz = tz_naive_fixture - rng = pd.date_range("2000-01-01 09:00", freq="H", periods=10, tz=tz) - expected = pd.date_range("2000-01-01 10:00", freq="H", periods=10, tz=tz) - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): + with pytest.raises(TypeError, match=msg): + rng + one + + with pytest.raises(TypeError, match=msg): rng += one - tm.assert_index_equal(rng, expected) - def test_dti_sub_int(self, tz_naive_fixture, one): - tz = tz_naive_fixture - rng = pd.date_range("2000-01-01 09:00", freq="H", periods=10, tz=tz) - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - result = rng - one - expected = pd.date_range("2000-01-01 08:00", freq="H", periods=10, tz=tz) - tm.assert_index_equal(result, expected) + with pytest.raises(TypeError, match=msg): + rng - one - def test_dti_isub_int(self, tz_naive_fixture, one): - tz = tz_naive_fixture - rng = pd.date_range("2000-01-01 09:00", freq="H", periods=10, tz=tz) - expected = pd.date_range("2000-01-01 08:00", freq="H", periods=10, tz=tz) - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): + with pytest.raises(TypeError, match=msg): rng -= one - tm.assert_index_equal(rng, expected) # ------------------------------------------------------------- # __add__/__sub__ with integer arrays @@ -1984,14 +1967,13 @@ def test_dti_add_intarray_tick(self, int_holder, freq): dti = pd.date_range("2016-01-01", periods=2, freq=freq) other = int_holder([4, -1]) - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - expected = DatetimeIndex([dti[n] + other[n] for n in range(len(dti))]) - result = dti + other - tm.assert_index_equal(result, expected) + msg = "Addition/subtraction of integers" - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - result = other + dti - tm.assert_index_equal(result, expected) + with pytest.raises(TypeError, match=msg): + dti + other + + with pytest.raises(TypeError, match=msg): + other + dti @pytest.mark.parametrize("freq", ["W", "M", "MS", "Q"]) @pytest.mark.parametrize("int_holder", [np.array, pd.Index]) @@ -2000,34 +1982,26 @@ def test_dti_add_intarray_non_tick(self, int_holder, freq): dti = pd.date_range("2016-01-01", periods=2, freq=freq) other = int_holder([4, -1]) - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - expected = DatetimeIndex([dti[n] + other[n] for n in range(len(dti))]) + msg = "Addition/subtraction of integers" - # tm.assert_produces_warning does not handle cases where we expect - # two warnings, in this case PerformanceWarning and FutureWarning. - # Until that is fixed, we don't catch either - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - result = dti + other - tm.assert_index_equal(result, expected) + with pytest.raises(TypeError, match=msg): + dti + other - with warnings.catch_warnings(): - warnings.simplefilter("ignore") - result = other + dti - tm.assert_index_equal(result, expected) + with pytest.raises(TypeError, match=msg): + other + dti @pytest.mark.parametrize("int_holder", [np.array, pd.Index]) def test_dti_add_intarray_no_freq(self, int_holder): # GH#19959 dti = pd.DatetimeIndex(["2016-01-01", "NaT", "2017-04-05 06:07:08"]) other = int_holder([9, 4, -1]) - nfmsg = "Cannot shift with no freq" tmsg = "cannot subtract DatetimeArray from" - with pytest.raises(NullFrequencyError, match=nfmsg): + msg = "Addition/subtraction of integers" + with pytest.raises(TypeError, match=msg): dti + other - with pytest.raises(NullFrequencyError, match=nfmsg): + with pytest.raises(TypeError, match=msg): other + dti - with pytest.raises(NullFrequencyError, match=nfmsg): + with pytest.raises(TypeError, match=msg): dti - other with pytest.raises(TypeError, match=tmsg): other - dti diff --git a/pandas/tests/arithmetic/test_timedelta64.py b/pandas/tests/arithmetic/test_timedelta64.py index 79270c2ea2cab..8ea38170bb489 100644 --- a/pandas/tests/arithmetic/test_timedelta64.py +++ b/pandas/tests/arithmetic/test_timedelta64.py @@ -5,7 +5,7 @@ import numpy as np import pytest -from pandas.errors import NullFrequencyError, OutOfBoundsDatetime, PerformanceWarning +from pandas.errors import OutOfBoundsDatetime, PerformanceWarning import pandas as pd from pandas import ( @@ -409,7 +409,7 @@ def test_addition_ops(self): tdi[0:1] + dti # random indexes - with pytest.raises(NullFrequencyError): + with pytest.raises(TypeError): tdi + pd.Int64Index([1, 2, 3]) # this is a union! @@ -997,17 +997,13 @@ def test_td64arr_addsub_numeric_invalid(self, box_with_array, other): tdser = pd.Series(["59 Days", "59 Days", "NaT"], dtype="m8[ns]") tdser = tm.box_expected(tdser, box) - err = TypeError - if box in [pd.Index, tm.to_array] and not isinstance(other, float): - err = NullFrequencyError - - with pytest.raises(err): + with pytest.raises(TypeError): tdser + other - with pytest.raises(err): + with pytest.raises(TypeError): other + tdser - with pytest.raises(err): + with pytest.raises(TypeError): tdser - other - with pytest.raises(err): + with pytest.raises(TypeError): other - tdser @pytest.mark.parametrize( @@ -1039,18 +1035,15 @@ def test_td64arr_add_sub_numeric_arr_invalid(self, box_with_array, vec, dtype): box = box_with_array tdser = pd.Series(["59 Days", "59 Days", "NaT"], dtype="m8[ns]") tdser = tm.box_expected(tdser, box) - err = TypeError - if box in [pd.Index, tm.to_array] and not dtype.startswith("float"): - err = NullFrequencyError vector = vec.astype(dtype) - with pytest.raises(err): + with pytest.raises(TypeError): tdser + vector - with pytest.raises(err): + with pytest.raises(TypeError): vector + tdser - with pytest.raises(err): + with pytest.raises(TypeError): tdser - vector - with pytest.raises(err): + with pytest.raises(TypeError): vector - tdser # ------------------------------------------------------------------ diff --git a/pandas/tests/indexes/datetimes/test_arithmetic.py b/pandas/tests/indexes/datetimes/test_arithmetic.py index 2bcaa973acd6b..6dd7bee8207d3 100644 --- a/pandas/tests/indexes/datetimes/test_arithmetic.py +++ b/pandas/tests/indexes/datetimes/test_arithmetic.py @@ -69,17 +69,11 @@ def test_dti_shift_freqs(self): def test_dti_shift_int(self): rng = date_range("1/1/2000", periods=20) - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - # GH#22535 - result = rng + 5 - + result = rng + 5 * rng.freq expected = rng.shift(5) tm.assert_index_equal(result, expected) - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - # GH#22535 - result = rng - 5 - + result = rng - 5 * rng.freq expected = rng.shift(-5) tm.assert_index_equal(result, expected) diff --git a/pandas/tests/indexes/timedeltas/test_arithmetic.py b/pandas/tests/indexes/timedeltas/test_arithmetic.py index 4544657f79af7..3603719eab036 100644 --- a/pandas/tests/indexes/timedeltas/test_arithmetic.py +++ b/pandas/tests/indexes/timedeltas/test_arithmetic.py @@ -100,86 +100,56 @@ def test_shift_no_freq(self): # ------------------------------------------------------------- # Binary operations TimedeltaIndex and integer - def test_tdi_add_int(self, one): - # Variants of `one` for #19012 + def test_tdi_add_sub_int(self, one): + # Variants of `one` for #19012, deprecated GH#22535 rng = timedelta_range("1 days 09:00:00", freq="H", periods=10) - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - # GH#22535 - result = rng + one - expected = timedelta_range("1 days 10:00:00", freq="H", periods=10) - tm.assert_index_equal(result, expected) + msg = "Addition/subtraction of integers" - def test_tdi_iadd_int(self, one): - rng = timedelta_range("1 days 09:00:00", freq="H", periods=10) - expected = timedelta_range("1 days 10:00:00", freq="H", periods=10) - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - # GH#22535 + with pytest.raises(TypeError, match=msg): + rng + one + with pytest.raises(TypeError, match=msg): rng += one - tm.assert_index_equal(rng, expected) - - def test_tdi_sub_int(self, one): - rng = timedelta_range("1 days 09:00:00", freq="H", periods=10) - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - # GH#22535 - result = rng - one - expected = timedelta_range("1 days 08:00:00", freq="H", periods=10) - tm.assert_index_equal(result, expected) - - def test_tdi_isub_int(self, one): - rng = timedelta_range("1 days 09:00:00", freq="H", periods=10) - expected = timedelta_range("1 days 08:00:00", freq="H", periods=10) - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - # GH#22535 + with pytest.raises(TypeError, match=msg): + rng - one + with pytest.raises(TypeError, match=msg): rng -= one - tm.assert_index_equal(rng, expected) # ------------------------------------------------------------- # __add__/__sub__ with integer arrays @pytest.mark.parametrize("box", [np.array, pd.Index]) - def test_tdi_add_integer_array(self, box): - # GH#19959 + def test_tdi_add_sub_integer_array(self, box): + # GH#19959, deprecated GH#22535 rng = timedelta_range("1 days 09:00:00", freq="H", periods=3) other = box([4, 3, 2]) - expected = TimedeltaIndex(["1 day 13:00:00"] * 3) - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - # GH#22535 - result = rng + other - tm.assert_index_equal(result, expected) + msg = "Addition/subtraction of integers and integer-arrays" - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - # GH#22535 - result = other + rng - tm.assert_index_equal(result, expected) + with pytest.raises(TypeError, match=msg): + rng + other - @pytest.mark.parametrize("box", [np.array, pd.Index]) - def test_tdi_sub_integer_array(self, box): - # GH#19959 - rng = timedelta_range("9H", freq="H", periods=3) - other = box([4, 3, 2]) - expected = TimedeltaIndex(["5H", "7H", "9H"]) - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - # GH#22535 - result = rng - other - tm.assert_index_equal(result, expected) + with pytest.raises(TypeError, match=msg): + other + rng + + with pytest.raises(TypeError, match=msg): + rng - other - with tm.assert_produces_warning(FutureWarning, check_stacklevel=False): - # GH#22535 - result = other - rng - tm.assert_index_equal(result, -expected) + with pytest.raises(TypeError, match=msg): + other - rng @pytest.mark.parametrize("box", [np.array, pd.Index]) def test_tdi_addsub_integer_array_no_freq(self, box): # GH#19959 tdi = TimedeltaIndex(["1 Day", "NaT", "3 Hours"]) other = box([14, -1, 16]) - with pytest.raises(NullFrequencyError): + msg = "Addition/subtraction of integers" + + with pytest.raises(TypeError, match=msg): tdi + other - with pytest.raises(NullFrequencyError): + with pytest.raises(TypeError, match=msg): other + tdi - with pytest.raises(NullFrequencyError): + with pytest.raises(TypeError, match=msg): tdi - other - with pytest.raises(NullFrequencyError): + with pytest.raises(TypeError, match=msg): other - tdi # ------------------------------------------------------------- diff --git a/pandas/tests/scalar/timestamp/test_arithmetic.py b/pandas/tests/scalar/timestamp/test_arithmetic.py index 9634c6d822236..1cab007c20a0e 100644 --- a/pandas/tests/scalar/timestamp/test_arithmetic.py +++ b/pandas/tests/scalar/timestamp/test_arithmetic.py @@ -3,10 +3,7 @@ import numpy as np import pytest -from pandas.errors import NullFrequencyError - from pandas import Timedelta, Timestamp -import pandas.util.testing as tm from pandas.tseries import offsets from pandas.tseries.frequencies import to_offset @@ -97,10 +94,12 @@ def test_addition_subtraction_types(self): # addition/subtraction of integers ts = Timestamp(dt, freq="D") - with tm.assert_produces_warning(FutureWarning): + msg = "Addition/subtraction of integers" + with pytest.raises(TypeError, match=msg): # GH#22535 add/sub with integers is deprecated - assert type(ts + 1) == Timestamp - assert type(ts - 1) == Timestamp + ts + 1 + with pytest.raises(TypeError, match=msg): + ts - 1 # Timestamp + datetime not supported, though subtraction is supported # and yields timedelta more tests in tseries/base/tests/test_base.py @@ -129,11 +128,6 @@ def test_addition_subtraction_preserve_frequency(self, freq, td, td64): ts = Timestamp("2014-03-05 00:00:00", freq=freq) original_freq = ts.freq - with tm.assert_produces_warning(FutureWarning): - # GH#22535 add/sub with integers is deprecated - assert (ts + 1).freq == original_freq - assert (ts - 1).freq == original_freq - assert (ts + 1 * original_freq).freq == original_freq assert (ts - 1 * original_freq).freq == original_freq @@ -179,12 +173,13 @@ def test_timestamp_add_timedelta64_unit(self, other, expected_difference): ], ) def test_add_int_no_freq_raises(self, ts, other): - with pytest.raises(NullFrequencyError, match="without freq"): + msg = "Addition/subtraction of integers and integer-arrays" + with pytest.raises(TypeError, match=msg): ts + other - with pytest.raises(NullFrequencyError, match="without freq"): + with pytest.raises(TypeError, match=msg): other + ts - with pytest.raises(NullFrequencyError, match="without freq"): + with pytest.raises(TypeError, match=msg): ts - other with pytest.raises(TypeError): other - ts @@ -206,17 +201,14 @@ def test_add_int_no_freq_raises(self, ts, other): ], ) def test_add_int_with_freq(self, ts, other): - with tm.assert_produces_warning(FutureWarning): - result1 = ts + other - with tm.assert_produces_warning(FutureWarning): - result2 = other + ts - assert np.all(result1 == result2) - - with tm.assert_produces_warning(FutureWarning): - result = result1 - other + with pytest.raises(TypeError): + ts + other + with pytest.raises(TypeError): + other + ts - assert np.all(result == ts) + with pytest.raises(TypeError): + ts - other with pytest.raises(TypeError): other - ts