Skip to content

Commit 3db6f66

Browse files
jbrockmendeljreback
authored andcommitted
Fix incorrect exception raised by Series[datetime64] + int (#19147)
1 parent 6a6bb40 commit 3db6f66

File tree

8 files changed

+109
-6
lines changed

8 files changed

+109
-6
lines changed

doc/source/whatsnew/v0.23.0.txt

+1
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,7 @@ Other API Changes
309309
- ``IntervalDtype`` now returns ``True`` when compared against ``'interval'`` regardless of subtype, and ``IntervalDtype.name`` now returns ``'interval'`` regardless of subtype (:issue:`18980`)
310310
- :func:`Series.to_csv` now accepts a ``compression`` argument that works in the same way as the ``compression`` argument in :func:`DataFrame.to_csv` (:issue:`18958`)
311311
- Addition or subtraction of ``NaT`` from :class:`TimedeltaIndex` will return ``TimedeltaIndex`` instead of ``DatetimeIndex`` (:issue:`19124`)
312+
- :func:`DatetimeIndex.shift` and :func:`TimedeltaIndex.shift` will now raise ``NullFrequencyError`` (which subclasses ``ValueError``, which was raised in older versions) when the index object frequency is ``None`` (:issue:`19147`)
312313

313314
.. _whatsnew_0230.deprecations:
314315

pandas/core/indexes/datetimelike.py

+8-2
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
from pandas.core import common as com, algorithms
3333
from pandas.core.algorithms import checked_add_with_arr
3434
from pandas.core.common import AbstractMethodError
35+
from pandas.errors import NullFrequencyError
3536

3637
import pandas.io.formats.printing as printing
3738
from pandas._libs import lib, iNaT, NaT
@@ -692,6 +693,9 @@ def __add__(self, other):
692693
return self._add_datelike(other)
693694
elif isinstance(other, Index):
694695
return self._add_datelike(other)
696+
elif is_integer_dtype(other) and self.freq is None:
697+
# GH#19123
698+
raise NullFrequencyError("Cannot shift with no freq")
695699
else: # pragma: no cover
696700
return NotImplemented
697701

@@ -731,7 +735,9 @@ def __sub__(self, other):
731735
raise TypeError("cannot subtract {typ1} and {typ2}"
732736
.format(typ1=type(self).__name__,
733737
typ2=type(other).__name__))
734-
738+
elif is_integer_dtype(other) and self.freq is None:
739+
# GH#19123
740+
raise NullFrequencyError("Cannot shift with no freq")
735741
else: # pragma: no cover
736742
return NotImplemented
737743

@@ -831,7 +837,7 @@ def shift(self, n, freq=None):
831837
return self
832838

833839
if self.freq is None:
834-
raise ValueError("Cannot shift with no freq")
840+
raise NullFrequencyError("Cannot shift with no freq")
835841

836842
start = self[0] + n * self.freq
837843
end = self[-1] + n * self.freq

pandas/core/ops.py

+36-3
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
from pandas.compat import bind_method
2121
import pandas.core.missing as missing
2222

23-
from pandas.errors import PerformanceWarning
23+
from pandas.errors import PerformanceWarning, NullFrequencyError
2424
from pandas.core.common import _values_from_object, _maybe_match_name
2525
from pandas.core.dtypes.missing import notna, isna
2626
from pandas.core.dtypes.common import (
@@ -672,9 +672,8 @@ def wrapper(left, right, name=name, na_op=na_op):
672672

673673
left, right = _align_method_SERIES(left, right)
674674
if is_datetime64_dtype(left) or is_datetime64tz_dtype(left):
675-
result = op(pd.DatetimeIndex(left), right)
675+
result = dispatch_to_index_op(op, left, right, pd.DatetimeIndex)
676676
res_name = _get_series_op_result_name(left, right)
677-
result.name = res_name # needs to be overriden if None
678677
return construct_result(left, result,
679678
index=left.index, name=res_name,
680679
dtype=result.dtype)
@@ -703,6 +702,40 @@ def wrapper(left, right, name=name, na_op=na_op):
703702
return wrapper
704703

705704

705+
def dispatch_to_index_op(op, left, right, index_class):
706+
"""
707+
Wrap Series left in the given index_class to delegate the operation op
708+
to the index implementation. DatetimeIndex and TimedeltaIndex perform
709+
type checking, timezone handling, overflow checks, etc.
710+
711+
Parameters
712+
----------
713+
op : binary operator (operator.add, operator.sub, ...)
714+
left : Series
715+
right : object
716+
index_class : DatetimeIndex or TimedeltaIndex
717+
718+
Returns
719+
-------
720+
result : object, usually DatetimeIndex, TimedeltaIndex, or Series
721+
"""
722+
left_idx = index_class(left)
723+
724+
# avoid accidentally allowing integer add/sub. For datetime64[tz] dtypes,
725+
# left_idx may inherit a freq from a cached DatetimeIndex.
726+
# See discussion in GH#19147.
727+
if left_idx.freq is not None:
728+
left_idx = left_idx._shallow_copy(freq=None)
729+
try:
730+
result = op(left_idx, right)
731+
except NullFrequencyError:
732+
# DatetimeIndex and TimedeltaIndex with freq == None raise ValueError
733+
# on add/sub of integers (or int-like). We re-raise as a TypeError.
734+
raise TypeError('incompatible type for a datetime/timedelta '
735+
'operation [{name}]'.format(name=op.__name__))
736+
return result
737+
738+
706739
def _get_series_op_result_name(left, right):
707740
# `left` is always a pd.Series
708741
if isinstance(right, (ABCSeries, pd.Index)):

pandas/errors/__init__.py

+8
Original file line numberDiff line numberDiff line change
@@ -67,5 +67,13 @@ class MergeError(ValueError):
6767
"""
6868

6969

70+
class NullFrequencyError(ValueError):
71+
"""
72+
Error raised when a null `freq` attribute is used in an operation
73+
that needs a non-null frequency, particularly `DatetimeIndex.shift`,
74+
`TimedeltaIndex.shift`, `PeriodIndex.shift`.
75+
"""
76+
77+
7078
class AccessorRegistrationWarning(Warning):
7179
"""Warning for attribute conflicts in accessor registration."""

pandas/tests/indexes/datetimes/test_ops.py

+7
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from itertools import product
99
import pandas as pd
10+
from pandas.errors import NullFrequencyError
1011
import pandas._libs.tslib as tslib
1112
from pandas._libs.tslibs.offsets import shift_months
1213
import pandas.util.testing as tm
@@ -593,6 +594,12 @@ def test_nat_new(self):
593594
exp = np.array([tslib.iNaT] * 5, dtype=np.int64)
594595
tm.assert_numpy_array_equal(result, exp)
595596

597+
def test_shift_no_freq(self):
598+
# GH#19147
599+
dti = pd.DatetimeIndex(['2011-01-01 10:00', '2011-01-01'], freq=None)
600+
with pytest.raises(NullFrequencyError):
601+
dti.shift(2)
602+
596603
def test_shift(self):
597604
# GH 9903
598605
for tz in self.tz:

pandas/tests/indexes/timedeltas/test_timedelta.py

+7
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from datetime import timedelta
55

66
import pandas as pd
7+
from pandas.errors import NullFrequencyError
78
import pandas.util.testing as tm
89
from pandas import (timedelta_range, date_range, Series, Timedelta,
910
TimedeltaIndex, Index, DataFrame,
@@ -50,6 +51,12 @@ def test_shift(self):
5051
'10 days 01:00:03'], freq='D')
5152
tm.assert_index_equal(result, expected)
5253

54+
def test_shift_no_freq(self):
55+
# GH#19147
56+
tdi = TimedeltaIndex(['1 days 01:00:00', '2 days 01:00:00'], freq=None)
57+
with pytest.raises(NullFrequencyError):
58+
tdi.shift(2)
59+
5360
def test_pickle_compat_construction(self):
5461
pass
5562

pandas/tests/series/test_operators.py

+39
Original file line numberDiff line numberDiff line change
@@ -693,6 +693,25 @@ def test_td64series_rsub_int_series_invalid(self, tdser):
693693
with pytest.raises(TypeError):
694694
Series([2, 3, 4]) - tdser
695695

696+
def test_td64_series_add_intlike(self):
697+
# GH#19123
698+
tdi = pd.TimedeltaIndex(['59 days', '59 days', 'NaT'])
699+
ser = Series(tdi)
700+
701+
other = Series([20, 30, 40], dtype='uint8')
702+
703+
pytest.raises(TypeError, ser.__add__, 1)
704+
pytest.raises(TypeError, ser.__sub__, 1)
705+
706+
pytest.raises(TypeError, ser.__add__, other)
707+
pytest.raises(TypeError, ser.__sub__, other)
708+
709+
pytest.raises(TypeError, ser.__add__, other.values)
710+
pytest.raises(TypeError, ser.__sub__, other.values)
711+
712+
pytest.raises(TypeError, ser.__add__, pd.Index(other))
713+
pytest.raises(TypeError, ser.__sub__, pd.Index(other))
714+
696715
@pytest.mark.parametrize('scalar', [1, 1.5, np.array(2)])
697716
def test_td64series_add_sub_numeric_scalar_invalid(self, scalar, tdser):
698717
with pytest.raises(TypeError):
@@ -1533,6 +1552,26 @@ def test_dt64_series_arith_overflow(self):
15331552
res = dt - ser
15341553
tm.assert_series_equal(res, -expected)
15351554

1555+
@pytest.mark.parametrize('tz', [None, 'Asia/Tokyo'])
1556+
def test_dt64_series_add_intlike(self, tz):
1557+
# GH#19123
1558+
dti = pd.DatetimeIndex(['2016-01-02', '2016-02-03', 'NaT'], tz=tz)
1559+
ser = Series(dti)
1560+
1561+
other = Series([20, 30, 40], dtype='uint8')
1562+
1563+
pytest.raises(TypeError, ser.__add__, 1)
1564+
pytest.raises(TypeError, ser.__sub__, 1)
1565+
1566+
pytest.raises(TypeError, ser.__add__, other)
1567+
pytest.raises(TypeError, ser.__sub__, other)
1568+
1569+
pytest.raises(TypeError, ser.__add__, other.values)
1570+
pytest.raises(TypeError, ser.__sub__, other.values)
1571+
1572+
pytest.raises(TypeError, ser.__add__, pd.Index(other))
1573+
pytest.raises(TypeError, ser.__sub__, pd.Index(other))
1574+
15361575

15371576
class TestSeriesOperators(TestData):
15381577
def test_op_method(self):

pandas/tests/series/test_timeseries.py

+3-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
import pandas.util._test_decorators as td
1212
from pandas._libs.tslib import iNaT
1313
from pandas.compat import lrange, StringIO, product
14+
from pandas.errors import NullFrequencyError
15+
1416
from pandas.core.indexes.timedeltas import TimedeltaIndex
1517
from pandas.core.indexes.datetimes import DatetimeIndex
1618
from pandas.tseries.offsets import BDay, BMonthEnd
@@ -123,7 +125,7 @@ def test_shift2(self):
123125
tm.assert_index_equal(result.index, exp_index)
124126

125127
idx = DatetimeIndex(['2000-01-01', '2000-01-02', '2000-01-04'])
126-
pytest.raises(ValueError, idx.shift, 1)
128+
pytest.raises(NullFrequencyError, idx.shift, 1)
127129

128130
def test_shift_dst(self):
129131
# GH 13926

0 commit comments

Comments
 (0)