From 49aa38451a3080f97372fb634d3b616343dc9880 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Mon, 18 Dec 2017 12:17:12 -0800 Subject: [PATCH 1/7] refactor ops to isolate datetime and timedelta logic --- pandas/core/ops.py | 277 ++++++++++++++++---------- pandas/tests/series/test_operators.py | 39 +++- 2 files changed, 199 insertions(+), 117 deletions(-) diff --git a/pandas/core/ops.py b/pandas/core/ops.py index 2fb0cbb14c225..f5ea7e5e1e58e 100644 --- a/pandas/core/ops.py +++ b/pandas/core/ops.py @@ -386,117 +386,171 @@ def __init__(self, left, right, name, na_op): self.lvalues, self.rvalues = self._convert_for_datetime(lvalues, rvalues) - def _validate(self, lvalues, rvalues, name): - # timedelta and integer mul/div + def _validate_datetime(self, lvalues, rvalues, name): + # assumes self.is_datetime_lhs + + if (self.is_timedelta_rhs or self.is_offset_rhs): + # datetime and timedelta/DateOffset + if name not in ('__add__', '__radd__', '__sub__'): + raise TypeError("can only operate on a datetime with a rhs of " + "a timedelta/DateOffset for addition and " + "subtraction, but the operator [{name}] was " + "passed".format(name=name)) + + elif self.is_datetime_rhs: + # 2 datetimes + if name not in ('__sub__', '__rsub__'): + raise TypeError("can only operate on a datetimes for" + " subtraction, but the operator [{name}] was" + " passed".format(name=name)) + + # if tz's must be equal (same or None) + if getattr(lvalues, 'tz', None) != getattr(rvalues, 'tz', None): + raise ValueError("Incompatible tz's on datetime subtraction " + "ops") + + else: + raise TypeError('cannot operate on a series without a rhs ' + 'of a series/ndarray of type datetime64[ns] ' + 'or a timedelta') - if ((self.is_timedelta_lhs and - (self.is_integer_rhs or self.is_floating_rhs)) or - (self.is_timedelta_rhs and - (self.is_integer_lhs or self.is_floating_lhs))): + def _validate_timedelta(self, name): + # assumes self.is_timedelta_lhs + if self.is_integer_rhs or self.is_floating_rhs: + # timedelta and integer mul/div if name not in ('__div__', '__truediv__', '__mul__', '__rmul__'): raise TypeError("can only operate on a timedelta and an " "integer or a float for division and " "multiplication, but the operator [{name}] " "was passed".format(name=name)) - - # 2 timedeltas - elif ((self.is_timedelta_lhs and - (self.is_timedelta_rhs or self.is_offset_rhs)) or - (self.is_timedelta_rhs and - (self.is_timedelta_lhs or self.is_offset_lhs))): - + elif self.is_timedelta_rhs or self.is_offset_rhs: + # 2 timedeltas if name not in ('__div__', '__rdiv__', '__truediv__', '__rtruediv__', '__add__', '__radd__', '__sub__', '__rsub__'): raise TypeError("can only operate on a timedeltas for addition" ", subtraction, and division, but the operator" " [{name}] was passed".format(name=name)) - - # datetime and timedelta/DateOffset - elif (self.is_datetime_lhs and - (self.is_timedelta_rhs or self.is_offset_rhs)): - - if name not in ('__add__', '__radd__', '__sub__'): - raise TypeError("can only operate on a datetime with a rhs of " - "a timedelta/DateOffset for addition and " - "subtraction, but the operator [{name}] was " - "passed".format(name=name)) - - elif (self.is_datetime_rhs and - (self.is_timedelta_lhs or self.is_offset_lhs)): + elif self.is_datetime_rhs: if name not in ('__add__', '__radd__', '__rsub__'): raise TypeError("can only operate on a timedelta/DateOffset " "with a rhs of a datetime for addition, " "but the operator [{name}] was passed" .format(name=name)) + else: + raise TypeError('cannot operate on a series without a rhs ' + 'of a series/ndarray of type datetime64[ns] ' + 'or a timedelta') - # 2 datetimes - elif self.is_datetime_lhs and self.is_datetime_rhs: - - if name not in ('__sub__', '__rsub__'): - raise TypeError("can only operate on a datetimes for" - " subtraction, but the operator [{name}] was" - " passed".format(name=name)) - - # if tz's must be equal (same or None) - if getattr(lvalues, 'tz', None) != getattr(rvalues, 'tz', None): - raise ValueError("Incompatible tz's on datetime subtraction " - "ops") + def _validate_offset(self, name): + # assumes self.is_offset_lhs - elif ((self.is_timedelta_lhs or self.is_offset_lhs) and - self.is_datetime_rhs): + if self.is_timedelta_rhs: + # 2 timedeltas + if name not in ('__div__', '__rdiv__', '__truediv__', + '__rtruediv__', '__add__', '__radd__', '__sub__', + '__rsub__'): + raise TypeError("can only operate on a timedeltas for addition" + ", subtraction, and division, but the operator" + " [{name}] was passed".format(name=name)) + elif self.is_datetime_rhs: if name not in ('__add__', '__radd__'): raise TypeError("can only operate on a timedelta/DateOffset " "and a datetime for addition, but the operator" " [{name}] was passed".format(name=name)) + else: raise TypeError('cannot operate on a series without a rhs ' 'of a series/ndarray of type datetime64[ns] ' 'or a timedelta') - def _convert_to_array(self, values, name=None, other=None): - """converts values to ndarray""" - from pandas.core.tools.timedeltas import to_timedelta + def _validate(self, lvalues, rvalues, name): + if self.is_datetime_lhs: + return self._validate_datetime(lvalues, rvalues, name) + elif self.is_timedelta_lhs: + return self._validate_timedelta(name) + elif self.is_offset_lhs: + return self._validate_offset(name) - ovalues = values - supplied_dtype = None - if not is_list_like(values): - values = np.array([values]) + if ((self.is_integer_lhs or self.is_floating_lhs) and + self.is_timedelta_rhs): - # if this is a Series that contains relevant dtype info, then use this - # instead of the inferred type; this avoids coercing Series([NaT], - # dtype='datetime64[ns]') to Series([NaT], dtype='timedelta64[ns]') + if name not in ('__div__', '__truediv__', '__mul__', '__rmul__'): + raise TypeError("can only operate on a timedelta and an " + "integer or a float for division and " + "multiplication, but the operator [{name}] " + "was passed".format(name=name)) + else: + raise TypeError('cannot operate on a series without a rhs ' + 'of a series/ndarray of type datetime64[ns] ' + 'or a timedelta') + + def _convert_datetimelike_to_array(self, raw_values, values, other): + """ + By the time we get here, we know that: + * inferred_type in ('datetime64', 'datetime', 'date', 'time') or + is_datetimetz(inferred_type) + * is_list_like(values) + """ + supplied_dtype = None + if raw_values is not values: + # skip unnecessary isinstance call because we know values is an + # ndarray + pass elif (isinstance(values, pd.Series) and (is_timedelta64_dtype(values) or is_datetime64_dtype(values))): + # if this is a Series that contains relevant dtype info, then use + # this instead of the inferred type; this avoids coercing + # Series([NaT], dtype='datetime64[ns]') to + # Series([NaT], dtype='timedelta64[ns]') supplied_dtype = values.dtype inferred_type = lib.infer_dtype(values) - if (inferred_type in ('datetime64', 'datetime', 'date', 'time') or - is_datetimetz(inferred_type)): + + if (supplied_dtype is None and other is not None and + (other.dtype in ('timedelta64[ns]', 'datetime64[ns]')) and + isna(values).all()): # if we have a other of timedelta, but use pd.NaT here we # we are in the wrong path - if (supplied_dtype is None and other is not None and - (other.dtype in ('timedelta64[ns]', 'datetime64[ns]')) and - isna(values).all()): - values = np.empty(values.shape, dtype='timedelta64[ns]') - values[:] = iNaT + values = np.empty(values.shape, dtype='timedelta64[ns]') + values[:] = iNaT + elif isinstance(values, pd.DatetimeIndex): # a datelike - elif isinstance(values, pd.DatetimeIndex): - values = values.to_series() + values = values.to_series() + + elif (isinstance(raw_values, datetime.datetime) and + hasattr(raw_values, 'tzinfo')): # datetime with tz - elif (isinstance(ovalues, datetime.datetime) and - hasattr(ovalues, 'tzinfo')): - values = pd.DatetimeIndex(values) + values = pd.DatetimeIndex(values) + + elif is_datetimetz(values): # datetime array with tz - elif is_datetimetz(values): - if isinstance(values, ABCSeries): - values = values._values - elif not (isinstance(values, (np.ndarray, ABCSeries)) and - is_datetime64_dtype(values)): - values = libts.array_to_datetime(values) + if isinstance(values, ABCSeries): + values = values._values + + elif not (isinstance(values, (np.ndarray, ABCSeries)) and + is_datetime64_dtype(values)): + values = libts.array_to_datetime(values) + + return values + + def _convert_to_array(self, values, name=None, other=None): + """converts values to ndarray""" + from pandas.core.tools.timedeltas import to_timedelta + + ovalues = values + if not is_list_like(values): + values = np.array([values]) + + inferred_type = lib.infer_dtype(values) + + if (inferred_type in ('datetime64', 'datetime', 'date', 'time') or + is_datetimetz(inferred_type)): + return self._convert_datetimelike_to_array(ovalues, values, other) + elif inferred_type in ('timedelta', 'timedelta64'): # have a timedelta, convert to to ns here values = to_timedelta(values, errors='coerce', box=False) @@ -545,25 +599,12 @@ def _convert_for_datetime(self, lvalues, rvalues): else: self.dtype = 'datetime64[ns]' - # if adding single offset try vectorized path - # in DatetimeIndex; otherwise elementwise apply - def _offset(lvalues, rvalues): - if len(lvalues) == 1: - rvalues = pd.DatetimeIndex(rvalues) - lvalues = lvalues[0] - else: - warnings.warn("Adding/subtracting array of DateOffsets to " - "Series not vectorized", PerformanceWarning) - rvalues = rvalues.astype('O') - - # pass thru on the na_op - self.na_op = lambda x, y: getattr(x, self.name)(y) - return lvalues, rvalues - if self.is_offset_lhs: - lvalues, rvalues = _offset(lvalues, rvalues) + lvalues, rvalues, self.na_op = _offset_op(lvalues, rvalues, + self.name) elif self.is_offset_rhs: - rvalues, lvalues = _offset(rvalues, lvalues) + rvalues, lvalues, self.na_op = _offset_op(rvalues, lvalues, + self.name) else: # with tz, convert to UTC @@ -577,28 +618,7 @@ def _offset(lvalues, rvalues): # otherwise it's a timedelta else: - - self.dtype = 'timedelta64[ns]' - - # convert Tick DateOffset to underlying delta - if self.is_offset_lhs: - lvalues = to_timedelta(lvalues, box=False) - if self.is_offset_rhs: - rvalues = to_timedelta(rvalues, box=False) - - lvalues = lvalues.astype(np.int64) - if not self.is_floating_rhs: - rvalues = rvalues.astype(np.int64) - - # time delta division -> unit less - # integer gets converted to timedelta in np < 1.6 - if ((self.is_timedelta_lhs and self.is_timedelta_rhs) and - not self.is_integer_rhs and not self.is_integer_lhs and - self.name in ('__div__', '__truediv__')): - self.dtype = 'float64' - self.fill_value = np.nan - lvalues = lvalues.astype(np.float64) - rvalues = rvalues.astype(np.float64) + lvalues, rvalues = self._convert_for_timedelta(lvalues, rvalues) # if we need to mask the results if mask.any(): @@ -618,6 +638,33 @@ def f(x): return lvalues, rvalues + def _convert_for_timedelta(self, lvalues, rvalues): + from pandas.core.tools.timedeltas import to_timedelta + + self.dtype = 'timedelta64[ns]' + + # convert Tick DateOffset to underlying delta + if self.is_offset_lhs: + lvalues = to_timedelta(lvalues, box=False) + if self.is_offset_rhs: + rvalues = to_timedelta(rvalues, box=False) + + lvalues = lvalues.astype(np.int64) + if not self.is_floating_rhs: + rvalues = rvalues.astype(np.int64) + + if ((self.is_timedelta_lhs and self.is_timedelta_rhs) and + not self.is_integer_rhs and not self.is_integer_lhs and + self.name in ('__div__', '__truediv__')): + # timedelta division -> unit less + # integer gets converted to timedelta in np < 1.6 + self.dtype = 'float64' + self.fill_value = np.nan + lvalues = lvalues.astype(np.float64) + rvalues = rvalues.astype(np.float64) + + return lvalues, rvalues + def _is_offset(self, arr_or_obj): """ check if obj or all elements of list-like is DateOffset """ if isinstance(arr_or_obj, ABCDateOffset): @@ -628,6 +675,22 @@ def _is_offset(self, arr_or_obj): return False +def _offset_op(lvalues, rvalues, opname): + # if adding single offset try vectorized path + # in DatetimeIndex; otherwise elementwise apply + if len(lvalues) == 1: + rvalues = pd.DatetimeIndex(rvalues) + lvalues = lvalues[0] + else: + warnings.warn("Adding/subtracting array of DateOffsets to " + "Series not vectorized", PerformanceWarning) + rvalues = rvalues.astype('O') + + # pass thru on the na_op + na_op = lambda x, y: getattr(x, opname)(y) + return lvalues, rvalues, na_op + + def _align_method_SERIES(left, right, align_asobject=False): """ align lhs and rhs Series """ diff --git a/pandas/tests/series/test_operators.py b/pandas/tests/series/test_operators.py index 89a6311153d15..745240dd9d01e 100644 --- a/pandas/tests/series/test_operators.py +++ b/pandas/tests/series/test_operators.py @@ -960,8 +960,37 @@ def test_timedelta64_ops_nat(self): assert_series_equal(timedelta_series / nan, nat_series_dtype_timedelta) + def test_operators_timedelta64(self): + td1 = Series([timedelta(minutes=5, seconds=3)] * 3) + td1.iloc[2] = np.nan + td2 = timedelta(minutes=5, seconds=4) + + td1 + td2 + td2 + td1 + td1 - td2 + td2 - td1 + td1 / td2 + td2 / td1 + + @pytest.mark.parametrize('op_str', ['__mul__', '__rmul__', + '__floordiv__', '__rfloordiv__', + '__pow__', '__rpow__']) + def test_operators_timedelta64_invalid(self, op_str): + td1 = Series([timedelta(minutes=5, seconds=3)] * 3) + td1.iloc[2] = np.nan + td2 = timedelta(minutes=5, seconds=4) + + # check that we are getting a TypeError + # with 'operate' (from core/ops.py) for the ops that are not + # defined + op = getattr(td1, op_str, None) + pattern = 'operate|unsupported|cannot' + with tm.assert_raises_regex(TypeError, pattern): + op(td2) + class TestDatetimeSeriesArithmetic(object): + def test_operators_datetimelike(self): def run_ops(ops, get_ser, test_ser): @@ -976,16 +1005,6 @@ def run_ops(ops, get_ser, test_ser): # ## timedelta64 ### td1 = Series([timedelta(minutes=5, seconds=3)] * 3) td1.iloc[2] = np.nan - td2 = timedelta(minutes=5, seconds=4) - ops = ['__mul__', '__floordiv__', '__pow__', '__rmul__', - '__rfloordiv__', '__rpow__'] - run_ops(ops, td1, td2) - td1 + td2 - td2 + td1 - td1 - td2 - td2 - td1 - td1 / td2 - td2 / td1 # ## datetime64 ### dt1 = Series([Timestamp('20111230'), Timestamp('20120101'), From 1d7903ef549e79878d2411abf4366df706ba3ed6 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Mon, 18 Dec 2017 17:36:36 -0800 Subject: [PATCH 2/7] revert out of scope --- pandas/core/ops.py | 81 +++++++++++++++++----------------------------- 1 file changed, 29 insertions(+), 52 deletions(-) diff --git a/pandas/core/ops.py b/pandas/core/ops.py index f5ea7e5e1e58e..f9c1a7952bbd4 100644 --- a/pandas/core/ops.py +++ b/pandas/core/ops.py @@ -487,70 +487,47 @@ def _validate(self, lvalues, rvalues, name): 'of a series/ndarray of type datetime64[ns] ' 'or a timedelta') - def _convert_datetimelike_to_array(self, raw_values, values, other): - """ - By the time we get here, we know that: - * inferred_type in ('datetime64', 'datetime', 'date', 'time') or - is_datetimetz(inferred_type) - * is_list_like(values) - """ - supplied_dtype = None - if raw_values is not values: - # skip unnecessary isinstance call because we know values is an - # ndarray - pass - elif (isinstance(values, pd.Series) and - (is_timedelta64_dtype(values) or is_datetime64_dtype(values))): - # if this is a Series that contains relevant dtype info, then use - # this instead of the inferred type; this avoids coercing - # Series([NaT], dtype='datetime64[ns]') to - # Series([NaT], dtype='timedelta64[ns]') - supplied_dtype = values.dtype - - inferred_type = lib.infer_dtype(values) - - if (supplied_dtype is None and other is not None and - (other.dtype in ('timedelta64[ns]', 'datetime64[ns]')) and - isna(values).all()): - # if we have a other of timedelta, but use pd.NaT here we - # we are in the wrong path - values = np.empty(values.shape, dtype='timedelta64[ns]') - values[:] = iNaT - - elif isinstance(values, pd.DatetimeIndex): - # a datelike - values = values.to_series() - - elif (isinstance(raw_values, datetime.datetime) and - hasattr(raw_values, 'tzinfo')): - # datetime with tz - values = pd.DatetimeIndex(values) - - elif is_datetimetz(values): - # datetime array with tz - if isinstance(values, ABCSeries): - values = values._values - - elif not (isinstance(values, (np.ndarray, ABCSeries)) and - is_datetime64_dtype(values)): - values = libts.array_to_datetime(values) - - return values - def _convert_to_array(self, values, name=None, other=None): """converts values to ndarray""" from pandas.core.tools.timedeltas import to_timedelta ovalues = values + supplied_dtype = None if not is_list_like(values): values = np.array([values]) - inferred_type = lib.infer_dtype(values) + # if this is a Series that contains relevant dtype info, then use this + # instead of the inferred type; this avoids coercing Series([NaT], + # dtype='datetime64[ns]') to Series([NaT], dtype='timedelta64[ns]') + elif (isinstance(values, pd.Series) and + (is_timedelta64_dtype(values) or is_datetime64_dtype(values))): + supplied_dtype = values.dtype + inferred_type = lib.infer_dtype(values) if (inferred_type in ('datetime64', 'datetime', 'date', 'time') or is_datetimetz(inferred_type)): - return self._convert_datetimelike_to_array(ovalues, values, other) + # if we have a other of timedelta, but use pd.NaT here we + # we are in the wrong path + if (supplied_dtype is None and other is not None and + (other.dtype in ('timedelta64[ns]', 'datetime64[ns]')) and + isna(values).all()): + values = np.empty(values.shape, dtype='timedelta64[ns]') + values[:] = iNaT + # a datelike + elif isinstance(values, pd.DatetimeIndex): + values = values.to_series() + # datetime with tz + elif (isinstance(ovalues, datetime.datetime) and + hasattr(ovalues, 'tzinfo')): + values = pd.DatetimeIndex(values) + # datetime array with tz + elif is_datetimetz(values): + if isinstance(values, ABCSeries): + values = values._values + elif not (isinstance(values, (np.ndarray, ABCSeries)) and + is_datetime64_dtype(values)): + values = libts.array_to_datetime(values) elif inferred_type in ('timedelta', 'timedelta64'): # have a timedelta, convert to to ns here values = to_timedelta(values, errors='coerce', box=False) From 2809a413dcfd4db575be61ed87a89702e99ed998 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Mon, 18 Dec 2017 17:38:02 -0800 Subject: [PATCH 3/7] Revert out of scope --- pandas/core/ops.py | 50 ++++++++++++++++++++-------------------------- 1 file changed, 22 insertions(+), 28 deletions(-) diff --git a/pandas/core/ops.py b/pandas/core/ops.py index f9c1a7952bbd4..2839fe830f457 100644 --- a/pandas/core/ops.py +++ b/pandas/core/ops.py @@ -595,7 +595,28 @@ def _convert_for_datetime(self, lvalues, rvalues): # otherwise it's a timedelta else: - lvalues, rvalues = self._convert_for_timedelta(lvalues, rvalues) + + self.dtype = 'timedelta64[ns]' + + # convert Tick DateOffset to underlying delta + if self.is_offset_lhs: + lvalues = to_timedelta(lvalues, box=False) + if self.is_offset_rhs: + rvalues = to_timedelta(rvalues, box=False) + + lvalues = lvalues.astype(np.int64) + if not self.is_floating_rhs: + rvalues = rvalues.astype(np.int64) + + # time delta division -> unit less + # integer gets converted to timedelta in np < 1.6 + if ((self.is_timedelta_lhs and self.is_timedelta_rhs) and + not self.is_integer_rhs and not self.is_integer_lhs and + self.name in ('__div__', '__truediv__')): + self.dtype = 'float64' + self.fill_value = np.nan + lvalues = lvalues.astype(np.float64) + rvalues = rvalues.astype(np.float64) # if we need to mask the results if mask.any(): @@ -615,33 +636,6 @@ def f(x): return lvalues, rvalues - def _convert_for_timedelta(self, lvalues, rvalues): - from pandas.core.tools.timedeltas import to_timedelta - - self.dtype = 'timedelta64[ns]' - - # convert Tick DateOffset to underlying delta - if self.is_offset_lhs: - lvalues = to_timedelta(lvalues, box=False) - if self.is_offset_rhs: - rvalues = to_timedelta(rvalues, box=False) - - lvalues = lvalues.astype(np.int64) - if not self.is_floating_rhs: - rvalues = rvalues.astype(np.int64) - - if ((self.is_timedelta_lhs and self.is_timedelta_rhs) and - not self.is_integer_rhs and not self.is_integer_lhs and - self.name in ('__div__', '__truediv__')): - # timedelta division -> unit less - # integer gets converted to timedelta in np < 1.6 - self.dtype = 'float64' - self.fill_value = np.nan - lvalues = lvalues.astype(np.float64) - rvalues = rvalues.astype(np.float64) - - return lvalues, rvalues - def _is_offset(self, arr_or_obj): """ check if obj or all elements of list-like is DateOffset """ if isinstance(arr_or_obj, ABCDateOffset): From d005760532e33df7482d864d415803f1c29e046e Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Mon, 18 Dec 2017 17:39:32 -0800 Subject: [PATCH 4/7] revert out of scope --- pandas/core/ops.py | 37 +++++++++++++++++-------------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/pandas/core/ops.py b/pandas/core/ops.py index 2839fe830f457..60d09bebe7f3b 100644 --- a/pandas/core/ops.py +++ b/pandas/core/ops.py @@ -576,12 +576,25 @@ def _convert_for_datetime(self, lvalues, rvalues): else: self.dtype = 'datetime64[ns]' + # if adding single offset try vectorized path + # in DatetimeIndex; otherwise elementwise apply + def _offset(lvalues, rvalues): + if len(lvalues) == 1: + rvalues = pd.DatetimeIndex(rvalues) + lvalues = lvalues[0] + else: + warnings.warn("Adding/subtracting array of DateOffsets to " + "Series not vectorized", PerformanceWarning) + rvalues = rvalues.astype('O') + + # pass thru on the na_op + self.na_op = lambda x, y: getattr(x, self.name)(y) + return lvalues, rvalues + if self.is_offset_lhs: - lvalues, rvalues, self.na_op = _offset_op(lvalues, rvalues, - self.name) + lvalues, rvalues = _offset(lvalues, rvalues) elif self.is_offset_rhs: - rvalues, lvalues, self.na_op = _offset_op(rvalues, lvalues, - self.name) + rvalues, lvalues = _offset(rvalues, lvalues) else: # with tz, convert to UTC @@ -646,22 +659,6 @@ def _is_offset(self, arr_or_obj): return False -def _offset_op(lvalues, rvalues, opname): - # if adding single offset try vectorized path - # in DatetimeIndex; otherwise elementwise apply - if len(lvalues) == 1: - rvalues = pd.DatetimeIndex(rvalues) - lvalues = lvalues[0] - else: - warnings.warn("Adding/subtracting array of DateOffsets to " - "Series not vectorized", PerformanceWarning) - rvalues = rvalues.astype('O') - - # pass thru on the na_op - na_op = lambda x, y: getattr(x, opname)(y) - return lvalues, rvalues, na_op - - def _align_method_SERIES(left, right, align_asobject=False): """ align lhs and rhs Series """ From 06bf5239b6709865e234cad75ab988b98cab4ca4 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Mon, 18 Dec 2017 17:42:39 -0800 Subject: [PATCH 5/7] make test more explicit --- pandas/tests/series/test_operators.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/pandas/tests/series/test_operators.py b/pandas/tests/series/test_operators.py index 745240dd9d01e..ec374b24ed14b 100644 --- a/pandas/tests/series/test_operators.py +++ b/pandas/tests/series/test_operators.py @@ -972,10 +972,7 @@ def test_operators_timedelta64(self): td1 / td2 td2 / td1 - @pytest.mark.parametrize('op_str', ['__mul__', '__rmul__', - '__floordiv__', '__rfloordiv__', - '__pow__', '__rpow__']) - def test_operators_timedelta64_invalid(self, op_str): + def test_operators_timedelta64_invalid(self): td1 = Series([timedelta(minutes=5, seconds=3)] * 3) td1.iloc[2] = np.nan td2 = timedelta(minutes=5, seconds=4) @@ -983,10 +980,19 @@ def test_operators_timedelta64_invalid(self, op_str): # check that we are getting a TypeError # with 'operate' (from core/ops.py) for the ops that are not # defined - op = getattr(td1, op_str, None) pattern = 'operate|unsupported|cannot' with tm.assert_raises_regex(TypeError, pattern): - op(td2) + td1 * td2 + with tm.assert_raises_regex(TypeError, pattern): + td2 * td1 + with tm.assert_raises_regex(TypeError, pattern): + td1 // td2 + with tm.assert_raises_regex(TypeError, pattern): + td2 // td1 + with tm.assert_raises_regex(TypeError, pattern): + td2 ** td1 + with tm.assert_raises_regex(TypeError, pattern): + td1 ** td2 class TestDatetimeSeriesArithmetic(object): From cbc04b72ea634a2186fb1e984275777137fba3c4 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Mon, 18 Dec 2017 18:21:35 -0800 Subject: [PATCH 6/7] rename duplicate test --- pandas/tests/series/test_operators.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pandas/tests/series/test_operators.py b/pandas/tests/series/test_operators.py index ec374b24ed14b..22bec47455eff 100644 --- a/pandas/tests/series/test_operators.py +++ b/pandas/tests/series/test_operators.py @@ -960,7 +960,8 @@ def test_timedelta64_ops_nat(self): assert_series_equal(timedelta_series / nan, nat_series_dtype_timedelta) - def test_operators_timedelta64(self): + def test_operators_timedelta64_with_timedelta(self): + # smoke tests td1 = Series([timedelta(minutes=5, seconds=3)] * 3) td1.iloc[2] = np.nan td2 = timedelta(minutes=5, seconds=4) @@ -972,7 +973,7 @@ def test_operators_timedelta64(self): td1 / td2 td2 / td1 - def test_operators_timedelta64_invalid(self): + def test_operators_timedelta64_with_timedelta_invalid(self): td1 = Series([timedelta(minutes=5, seconds=3)] * 3) td1.iloc[2] = np.nan td2 = timedelta(minutes=5, seconds=4) From a3cab61d442f67ba5628194c078031929449ca75 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Thu, 21 Dec 2017 13:42:43 -0800 Subject: [PATCH 7/7] edits per request; parametrize tests --- pandas/core/ops.py | 20 ++++++------- pandas/tests/series/test_operators.py | 41 ++++++++++++++++----------- 2 files changed, 33 insertions(+), 28 deletions(-) diff --git a/pandas/core/ops.py b/pandas/core/ops.py index 60d09bebe7f3b..6165eea708796 100644 --- a/pandas/core/ops.py +++ b/pandas/core/ops.py @@ -419,11 +419,7 @@ def _validate_timedelta(self, name): if self.is_integer_rhs or self.is_floating_rhs: # timedelta and integer mul/div - if name not in ('__div__', '__truediv__', '__mul__', '__rmul__'): - raise TypeError("can only operate on a timedelta and an " - "integer or a float for division and " - "multiplication, but the operator [{name}] " - "was passed".format(name=name)) + self._check_timedelta_with_numeric(name) elif self.is_timedelta_rhs or self.is_offset_rhs: # 2 timedeltas if name not in ('__div__', '__rdiv__', '__truediv__', @@ -476,17 +472,19 @@ def _validate(self, lvalues, rvalues, name): if ((self.is_integer_lhs or self.is_floating_lhs) and self.is_timedelta_rhs): - - if name not in ('__div__', '__truediv__', '__mul__', '__rmul__'): - raise TypeError("can only operate on a timedelta and an " - "integer or a float for division and " - "multiplication, but the operator [{name}] " - "was passed".format(name=name)) + self._check_timedelta_with_numeric(name) else: raise TypeError('cannot operate on a series without a rhs ' 'of a series/ndarray of type datetime64[ns] ' 'or a timedelta') + def _check_timedelta_with_numeric(self, name): + if name not in ('__div__', '__truediv__', '__mul__', '__rmul__'): + raise TypeError("can only operate on a timedelta and an " + "integer or a float for division and " + "multiplication, but the operator [{name}] " + "was passed".format(name=name)) + def _convert_to_array(self, values, name=None, other=None): """converts values to ndarray""" from pandas.core.tools.timedeltas import to_timedelta diff --git a/pandas/tests/series/test_operators.py b/pandas/tests/series/test_operators.py index 22bec47455eff..4adbdbca82fd2 100644 --- a/pandas/tests/series/test_operators.py +++ b/pandas/tests/series/test_operators.py @@ -960,40 +960,47 @@ def test_timedelta64_ops_nat(self): assert_series_equal(timedelta_series / nan, nat_series_dtype_timedelta) - def test_operators_timedelta64_with_timedelta(self): + @pytest.mark.parametrize('scalar_td', [timedelta(minutes=5, seconds=4), + Timedelta(minutes=5, seconds=4), + Timedelta('5m4s').to_timedelta64()]) + def test_operators_timedelta64_with_timedelta(self, scalar_td): # smoke tests td1 = Series([timedelta(minutes=5, seconds=3)] * 3) td1.iloc[2] = np.nan - td2 = timedelta(minutes=5, seconds=4) - - td1 + td2 - td2 + td1 - td1 - td2 - td2 - td1 - td1 / td2 - td2 / td1 - def test_operators_timedelta64_with_timedelta_invalid(self): + td1 + scalar_td + scalar_td + td1 + td1 - scalar_td + scalar_td - td1 + td1 / scalar_td + scalar_td / td1 + + @pytest.mark.parametrize('scalar_td', [ + timedelta(minutes=5, seconds=4), + pytest.param(Timedelta('5m4s'), + marks=pytest.mark.xfail(reason="Timedelta.__floordiv__ " + "bug GH#18846")), + Timedelta('5m4s').to_timedelta64()]) + def test_operators_timedelta64_with_timedelta_invalid(self, scalar_td): td1 = Series([timedelta(minutes=5, seconds=3)] * 3) td1.iloc[2] = np.nan - td2 = timedelta(minutes=5, seconds=4) # check that we are getting a TypeError # with 'operate' (from core/ops.py) for the ops that are not # defined pattern = 'operate|unsupported|cannot' with tm.assert_raises_regex(TypeError, pattern): - td1 * td2 + td1 * scalar_td with tm.assert_raises_regex(TypeError, pattern): - td2 * td1 + scalar_td * td1 with tm.assert_raises_regex(TypeError, pattern): - td1 // td2 + td1 // scalar_td with tm.assert_raises_regex(TypeError, pattern): - td2 // td1 + scalar_td // td1 with tm.assert_raises_regex(TypeError, pattern): - td2 ** td1 + scalar_td ** td1 with tm.assert_raises_regex(TypeError, pattern): - td1 ** td2 + td1 ** scalar_td class TestDatetimeSeriesArithmetic(object):