From 4b93740f6bf54ef8454bcb8a48a518db799f3b7c Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Tue, 12 May 2020 21:19:28 -0700 Subject: [PATCH 1/3] REF: move more of Tick into liboffsets._Tick --- pandas/_libs/tslibs/offsets.pyx | 77 ++++++++++++++++++- .../tests/scalar/timestamp/test_arithmetic.py | 2 +- pandas/tseries/offsets.py | 69 +---------------- 3 files changed, 80 insertions(+), 68 deletions(-) diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx index f2860fad75428..9265a99ce65ac 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -35,6 +35,7 @@ from pandas._libs.tslibs.np_datetime cimport ( from pandas._libs.tslibs.timezones cimport utc_pytz as UTC from pandas._libs.tslibs.tzconversion cimport tz_convert_single +from pandas._libs.tslibs.timedeltas import Timedelta from pandas._libs.tslibs.timestamps import Timestamp # --------------------------------------------------------------------- @@ -643,7 +644,10 @@ class _BaseOffset: # ------------------------------------------------------------------ - def _validate_n(self, n): + # Staticmethod so we can call from _Tick.__init__, will be unnecessary + # once BaseOffset is a cdef class and is inherited by _Tick + @staticmethod + def _validate_n(n): """ Require that `n` be an integer. @@ -755,6 +759,33 @@ cdef class _Tick(ABCTick): # ensure that reversed-ops with numpy scalars return NotImplemented __array_priority__ = 1000 _adjust_dst = False + _inc = Timedelta(microseconds=1000) + _prefix = "undefined" + _attributes = frozenset(["n", "normalize"]) + + cdef readonly: + int64_t n + bint normalize + dict _cache + + def __init__(self, n=1, normalize=False): + n = _BaseOffset._validate_n(n) + self.n = n + self.normalize = False + self._cache = {} + if normalize: + # GH#21427 + raise ValueError( + "Tick offset with `normalize=True` are not allowed." + ) + + @property + def delta(self) -> Timedelta: + return self.n * self._inc + + @property + def nanos(self) -> int64_t: + return self.delta.value def is_on_offset(self, dt) -> bool: return True @@ -762,6 +793,44 @@ cdef class _Tick(ABCTick): def is_anchored(self) -> bool: return False + # -------------------------------------------------------------------- + # Comparison and Arithmetic Methods + + def __eq__(self, other): + if isinstance(other, str): + try: + # GH#23524 if to_offset fails, we are dealing with an + # incomparable type so == is False and != is True + other = to_offset(other) + except ValueError: + # e.g. "infer" + return False + return self.delta == other + + def __ne__(self, other): + if isinstance(other, str): + try: + # GH#23524 if to_offset fails, we are dealing with an + # incomparable type so == is False and != is True + other = to_offset(other) + except ValueError: + # e.g. "infer" + return True + + return self.delta != other + + def __le__(self, other): + return self.delta.__le__(other) + + def __lt__(self, other): + return self.delta.__lt__(other) + + def __ge__(self, other): + return self.delta.__ge__(other) + + def __gt__(self, other): + return self.delta.__gt__(other) + def __truediv__(self, other): if not isinstance(self, _Tick): # cython semantics mean the args are sometimes swapped @@ -770,11 +839,15 @@ cdef class _Tick(ABCTick): result = self.delta.__truediv__(other) return _wrap_timedelta_result(result) + # -------------------------------------------------------------------- + # Pickle Methods + def __reduce__(self): return (type(self), (self.n,)) def __setstate__(self, state): - object.__setattr__(self, "n", state["n"]) + self.n = state["n"] + self.normalize = False class BusinessMixin: diff --git a/pandas/tests/scalar/timestamp/test_arithmetic.py b/pandas/tests/scalar/timestamp/test_arithmetic.py index b038ee1aee106..ed0045bcab989 100644 --- a/pandas/tests/scalar/timestamp/test_arithmetic.py +++ b/pandas/tests/scalar/timestamp/test_arithmetic.py @@ -52,7 +52,7 @@ def test_overflow_offset_raises(self): # used to crash, so check for proper overflow exception stamp = Timestamp("2000/1/1") - offset_overflow = to_offset("D") * 100 ** 25 + offset_overflow = to_offset("D") * 100 ** 5 with pytest.raises(OverflowError, match=msg): stamp + offset_overflow diff --git a/pandas/tseries/offsets.py b/pandas/tseries/offsets.py index 4c06fea51ea8d..ba7d586d573fb 100644 --- a/pandas/tseries/offsets.py +++ b/pandas/tseries/offsets.py @@ -1,6 +1,6 @@ from datetime import date, datetime, timedelta import operator -from typing import Any, Optional +from typing import Optional from dateutil.easter import easter import numpy as np @@ -2129,35 +2129,7 @@ def is_on_offset(self, dt: datetime) -> bool: # Ticks -def _tick_comp(op): - """ - Tick comparisons should behave identically to Timedelta comparisons. - """ - - def f(self, other): - return op(self.delta, other) - - f.__name__ = f"__{op.__name__}__" - return f - - class Tick(liboffsets._Tick, SingleConstructorOffset): - _inc = Timedelta(microseconds=1000) - _prefix = "undefined" - _attributes = frozenset(["n", "normalize"]) - - def __init__(self, n=1, normalize=False): - BaseOffset.__init__(self, n, normalize) - if normalize: - raise ValueError( - "Tick offset with `normalize=True` are not allowed." - ) # GH#21427 - - __gt__ = _tick_comp(operator.gt) - __ge__ = _tick_comp(operator.ge) - __lt__ = _tick_comp(operator.lt) - __le__ = _tick_comp(operator.le) - def __add__(self, other): if isinstance(other, Tick): if type(self) == type(other): @@ -2175,47 +2147,11 @@ def __add__(self, other): f"the add operation between {self} and {other} will overflow" ) from err - def __eq__(self, other: Any) -> bool: - if isinstance(other, str): - from pandas.tseries.frequencies import to_offset - - try: - # GH#23524 if to_offset fails, we are dealing with an - # incomparable type so == is False and != is True - other = to_offset(other) - except ValueError: - # e.g. "infer" - return False - - return _tick_comp(operator.eq)(self, other) - # This is identical to DateOffset.__hash__, but has to be redefined here # for Python 3, because we've redefined __eq__. def __hash__(self) -> int: return hash(self._params) - def __ne__(self, other): - if isinstance(other, str): - from pandas.tseries.frequencies import to_offset - - try: - # GH#23524 if to_offset fails, we are dealing with an - # incomparable type so == is False and != is True - other = to_offset(other) - except ValueError: - # e.g. "infer" - return True - - return _tick_comp(operator.ne)(self, other) - - @property - def delta(self) -> Timedelta: - return self.n * self._inc - - @property - def nanos(self): - return delta_to_nanoseconds(self.delta) - def apply(self, other): # Timestamp can handle tz and nano sec, thus no need to use apply_wraps if isinstance(other, Timestamp): @@ -2235,6 +2171,9 @@ def apply(self, other): if isinstance(other, timedelta): return other + self.delta elif isinstance(other, type(self)): + # TODO: this is reached in tests that specifically call apply, + # but should not be reached "naturally" because __add__ should + # catch this case first. return type(self)(self.n + other.n) raise ApplyTypeError(f"Unhandled type: {type(other).__name__}") From 634e1477c63b1aa0038c945585d2bcd44bf27122 Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Wed, 13 May 2020 08:43:30 -0700 Subject: [PATCH 2/3] troubleshoot docs --- doc/source/reference/offset_frequency.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/doc/source/reference/offset_frequency.rst b/doc/source/reference/offset_frequency.rst index 6240181708f97..9b2753ca02495 100644 --- a/doc/source/reference/offset_frequency.rst +++ b/doc/source/reference/offset_frequency.rst @@ -1044,6 +1044,7 @@ Properties Tick.nanos Tick.normalize Tick.rule_code + Tick.n Methods ~~~~~~~ @@ -1077,6 +1078,7 @@ Properties Day.nanos Day.normalize Day.rule_code + Day.n Methods ~~~~~~~ @@ -1110,6 +1112,7 @@ Properties Hour.nanos Hour.normalize Hour.rule_code + Hour.n Methods ~~~~~~~ @@ -1143,6 +1146,7 @@ Properties Minute.nanos Minute.normalize Minute.rule_code + Minute.n Methods ~~~~~~~ @@ -1176,6 +1180,7 @@ Properties Second.nanos Second.normalize Second.rule_code + Second.n Methods ~~~~~~~ @@ -1209,6 +1214,7 @@ Properties Milli.nanos Milli.normalize Milli.rule_code + Milli.n Methods ~~~~~~~ @@ -1242,6 +1248,7 @@ Properties Micro.nanos Micro.normalize Micro.rule_code + Micro.n Methods ~~~~~~~ @@ -1275,6 +1282,7 @@ Properties Nano.nanos Nano.normalize Nano.rule_code + Nano.n Methods ~~~~~~~ From 3d4e54e0ef4198163441e8d9cbb22b807b287f93 Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Thu, 14 May 2020 10:23:25 -0700 Subject: [PATCH 3/3] simplify __ne__ --- pandas/_libs/tslibs/offsets.pyx | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx index ab950d51f1d7d..d219f4b8d7944 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -813,16 +813,7 @@ cdef class _Tick(ABCTick): return self.delta == other def __ne__(self, other): - if isinstance(other, str): - try: - # GH#23524 if to_offset fails, we are dealing with an - # incomparable type so == is False and != is True - other = to_offset(other) - except ValueError: - # e.g. "infer" - return True - - return self.delta != other + return not (self == other) def __le__(self, other): return self.delta.__le__(other)