Skip to content

Commit 5c57e7b

Browse files
jbrockmendeljreback
authored andcommitted
BUG: Timestamp+int should raise NullFrequencyError, not ValueError (#28268)
1 parent f04c4db commit 5c57e7b

File tree

7 files changed

+32
-30
lines changed

7 files changed

+32
-30
lines changed

doc/source/whatsnew/v1.0.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ Datetimelike
9999
- Bug in ``HDFStore.__getitem__`` incorrectly reading tz attribute created in Python 2 (:issue:`26443`)
100100
- Bug in :meth:`pandas.core.groupby.SeriesGroupBy.nunique` where ``NaT`` values were interfering with the count of unique values (:issue:`27951`)
101101
- Bug in :class:`Timestamp` subtraction when subtracting a :class:`Timestamp` from a ``np.datetime64`` object incorrectly raising ``TypeError`` (:issue:`28286`)
102+
- Addition and subtraction of integer or integer-dtype arrays with :class:`Timestamp` will now raise ``NullFrequencyError`` instead of ``ValueError`` (:issue:`28268`)
102103
-
103104

104105

pandas/_libs/tslibs/__init__.py

+3
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,6 @@
77
from .timedeltas import Timedelta, delta_to_nanoseconds, ints_to_pytimedelta
88
from .timestamps import Timestamp
99
from .tzconversion import tz_convert_single
10+
11+
# import fails if we do this before np_datetime
12+
from .c_timestamp import NullFrequencyError # isort:skip

pandas/_libs/tslibs/c_timestamp.pyx

+20-14
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,15 @@ from pandas._libs.tslibs.timezones import UTC
4242
from pandas._libs.tslibs.tzconversion cimport tz_convert_single
4343

4444

45+
class NullFrequencyError(ValueError):
46+
"""
47+
Error raised when a null `freq` attribute is used in an operation
48+
that needs a non-null frequency, particularly `DatetimeIndex.shift`,
49+
`TimedeltaIndex.shift`, `PeriodIndex.shift`.
50+
"""
51+
pass
52+
53+
4554
def maybe_integer_op_deprecated(obj):
4655
# GH#22535 add/sub of integers and int-arrays is deprecated
4756
if obj.freq is not None:
@@ -227,8 +236,8 @@ cdef class _Timestamp(datetime):
227236
# to be compat with Period
228237
return NaT
229238
elif self.freq is None:
230-
raise ValueError("Cannot add integral value to Timestamp "
231-
"without freq.")
239+
raise NullFrequencyError(
240+
"Cannot add integral value to Timestamp without freq.")
232241
return self.__class__((self.freq * other).apply(self),
233242
freq=self.freq)
234243

@@ -246,17 +255,15 @@ cdef class _Timestamp(datetime):
246255

247256
result = self.__class__(self.value + nanos,
248257
tz=self.tzinfo, freq=self.freq)
249-
if getattr(other, 'normalize', False):
250-
# DateOffset
251-
result = result.normalize()
252258
return result
253259

254260
elif is_array(other):
255261
if other.dtype.kind in ['i', 'u']:
256262
maybe_integer_op_deprecated(self)
257263
if self.freq is None:
258-
raise ValueError("Cannot add integer-dtype array "
259-
"to Timestamp without freq.")
264+
raise NullFrequencyError(
265+
"Cannot add integer-dtype array "
266+
"to Timestamp without freq.")
260267
return self.freq * other + self
261268

262269
# index/series like
@@ -270,6 +277,7 @@ cdef class _Timestamp(datetime):
270277
return result
271278

272279
def __sub__(self, other):
280+
273281
if (is_timedelta64_object(other) or is_integer_object(other) or
274282
PyDelta_Check(other) or hasattr(other, 'delta')):
275283
# `delta` attribute is for offsets.Tick or offsets.Week obj
@@ -280,15 +288,16 @@ cdef class _Timestamp(datetime):
280288
if other.dtype.kind in ['i', 'u']:
281289
maybe_integer_op_deprecated(self)
282290
if self.freq is None:
283-
raise ValueError("Cannot subtract integer-dtype array "
284-
"from Timestamp without freq.")
291+
raise NullFrequencyError(
292+
"Cannot subtract integer-dtype array "
293+
"from Timestamp without freq.")
285294
return self - self.freq * other
286295

287296
typ = getattr(other, '_typ', None)
288297
if typ is not None:
289298
return NotImplemented
290299

291-
elif other is NaT:
300+
if other is NaT:
292301
return NaT
293302

294303
# coerce if necessary if we are a Timestamp-like
@@ -311,15 +320,12 @@ cdef class _Timestamp(datetime):
311320
return Timedelta(self.value - other.value)
312321
except (OverflowError, OutOfBoundsDatetime):
313322
pass
314-
315323
elif is_datetime64_object(self):
316324
# GH#28286 cython semantics for __rsub__, `other` is actually
317325
# the Timestamp
318326
return type(other)(self) - other
319327

320-
# scalar Timestamp/datetime - Timedelta -> yields a Timestamp (with
321-
# same timezone if specified)
322-
return datetime.__sub__(self, other)
328+
return NotImplemented
323329

324330
cdef int64_t _maybe_convert_value_to_local(self):
325331
"""Convert UTC i8 value to local i8 value if tz exists"""

pandas/errors/__init__.py

+1-9
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
Expose public exceptions & warnings
55
"""
66

7-
from pandas._libs.tslibs import OutOfBoundsDatetime
7+
from pandas._libs.tslibs import NullFrequencyError, OutOfBoundsDatetime
88

99

1010
class PerformanceWarning(Warning):
@@ -157,14 +157,6 @@ class MergeError(ValueError):
157157
"""
158158

159159

160-
class NullFrequencyError(ValueError):
161-
"""
162-
Error raised when a null `freq` attribute is used in an operation
163-
that needs a non-null frequency, particularly `DatetimeIndex.shift`,
164-
`TimedeltaIndex.shift`, `PeriodIndex.shift`.
165-
"""
166-
167-
168160
class AccessorRegistrationWarning(Warning):
169161
"""Warning for attribute conflicts in accessor registration."""
170162

pandas/tests/arithmetic/test_timedelta64.py

+1-4
Original file line numberDiff line numberDiff line change
@@ -241,10 +241,7 @@ def test_subtraction_ops(self):
241241
with pytest.raises(TypeError, match=msg):
242242
tdi - dti
243243

244-
msg = (
245-
r"descriptor '__sub__' requires a 'datetime\.datetime' object"
246-
" but received a 'Timedelta'"
247-
)
244+
msg = r"unsupported operand type\(s\) for -"
248245
with pytest.raises(TypeError, match=msg):
249246
td - dt
250247

pandas/tests/scalar/timestamp/test_arithmetic.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import numpy as np
44
import pytest
55

6+
from pandas.errors import NullFrequencyError
7+
68
from pandas import Timedelta, Timestamp
79
import pandas.util.testing as tm
810

@@ -177,12 +179,12 @@ def test_timestamp_add_timedelta64_unit(self, other, expected_difference):
177179
],
178180
)
179181
def test_add_int_no_freq_raises(self, ts, other):
180-
with pytest.raises(ValueError, match="without freq"):
182+
with pytest.raises(NullFrequencyError, match="without freq"):
181183
ts + other
182-
with pytest.raises(ValueError, match="without freq"):
184+
with pytest.raises(NullFrequencyError, match="without freq"):
183185
other + ts
184186

185-
with pytest.raises(ValueError, match="without freq"):
187+
with pytest.raises(NullFrequencyError, match="without freq"):
186188
ts - other
187189
with pytest.raises(TypeError):
188190
other - ts

pandas/tests/tslibs/test_api.py

+1
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ def test_namespace():
2929
"NaTType",
3030
"iNaT",
3131
"is_null_datetimelike",
32+
"NullFrequencyError",
3233
"OutOfBoundsDatetime",
3334
"Period",
3435
"IncompatibleFrequency",

0 commit comments

Comments
 (0)