Skip to content

Commit 5c6748e

Browse files
jbrockmendelproost
authored andcommitted
DEPR: integer add/sub for Timestamp, DatetimeIndex, TimedeltaIndex (pandas-dev#30117)
1 parent 3eb4466 commit 5c6748e

File tree

8 files changed

+102
-193
lines changed

8 files changed

+102
-193
lines changed

doc/source/whatsnew/v1.0.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -547,6 +547,7 @@ or ``matplotlib.Axes.plot``. See :ref:`plotting.formatters` for more.
547547
- Removed support for nested renaming in :meth:`DataFrame.aggregate`, :meth:`Series.aggregate`, :meth:`DataFrameGroupBy.aggregate`, :meth:`SeriesGroupBy.aggregate`, :meth:`Rolling.aggregate` (:issue:`18529`)
548548
- Passing ``datetime64`` data to :class:`TimedeltaIndex` or ``timedelta64`` data to ``DatetimeIndex`` now raises ``TypeError`` (:issue:`23539`, :issue:`23937`)
549549
- A tuple passed to :meth:`DataFrame.groupby` is now exclusively treated as a single key (:issue:`18314`)
550+
- 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`)
550551
- Removed :meth:`Series.from_array` (:issue:`18258`)
551552
- Removed :meth:`DataFrame.from_items` (:issue:`18458`)
552553
- Removed :meth:`DataFrame.as_matrix`, :meth:`Series.as_matrix` (:issue:`18458`)

pandas/_libs/tslibs/c_timestamp.pyx

+15-30
Original file line numberDiff line numberDiff line change
@@ -51,15 +51,18 @@ class NullFrequencyError(ValueError):
5151
pass
5252

5353

54-
def maybe_integer_op_deprecated(obj):
55-
# GH#22535 add/sub of integers and int-arrays is deprecated
56-
if obj.freq is not None:
57-
warnings.warn("Addition/subtraction of integers and integer-arrays "
58-
f"to {type(obj).__name__} is deprecated, "
59-
"will be removed in a future "
60-
"version. Instead of adding/subtracting `n`, use "
61-
"`n * self.freq`"
62-
, FutureWarning)
54+
def integer_op_not_supported(obj):
55+
# GH#22535 add/sub of integers and int-arrays is no longer allowed
56+
# Note we return rather than raise the exception so we can raise in
57+
# the caller; mypy finds this more palatable.
58+
cls = type(obj).__name__
59+
60+
int_addsub_msg = (
61+
f"Addition/subtraction of integers and integer-arrays with {cls} is "
62+
"no longer supported. Instead of adding/subtracting `n`, "
63+
"use `n * obj.freq`"
64+
)
65+
return TypeError(int_addsub_msg)
6366

6467

6568
cdef class _Timestamp(datetime):
@@ -229,15 +232,7 @@ cdef class _Timestamp(datetime):
229232
return type(self)(self.value + other_int, tz=self.tzinfo, freq=self.freq)
230233

231234
elif is_integer_object(other):
232-
maybe_integer_op_deprecated(self)
233-
234-
if self is NaT:
235-
# to be compat with Period
236-
return NaT
237-
elif self.freq is None:
238-
raise NullFrequencyError(
239-
"Cannot add integral value to Timestamp without freq.")
240-
return type(self)((self.freq * other).apply(self), freq=self.freq)
235+
raise integer_op_not_supported(self)
241236

242237
elif PyDelta_Check(other) or hasattr(other, 'delta'):
243238
# delta --> offsets.Tick
@@ -256,12 +251,7 @@ cdef class _Timestamp(datetime):
256251

257252
elif is_array(other):
258253
if other.dtype.kind in ['i', 'u']:
259-
maybe_integer_op_deprecated(self)
260-
if self.freq is None:
261-
raise NullFrequencyError(
262-
"Cannot add integer-dtype array "
263-
"to Timestamp without freq.")
264-
return self.freq * other + self
254+
raise integer_op_not_supported(self)
265255

266256
# index/series like
267257
elif hasattr(other, '_typ'):
@@ -283,12 +273,7 @@ cdef class _Timestamp(datetime):
283273

284274
elif is_array(other):
285275
if other.dtype.kind in ['i', 'u']:
286-
maybe_integer_op_deprecated(self)
287-
if self.freq is None:
288-
raise NullFrequencyError(
289-
"Cannot subtract integer-dtype array "
290-
"from Timestamp without freq.")
291-
return self - self.freq * other
276+
raise integer_op_not_supported(self)
292277

293278
typ = getattr(other, '_typ', None)
294279
if typ is not None:

pandas/core/arrays/datetimelike.py

+5-5
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import numpy as np
77

88
from pandas._libs import NaT, NaTType, Timestamp, algos, iNaT, lib
9-
from pandas._libs.tslibs.c_timestamp import maybe_integer_op_deprecated
9+
from pandas._libs.tslibs.c_timestamp import integer_op_not_supported
1010
from pandas._libs.tslibs.period import DIFFERENT_FREQ, IncompatibleFrequency, Period
1111
from pandas._libs.tslibs.timedeltas import Timedelta, delta_to_nanoseconds
1212
from pandas._libs.tslibs.timestamps import RoundTo, round_nsint64
@@ -1207,7 +1207,7 @@ def __add__(self, other):
12071207
# This check must come after the check for np.timedelta64
12081208
# as is_integer returns True for these
12091209
if not is_period_dtype(self):
1210-
maybe_integer_op_deprecated(self)
1210+
raise integer_op_not_supported(self)
12111211
result = self._time_shift(other)
12121212

12131213
# array-like others
@@ -1222,7 +1222,7 @@ def __add__(self, other):
12221222
return self._add_datetime_arraylike(other)
12231223
elif is_integer_dtype(other):
12241224
if not is_period_dtype(self):
1225-
maybe_integer_op_deprecated(self)
1225+
raise integer_op_not_supported(self)
12261226
result = self._addsub_int_array(other, operator.add)
12271227
else:
12281228
# Includes Categorical, other ExtensionArrays
@@ -1259,7 +1259,7 @@ def __sub__(self, other):
12591259
# This check must come after the check for np.timedelta64
12601260
# as is_integer returns True for these
12611261
if not is_period_dtype(self):
1262-
maybe_integer_op_deprecated(self)
1262+
raise integer_op_not_supported(self)
12631263
result = self._time_shift(-other)
12641264

12651265
elif isinstance(other, Period):
@@ -1280,7 +1280,7 @@ def __sub__(self, other):
12801280
result = self._sub_period_array(other)
12811281
elif is_integer_dtype(other):
12821282
if not is_period_dtype(self):
1283-
maybe_integer_op_deprecated(self)
1283+
raise integer_op_not_supported(self)
12841284
result = self._addsub_int_array(other, operator.sub)
12851285
else:
12861286
# Includes ExtensionArrays, float_dtype

pandas/tests/arithmetic/test_datetime64.py

+27-53
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
from pandas._libs.tslibs.conversion import localize_pydatetime
1414
from pandas._libs.tslibs.offsets import shift_months
1515
from pandas.compat.numpy import np_datetime64_compat
16-
from pandas.errors import NullFrequencyError, PerformanceWarning
16+
from pandas.errors import PerformanceWarning
1717

1818
import pandas as pd
1919
from pandas import (
@@ -1856,10 +1856,8 @@ def test_dt64_series_add_intlike(self, tz, op):
18561856
method = getattr(ser, op)
18571857
msg = "|".join(
18581858
[
1859-
"incompatible type for a .* operation",
1860-
"cannot evaluate a numeric op",
1861-
"ufunc .* cannot use operands",
1862-
"cannot (add|subtract)",
1859+
"Addition/subtraction of integers and integer-arrays",
1860+
"cannot subtract .* from ndarray",
18631861
]
18641862
)
18651863
with pytest.raises(TypeError, match=msg):
@@ -1941,38 +1939,23 @@ class TestDatetimeIndexArithmetic:
19411939
# -------------------------------------------------------------
19421940
# Binary operations DatetimeIndex and int
19431941

1944-
def test_dti_add_int(self, tz_naive_fixture, one):
1942+
def test_dti_addsub_int(self, tz_naive_fixture, one):
19451943
# Variants of `one` for #19012
19461944
tz = tz_naive_fixture
19471945
rng = pd.date_range("2000-01-01 09:00", freq="H", periods=10, tz=tz)
1948-
with tm.assert_produces_warning(FutureWarning, check_stacklevel=False):
1949-
result = rng + one
1950-
expected = pd.date_range("2000-01-01 10:00", freq="H", periods=10, tz=tz)
1951-
tm.assert_index_equal(result, expected)
1946+
msg = "Addition/subtraction of integers"
19521947

1953-
def test_dti_iadd_int(self, tz_naive_fixture, one):
1954-
tz = tz_naive_fixture
1955-
rng = pd.date_range("2000-01-01 09:00", freq="H", periods=10, tz=tz)
1956-
expected = pd.date_range("2000-01-01 10:00", freq="H", periods=10, tz=tz)
1957-
with tm.assert_produces_warning(FutureWarning, check_stacklevel=False):
1948+
with pytest.raises(TypeError, match=msg):
1949+
rng + one
1950+
1951+
with pytest.raises(TypeError, match=msg):
19581952
rng += one
1959-
tm.assert_index_equal(rng, expected)
19601953

1961-
def test_dti_sub_int(self, tz_naive_fixture, one):
1962-
tz = tz_naive_fixture
1963-
rng = pd.date_range("2000-01-01 09:00", freq="H", periods=10, tz=tz)
1964-
with tm.assert_produces_warning(FutureWarning, check_stacklevel=False):
1965-
result = rng - one
1966-
expected = pd.date_range("2000-01-01 08:00", freq="H", periods=10, tz=tz)
1967-
tm.assert_index_equal(result, expected)
1954+
with pytest.raises(TypeError, match=msg):
1955+
rng - one
19681956

1969-
def test_dti_isub_int(self, tz_naive_fixture, one):
1970-
tz = tz_naive_fixture
1971-
rng = pd.date_range("2000-01-01 09:00", freq="H", periods=10, tz=tz)
1972-
expected = pd.date_range("2000-01-01 08:00", freq="H", periods=10, tz=tz)
1973-
with tm.assert_produces_warning(FutureWarning, check_stacklevel=False):
1957+
with pytest.raises(TypeError, match=msg):
19741958
rng -= one
1975-
tm.assert_index_equal(rng, expected)
19761959

19771960
# -------------------------------------------------------------
19781961
# __add__/__sub__ with integer arrays
@@ -1984,14 +1967,13 @@ def test_dti_add_intarray_tick(self, int_holder, freq):
19841967
dti = pd.date_range("2016-01-01", periods=2, freq=freq)
19851968
other = int_holder([4, -1])
19861969

1987-
with tm.assert_produces_warning(FutureWarning, check_stacklevel=False):
1988-
expected = DatetimeIndex([dti[n] + other[n] for n in range(len(dti))])
1989-
result = dti + other
1990-
tm.assert_index_equal(result, expected)
1970+
msg = "Addition/subtraction of integers"
19911971

1992-
with tm.assert_produces_warning(FutureWarning, check_stacklevel=False):
1993-
result = other + dti
1994-
tm.assert_index_equal(result, expected)
1972+
with pytest.raises(TypeError, match=msg):
1973+
dti + other
1974+
1975+
with pytest.raises(TypeError, match=msg):
1976+
other + dti
19951977

19961978
@pytest.mark.parametrize("freq", ["W", "M", "MS", "Q"])
19971979
@pytest.mark.parametrize("int_holder", [np.array, pd.Index])
@@ -2000,34 +1982,26 @@ def test_dti_add_intarray_non_tick(self, int_holder, freq):
20001982
dti = pd.date_range("2016-01-01", periods=2, freq=freq)
20011983
other = int_holder([4, -1])
20021984

2003-
with tm.assert_produces_warning(FutureWarning, check_stacklevel=False):
2004-
expected = DatetimeIndex([dti[n] + other[n] for n in range(len(dti))])
1985+
msg = "Addition/subtraction of integers"
20051986

2006-
# tm.assert_produces_warning does not handle cases where we expect
2007-
# two warnings, in this case PerformanceWarning and FutureWarning.
2008-
# Until that is fixed, we don't catch either
2009-
with warnings.catch_warnings():
2010-
warnings.simplefilter("ignore")
2011-
result = dti + other
2012-
tm.assert_index_equal(result, expected)
1987+
with pytest.raises(TypeError, match=msg):
1988+
dti + other
20131989

2014-
with warnings.catch_warnings():
2015-
warnings.simplefilter("ignore")
2016-
result = other + dti
2017-
tm.assert_index_equal(result, expected)
1990+
with pytest.raises(TypeError, match=msg):
1991+
other + dti
20181992

20191993
@pytest.mark.parametrize("int_holder", [np.array, pd.Index])
20201994
def test_dti_add_intarray_no_freq(self, int_holder):
20211995
# GH#19959
20221996
dti = pd.DatetimeIndex(["2016-01-01", "NaT", "2017-04-05 06:07:08"])
20231997
other = int_holder([9, 4, -1])
2024-
nfmsg = "Cannot shift with no freq"
20251998
tmsg = "cannot subtract DatetimeArray from"
2026-
with pytest.raises(NullFrequencyError, match=nfmsg):
1999+
msg = "Addition/subtraction of integers"
2000+
with pytest.raises(TypeError, match=msg):
20272001
dti + other
2028-
with pytest.raises(NullFrequencyError, match=nfmsg):
2002+
with pytest.raises(TypeError, match=msg):
20292003
other + dti
2030-
with pytest.raises(NullFrequencyError, match=nfmsg):
2004+
with pytest.raises(TypeError, match=msg):
20312005
dti - other
20322006
with pytest.raises(TypeError, match=tmsg):
20332007
other - dti

pandas/tests/arithmetic/test_timedelta64.py

+10-17
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import numpy as np
66
import pytest
77

8-
from pandas.errors import NullFrequencyError, OutOfBoundsDatetime, PerformanceWarning
8+
from pandas.errors import OutOfBoundsDatetime, PerformanceWarning
99

1010
import pandas as pd
1111
from pandas import (
@@ -409,7 +409,7 @@ def test_addition_ops(self):
409409
tdi[0:1] + dti
410410

411411
# random indexes
412-
with pytest.raises(NullFrequencyError):
412+
with pytest.raises(TypeError):
413413
tdi + pd.Int64Index([1, 2, 3])
414414

415415
# this is a union!
@@ -997,17 +997,13 @@ def test_td64arr_addsub_numeric_invalid(self, box_with_array, other):
997997
tdser = pd.Series(["59 Days", "59 Days", "NaT"], dtype="m8[ns]")
998998
tdser = tm.box_expected(tdser, box)
999999

1000-
err = TypeError
1001-
if box in [pd.Index, tm.to_array] and not isinstance(other, float):
1002-
err = NullFrequencyError
1003-
1004-
with pytest.raises(err):
1000+
with pytest.raises(TypeError):
10051001
tdser + other
1006-
with pytest.raises(err):
1002+
with pytest.raises(TypeError):
10071003
other + tdser
1008-
with pytest.raises(err):
1004+
with pytest.raises(TypeError):
10091005
tdser - other
1010-
with pytest.raises(err):
1006+
with pytest.raises(TypeError):
10111007
other - tdser
10121008

10131009
@pytest.mark.parametrize(
@@ -1039,18 +1035,15 @@ def test_td64arr_add_sub_numeric_arr_invalid(self, box_with_array, vec, dtype):
10391035
box = box_with_array
10401036
tdser = pd.Series(["59 Days", "59 Days", "NaT"], dtype="m8[ns]")
10411037
tdser = tm.box_expected(tdser, box)
1042-
err = TypeError
1043-
if box in [pd.Index, tm.to_array] and not dtype.startswith("float"):
1044-
err = NullFrequencyError
10451038

10461039
vector = vec.astype(dtype)
1047-
with pytest.raises(err):
1040+
with pytest.raises(TypeError):
10481041
tdser + vector
1049-
with pytest.raises(err):
1042+
with pytest.raises(TypeError):
10501043
vector + tdser
1051-
with pytest.raises(err):
1044+
with pytest.raises(TypeError):
10521045
tdser - vector
1053-
with pytest.raises(err):
1046+
with pytest.raises(TypeError):
10541047
vector - tdser
10551048

10561049
# ------------------------------------------------------------------

pandas/tests/indexes/datetimes/test_arithmetic.py

+2-8
Original file line numberDiff line numberDiff line change
@@ -69,17 +69,11 @@ def test_dti_shift_freqs(self):
6969
def test_dti_shift_int(self):
7070
rng = date_range("1/1/2000", periods=20)
7171

72-
with tm.assert_produces_warning(FutureWarning, check_stacklevel=False):
73-
# GH#22535
74-
result = rng + 5
75-
72+
result = rng + 5 * rng.freq
7673
expected = rng.shift(5)
7774
tm.assert_index_equal(result, expected)
7875

79-
with tm.assert_produces_warning(FutureWarning, check_stacklevel=False):
80-
# GH#22535
81-
result = rng - 5
82-
76+
result = rng - 5 * rng.freq
8377
expected = rng.shift(-5)
8478
tm.assert_index_equal(result, expected)
8579

0 commit comments

Comments
 (0)