Skip to content

Commit

Permalink
DEPR: integer add/sub for Timestamp, DatetimeIndex, TimedeltaIndex (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
jbrockmendel authored and jreback committed Dec 6, 2019
1 parent 282a0e4 commit 51bd049
Show file tree
Hide file tree
Showing 8 changed files with 102 additions and 193 deletions.
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

0 comments on commit 51bd049

Please sign in to comment.