diff --git a/pandas/core/frame.py b/pandas/core/frame.py index a58d34574d28d..f3fd924ee7e6e 100644 --- a/pandas/core/frame.py +++ b/pandas/core/frame.py @@ -4944,7 +4944,7 @@ def _combine_match_columns(self, other, func, level=None): assert left.columns.equals(right.index) return ops.dispatch_to_series(left, right, func, axis="columns") - def _combine_const(self, other, func, errors='raise'): + def _combine_const(self, other, func): assert lib.is_scalar(other) or np.ndim(other) == 0 return ops.dispatch_to_series(self, other, func) diff --git a/pandas/core/indexes/base.py b/pandas/core/indexes/base.py index a9edad1fa2e01..1ffdac1989129 100644 --- a/pandas/core/indexes/base.py +++ b/pandas/core/indexes/base.py @@ -4702,6 +4702,13 @@ def dropna(self, how='any'): def _evaluate_with_timedelta_like(self, other, op): # Timedelta knows how to operate with np.array, so dispatch to that # operation and then wrap the results + if self._is_numeric_dtype and op.__name__ in ['add', 'sub', + 'radd', 'rsub']: + raise TypeError("Operation {opname} between {cls} and {other} " + "is invalid".format(opname=op.__name__, + cls=type(self).__name__, + other=type(other).__name__)) + other = Timedelta(other) values = self.values diff --git a/pandas/core/indexes/range.py b/pandas/core/indexes/range.py index cce5fda7dba28..673ab9f2118a4 100644 --- a/pandas/core/indexes/range.py +++ b/pandas/core/indexes/range.py @@ -14,7 +14,8 @@ from pandas.core.dtypes import concat as _concat from pandas.core.dtypes.common import ( is_int64_dtype, is_integer, is_scalar, is_timedelta64_dtype) -from pandas.core.dtypes.generic import ABCSeries, ABCTimedeltaIndex +from pandas.core.dtypes.generic import ( + ABCDataFrame, ABCSeries, ABCTimedeltaIndex) from pandas.core import ops import pandas.core.common as com @@ -558,6 +559,9 @@ def __getitem__(self, key): return super_getitem(key) def __floordiv__(self, other): + if isinstance(other, (ABCSeries, ABCDataFrame)): + return NotImplemented + if is_integer(other) and other != 0: if (len(self) == 0 or self._start % other == 0 and @@ -589,7 +593,7 @@ def _make_evaluate_binop(op, step=False): """ def _evaluate_numeric_binop(self, other): - if isinstance(other, ABCSeries): + if isinstance(other, (ABCSeries, ABCDataFrame)): return NotImplemented elif isinstance(other, ABCTimedeltaIndex): # Defer to TimedeltaIndex implementation diff --git a/pandas/core/ops.py b/pandas/core/ops.py index 2335b26c576eb..fbfdfb9c01237 100644 --- a/pandas/core/ops.py +++ b/pandas/core/ops.py @@ -130,6 +130,13 @@ def maybe_upcast_for_op(obj): # implementation; otherwise operation against numeric-dtype # raises TypeError return pd.Timedelta(obj) + elif isinstance(obj, np.timedelta64) and not isna(obj): + # In particular non-nanosecond timedelta64 needs to be cast to + # nanoseconds, or else we get undesired behavior like + # np.timedelta64(3, 'D') / 2 == np.timedelta64(1, 'D') + # The isna check is to avoid casting timedelta64("NaT"), which would + # return NaT and incorrectly be treated as a datetime-NaT. + return pd.Timedelta(obj) elif isinstance(obj, np.ndarray) and is_timedelta64_dtype(obj): # GH#22390 Unfortunately we need to special-case right-hand # timedelta64 dtypes because numpy casts integer dtypes to @@ -1405,11 +1412,12 @@ def wrapper(left, right): index=left.index, name=res_name, dtype=result.dtype) - elif is_timedelta64_dtype(right) and not is_scalar(right): - # i.e. exclude np.timedelta64 object + elif is_timedelta64_dtype(right): + # We should only get here with non-scalar or timedelta64('NaT') + # values for right # Note: we cannot use dispatch_to_index_op because - # that may incorrectly raise TypeError when we - # should get NullFrequencyError + # that may incorrectly raise TypeError when we + # should get NullFrequencyError result = op(pd.Index(left), right) return construct_result(left, result, index=left.index, name=res_name, @@ -1941,8 +1949,7 @@ def f(self, other): # straight boolean comparisons we want to allow all columns # (regardless of dtype to pass thru) See #4537 for discussion. - res = self._combine_const(other, func, - errors='ignore') + res = self._combine_const(other, func) return res.fillna(True).astype(bool) f.__name__ = op_name diff --git a/pandas/core/sparse/frame.py b/pandas/core/sparse/frame.py index c7d8be0d2e9e4..ee7de49bc1bce 100644 --- a/pandas/core/sparse/frame.py +++ b/pandas/core/sparse/frame.py @@ -620,7 +620,7 @@ def _combine_match_columns(self, other, func, level=None): new_data, index=left.index, columns=left.columns, default_fill_value=self.default_fill_value).__finalize__(self) - def _combine_const(self, other, func, errors='raise'): + def _combine_const(self, other, func): return self._apply_columns(lambda x: func(x, other)) def _get_op_result_fill_value(self, other, func): diff --git a/pandas/tests/arithmetic/conftest.py b/pandas/tests/arithmetic/conftest.py index b800b66e8edea..cbe26a06d34c6 100644 --- a/pandas/tests/arithmetic/conftest.py +++ b/pandas/tests/arithmetic/conftest.py @@ -70,7 +70,8 @@ def scalar_td(request): pd.Timedelta(days=3).to_pytimedelta(), pd.Timedelta('72:00:00'), np.timedelta64(3, 'D'), - np.timedelta64(72, 'h')]) + np.timedelta64(72, 'h')], + ids=lambda x: type(x).__name__) def three_days(request): """ Several timedelta-like and DateOffset objects that each represent @@ -84,7 +85,8 @@ def three_days(request): pd.Timedelta(hours=2).to_pytimedelta(), pd.Timedelta(seconds=2 * 3600), np.timedelta64(2, 'h'), - np.timedelta64(120, 'm')]) + np.timedelta64(120, 'm')], + ids=lambda x: type(x).__name__) def two_hours(request): """ Several timedelta-like and DateOffset objects that each represent diff --git a/pandas/tests/arithmetic/test_numeric.py b/pandas/tests/arithmetic/test_numeric.py index 25845dd8b3151..9163f2e1a3d1c 100644 --- a/pandas/tests/arithmetic/test_numeric.py +++ b/pandas/tests/arithmetic/test_numeric.py @@ -148,15 +148,11 @@ def test_numeric_arr_mul_tdscalar(self, scalar_td, numeric_idx, box): tm.assert_equal(commute, expected) def test_numeric_arr_rdiv_tdscalar(self, three_days, numeric_idx, box): - index = numeric_idx[1:3] - broken = (isinstance(three_days, np.timedelta64) and - three_days.dtype != 'm8[ns]') - broken = broken or isinstance(three_days, pd.offsets.Tick) - if box is not pd.Index and broken: - # np.timedelta64(3, 'D') / 2 == np.timedelta64(1, 'D') - raise pytest.xfail("timedelta64 not converted to nanos; " - "Tick division not implemented") + if box is not pd.Index and isinstance(three_days, pd.offsets.Tick): + raise pytest.xfail("Tick division not implemented") + + index = numeric_idx[1:3] expected = TimedeltaIndex(['3 Days', '36 Hours']) @@ -169,6 +165,26 @@ def test_numeric_arr_rdiv_tdscalar(self, three_days, numeric_idx, box): with pytest.raises(TypeError): index / three_days + @pytest.mark.parametrize('other', [ + pd.Timedelta(hours=31), + pd.Timedelta(hours=31).to_pytimedelta(), + pd.Timedelta(hours=31).to_timedelta64(), + pd.Timedelta(hours=31).to_timedelta64().astype('m8[h]'), + np.timedelta64('NaT'), + np.timedelta64('NaT', 'D'), + pd.offsets.Minute(3), + pd.offsets.Second(0)]) + def test_add_sub_timedeltalike_invalid(self, numeric_idx, other, box): + left = tm.box_expected(numeric_idx, box) + with pytest.raises(TypeError): + left + other + with pytest.raises(TypeError): + other + left + with pytest.raises(TypeError): + left - other + with pytest.raises(TypeError): + other - left + # ------------------------------------------------------------------ # Arithmetic diff --git a/pandas/tests/arithmetic/test_timedelta64.py b/pandas/tests/arithmetic/test_timedelta64.py index 9930297fd1a3c..d1ea51a46889f 100644 --- a/pandas/tests/arithmetic/test_timedelta64.py +++ b/pandas/tests/arithmetic/test_timedelta64.py @@ -1051,10 +1051,8 @@ def test_tdi_mul_float_series(self, box_df_fail): pd.Float64Index(range(1, 11)), pd.RangeIndex(1, 11) ], ids=lambda x: type(x).__name__) - def test_tdi_rmul_arraylike(self, other, box_df_fail): - # RangeIndex fails to return NotImplemented, for others - # DataFrame tries to broadcast incorrectly - box = box_df_fail + def test_tdi_rmul_arraylike(self, other, box_df_broadcast_failure): + box = box_df_broadcast_failure tdi = TimedeltaIndex(['1 Day'] * 10) expected = timedelta_range('1 days', '10 days') diff --git a/pandas/tests/indexes/test_range.py b/pandas/tests/indexes/test_range.py index ecda48822eb0f..efea9b58ecb7a 100644 --- a/pandas/tests/indexes/test_range.py +++ b/pandas/tests/indexes/test_range.py @@ -185,6 +185,25 @@ def test_constructor_name(self): assert copy.name == 'copy' assert new.name == 'new' + # TODO: mod, divmod? + @pytest.mark.parametrize('op', [operator.add, operator.sub, + operator.mul, operator.floordiv, + operator.truediv, operator.pow]) + def test_arithmetic_with_frame_or_series(self, op): + # check that we return NotImplemented when operating with Series + # or DataFrame + index = pd.RangeIndex(5) + other = pd.Series(np.random.randn(5)) + + expected = op(pd.Series(index), other) + result = op(index, other) + tm.assert_series_equal(result, expected) + + other = pd.DataFrame(np.random.randn(2, 5)) + expected = op(pd.DataFrame([index, index]), other) + result = op(index, other) + tm.assert_frame_equal(result, expected) + def test_numeric_compat2(self): # validate that we are handling the RangeIndex overrides to numeric ops # and returning RangeIndex where possible diff --git a/pandas/tests/internals/test_internals.py b/pandas/tests/internals/test_internals.py index cdf35ea96588a..b327b158adc24 100644 --- a/pandas/tests/internals/test_internals.py +++ b/pandas/tests/internals/test_internals.py @@ -1243,7 +1243,6 @@ def test_binop_other(self, op, value, dtype): (operator.mul, '