Skip to content

DEPR: integer add/sub for Timestamp, DatetimeIndex, TimedeltaIndex #30117

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Dec 6, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions doc/source/whatsnew/v1.0.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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`)
Expand Down
45 changes: 15 additions & 30 deletions pandas/_libs/tslibs/c_timestamp.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand All @@ -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'):
Expand All @@ -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:
Expand Down
10 changes: 5 additions & 5 deletions pandas/core/arrays/datetimelike.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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):
Expand All @@ -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
Expand Down
80 changes: 27 additions & 53 deletions pandas/tests/arithmetic/test_datetime64.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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
Expand All @@ -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])
Expand All @@ -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
Expand Down
27 changes: 10 additions & 17 deletions pandas/tests/arithmetic/test_timedelta64.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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!
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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

# ------------------------------------------------------------------
Expand Down
10 changes: 2 additions & 8 deletions pandas/tests/indexes/datetimes/test_arithmetic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
Loading