From 23381018888497fd3b17db11137d2e2983210f26 Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Thu, 7 May 2020 15:55:49 -0700 Subject: [PATCH 1/2] REF: move apply_wraps up into liboffstes --- pandas/_libs/tslibs/offsets.pyx | 102 ++++++++++++++++++++++++++++- pandas/_libs/tslibs/timedeltas.pyx | 2 +- pandas/tseries/offsets.py | 87 +----------------------- 3 files changed, 103 insertions(+), 88 deletions(-) diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx index ff0d96f0f2525..fc0d10c8a4895 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -6,7 +6,7 @@ import warnings from cpython.datetime cimport (PyDateTime_IMPORT, PyDateTime_Check, PyDelta_Check, - datetime, timedelta, + datetime, timedelta, date, time as dt_time) PyDateTime_IMPORT @@ -29,12 +29,13 @@ from pandas._libs.tslibs.conversion cimport ( convert_datetime_to_tsobject, localize_pydatetime, ) -from pandas._libs.tslibs.nattype cimport NPY_NAT +from pandas._libs.tslibs.nattype cimport NPY_NAT, c_NaT as NaT from pandas._libs.tslibs.np_datetime cimport ( npy_datetimestruct, dtstruct_to_dt64, dt64_to_dtstruct) from pandas._libs.tslibs.timezones cimport utc_pytz as UTC from pandas._libs.tslibs.tzconversion cimport tz_convert_single +from pandas._libs.tslibs.timestamps import Timestamp # --------------------------------------------------------------------- # Constants @@ -154,6 +155,63 @@ def apply_index_wraps(func): return wrapper +def apply_wraps(func): + # Note: normally we would use `@functools.wraps(func)`, but this does + # not play nicely with cython class methods + + def wrapper(self, other): + if other is NaT: + return NaT + elif isinstance(other, (timedelta, BaseOffset)): + # timedelta path + return func(self, other) + elif isinstance(other, (np.datetime64, datetime, date)): + other = Timestamp(other) + else: + raise TypeError(other) + + tz = other.tzinfo + nano = other.nanosecond + + if self._adjust_dst: + other = other.tz_localize(None) + + result = func(self, other) + + result = Timestamp(result) + if self._adjust_dst: + result = result.tz_localize(tz) + + if self.normalize: + result = result.normalize() + + # nanosecond may be deleted depending on offset process + if not self.normalize and nano != 0: + if result.nanosecond != nano: + if result.tz is not None: + # convert to UTC + value = result.tz_localize(None).value + else: + value = result.value + result = Timestamp(value + nano) + + if tz is not None and result.tzinfo is None: + result = result.tz_localize(tz) + + return result + + # do @functools.wraps(func) manually since it doesn't work on cdef funcs + wrapper.__name__ = func.__name__ + wrapper.__doc__ = func.__doc__ + try: + wrapper.__module__ = func.__module__ + except AttributeError: + # AttributeError: 'method_descriptor' object has no + # attribute '__module__' + pass + return wrapper + + cdef _wrap_timedelta_result(result): """ Tick operations dispatch to their Timedelta counterparts. Wrap the result @@ -348,6 +406,10 @@ class _BaseOffset: _typ = "dateoffset" _day_opt = None _attributes = frozenset(['n', 'normalize']) + _use_relativedelta = False + _adjust_dst = False + _deprecations = frozenset(["isAnchored", "onOffset"]) + normalize = False # default for prior pickles def __init__(self, n=1, normalize=False): n = self._validate_n(n) @@ -508,11 +570,41 @@ class _BaseOffset: # ------------------------------------------------------------------ + def rollback(self, dt): + """ + Roll provided date backward to next offset only if not on offset. + + Returns + ------- + TimeStamp + Rolled timestamp if not on offset, otherwise unchanged timestamp. + """ + dt = Timestamp(dt) + if not self.is_on_offset(dt): + dt = dt - type(self)(1, normalize=self.normalize, **self.kwds) + return dt + + def rollforward(self, dt): + """ + Roll provided date forward to next offset only if not on offset. + + Returns + ------- + TimeStamp + Rolled timestamp if not on offset, otherwise unchanged timestamp. + """ + dt = Timestamp(dt) + if not self.is_on_offset(dt): + dt = dt + type(self)(1, normalize=self.normalize, **self.kwds) + return dt + def _get_offset_day(self, datetime other): # subclass must implement `_day_opt`; calling from the base class # will raise NotImplementedError. return get_day_of_month(other, self._day_opt) + # ------------------------------------------------------------------ + def _validate_n(self, n): """ Require that `n` be an integer. @@ -628,6 +720,12 @@ cdef class _Tick(ABCTick): # ensure that reversed-ops with numpy scalars return NotImplemented __array_priority__ = 1000 + def is_on_offset(self, dt) -> bool: + return True + + def is_anchored(self) -> bool: + return False + def __truediv__(self, other): if not isinstance(self, _Tick): # cython semantics mean the args are sometimes swapped diff --git a/pandas/_libs/tslibs/timedeltas.pyx b/pandas/_libs/tslibs/timedeltas.pyx index 9016df335e2c5..af9830dc31838 100644 --- a/pandas/_libs/tslibs/timedeltas.pyx +++ b/pandas/_libs/tslibs/timedeltas.pyx @@ -31,7 +31,6 @@ from pandas._libs.tslibs.np_datetime cimport ( from pandas._libs.tslibs.nattype import nat_strings from pandas._libs.tslibs.nattype cimport ( checknull_with_nat, NPY_NAT, c_NaT as NaT) -from pandas._libs.tslibs.offsets cimport to_offset # ---------------------------------------------------------------------- # Constants @@ -1284,6 +1283,7 @@ class Timedelta(_Timedelta): cdef: int64_t result, unit + from pandas.tseries.frequencies import to_offset unit = to_offset(freq).nanos result = unit * rounder(self.value / float(unit)) return Timedelta(result, unit='ns') diff --git a/pandas/tseries/offsets.py b/pandas/tseries/offsets.py index e1de9d1bcf832..b61f44fb5580a 100644 --- a/pandas/tseries/offsets.py +++ b/pandas/tseries/offsets.py @@ -1,5 +1,4 @@ from datetime import date, datetime, timedelta -import functools import operator from typing import Any, Optional @@ -23,6 +22,7 @@ BusinessMixin, CustomMixin, apply_index_wraps, + apply_wraps, as_datetime, is_normalized, roll_yearday, @@ -74,52 +74,6 @@ ] -def apply_wraps(func): - @functools.wraps(func) - def wrapper(self, other): - if other is NaT: - return NaT - elif isinstance(other, (timedelta, DateOffset)): - # timedelta path - return func(self, other) - elif isinstance(other, (np.datetime64, datetime, date)): - other = Timestamp(other) - else: - raise TypeError(other) - - tz = other.tzinfo - nano = other.nanosecond - - if self._adjust_dst: - other = other.tz_localize(None) - - result = func(self, other) - - result = Timestamp(result) - if self._adjust_dst: - result = result.tz_localize(tz) - - if self.normalize: - result = result.normalize() - - # nanosecond may be deleted depending on offset process - if not self.normalize and nano != 0: - if not isinstance(self, Nano) and result.nanosecond != nano: - if result.tz is not None: - # convert to UTC - value = result.tz_localize(None).value - else: - value = result.value - result = Timestamp(value + nano) - - if tz is not None and result.tzinfo is None: - result = result.tz_localize(tz) - - return result - - return wrapper - - # --------------------------------------------------------------------- # DateOffset @@ -221,13 +175,7 @@ def __add__(date): _params = cache_readonly(BaseOffset._params.fget) freqstr = cache_readonly(BaseOffset.freqstr.fget) - _use_relativedelta = False - _adjust_dst = False _attributes = frozenset(["n", "normalize"] + list(liboffsets.relativedelta_kwds)) - _deprecations = frozenset(["isAnchored", "onOffset"]) - - # default for prior pickles - normalize = False def __init__(self, n=1, normalize=False, **kwds): BaseOffset.__init__(self, n, normalize) @@ -356,39 +304,11 @@ def _repr_attrs(self) -> str: out += ": " + ", ".join(attrs) return out - def rollback(self, dt): - """ - Roll provided date backward to next offset only if not on offset. - - Returns - ------- - TimeStamp - Rolled timestamp if not on offset, otherwise unchanged timestamp. - """ - dt = Timestamp(dt) - if not self.is_on_offset(dt): - dt = dt - type(self)(1, normalize=self.normalize, **self.kwds) - return dt - - def rollforward(self, dt): - """ - Roll provided date forward to next offset only if not on offset. - - Returns - ------- - TimeStamp - Rolled timestamp if not on offset, otherwise unchanged timestamp. - """ - dt = Timestamp(dt) - if not self.is_on_offset(dt): - dt = dt + type(self)(1, normalize=self.normalize, **self.kwds) - return dt - def is_on_offset(self, dt): if self.normalize and not is_normalized(dt): return False # TODO, see #1395 - if type(self) == DateOffset or isinstance(self, Tick): + if type(self) is DateOffset: return True # Default (slow) method for determining if some date is a member of the @@ -2485,9 +2405,6 @@ def apply(self, other): raise ApplyTypeError(f"Unhandled type: {type(other).__name__}") - def is_anchored(self) -> bool: - return False - def delta_to_tick(delta: timedelta) -> Tick: if delta.microseconds == 0 and getattr(delta, "nanoseconds", 0) == 0: From bde11501127775bf275b9ac1149c099edc0783df Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Fri, 8 May 2020 09:54:03 -0700 Subject: [PATCH 2/2] dummy commit to force CI