From 861a0f915f11fc48064eba164cd05019abfbbd85 Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Sat, 23 May 2020 13:09:27 -0700 Subject: [PATCH 1/2] REF: move Year/Month/Quarter offsets to liboffsets --- pandas/_libs/tslibs/offsets.pyx | 167 +++++++++++++++++++++++++++++++- pandas/tseries/offsets.py | 158 +++--------------------------- 2 files changed, 175 insertions(+), 150 deletions(-) diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx index f9ddb6fabc7bb..630b2a965a40b 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -456,11 +456,12 @@ cdef class BaseOffset: equality between two DateOffset objects. """ # NB: non-cython subclasses override property with cache_readonly - all_paras = self.__dict__.copy() + d = getattr(self, "__dict__", {}) + all_paras = d.copy() all_paras["n"] = self.n all_paras["normalize"] = self.normalize for attr in self._attributes: - if hasattr(self, attr) and attr not in self.__dict__: + if hasattr(self, attr) and attr not in d: # cython attributes are not in __dict__ all_paras[attr] = getattr(self, attr) @@ -711,6 +712,15 @@ cdef class BaseOffset: def __setstate__(self, state): """Reconstruct an instance from a pickled state""" + if isinstance(self, MonthOffset): + # We can't just override MonthOffset.__setstate__ because of the + # combination of MRO resolution and cython not handling + # multiple inheritance nicely for cdef classes. + state.pop("_use_relativedelta", False) + state.pop("offset", None) + state.pop("_offset", None) + state.pop("kwds", {}) + if 'offset' in state: # Older (<0.22.0) versions have offset attribute instead of _offset if '_offset' in state: # pragma: no cover @@ -728,6 +738,12 @@ cdef class BaseOffset: self.n = state.pop("n") self.normalize = state.pop("normalize") self._cache = state.pop("_cache", {}) + + if not len(state): + # FIXME: kludge because some classes no longer have a __dict__, + # so we need to short-circuit before raising on the next line + return + self.__dict__.update(state) if 'weekmask' in state and 'holidays' in state: @@ -739,7 +755,7 @@ cdef class BaseOffset: def __getstate__(self): """Return a pickleable state""" - state = self.__dict__.copy() + state = getattr(self, "__dict__", {}).copy() state["n"] = self.n state["normalize"] = self.normalize @@ -1020,8 +1036,11 @@ cdef class BusinessMixin(SingleConstructorOffset): cpdef __setstate__(self, state): # We need to use a cdef/cpdef method to set the readonly _offset attribute + if "_offset" in state: + self._offset = state.pop("_offset") + elif "offset" in state: + self._offset = state.pop("offset") BaseOffset.__setstate__(self, state) - self._offset = state["_offset"] class BusinessHourMixin(BusinessMixin): @@ -1180,6 +1199,7 @@ class WeekOfMonthMixin(SingleConstructorOffset): # ---------------------------------------------------------------------- +# Year-Based Offset Classes cdef class YearOffset(SingleConstructorOffset): """ @@ -1201,6 +1221,12 @@ cdef class YearOffset(SingleConstructorOffset): if month < 1 or month > 12: raise ValueError("Month must go from 1 to 12") + cpdef __setstate__(self, state): + self.month = state.pop("month") + self.n = state.pop("n") + self.normalize = state.pop("normalize") + self._cache = {} + def __reduce__(self): return type(self), (self.n, self.normalize, self.month) @@ -1242,6 +1268,51 @@ cdef class YearOffset(SingleConstructorOffset): return type(dtindex)._simple_new(shifted, dtype=dtindex.dtype) +cdef class BYearEnd(YearOffset): + """ + DateOffset increments between business EOM dates. + """ + + _outputName = "BusinessYearEnd" + _default_month = 12 + _prefix = "BA" + _day_opt = "business_end" + + +cdef class BYearBegin(YearOffset): + """ + DateOffset increments between business year begin dates. + """ + + _outputName = "BusinessYearBegin" + _default_month = 1 + _prefix = "BAS" + _day_opt = "business_start" + + +cdef class YearEnd(YearOffset): + """ + DateOffset increments between calendar year ends. + """ + + _default_month = 12 + _prefix = "A" + _day_opt = "end" + + +cdef class YearBegin(YearOffset): + """ + DateOffset increments between calendar year begin dates. + """ + + _default_month = 1 + _prefix = "AS" + _day_opt = "start" + + +# ---------------------------------------------------------------------- +# Quarter-Based Offset Classes + cdef class QuarterOffset(SingleConstructorOffset): _attributes = frozenset(["n", "normalize", "startingMonth"]) # TODO: Consider combining QuarterOffset and YearOffset __init__ at some @@ -1262,6 +1333,11 @@ cdef class QuarterOffset(SingleConstructorOffset): startingMonth = self._default_startingMonth self.startingMonth = startingMonth + cpdef __setstate__(self, state): + self.startingMonth = state.pop("startingMonth") + self.n = state.pop("n") + self.normalize = state.pop("normalize") + def __reduce__(self): return type(self), (self.n, self.normalize, self.startingMonth) @@ -1311,6 +1387,57 @@ cdef class QuarterOffset(SingleConstructorOffset): return type(dtindex)._simple_new(shifted, dtype=dtindex.dtype) +cdef class BQuarterEnd(QuarterOffset): + """ + DateOffset increments between business Quarter dates. + + startingMonth = 1 corresponds to dates like 1/31/2007, 4/30/2007, ... + startingMonth = 2 corresponds to dates like 2/28/2007, 5/31/2007, ... + startingMonth = 3 corresponds to dates like 3/30/2007, 6/29/2007, ... + """ + _outputName = "BusinessQuarterEnd" + _default_startingMonth = 3 + _from_name_startingMonth = 12 + _prefix = "BQ" + _day_opt = "business_end" + + +# TODO: This is basically the same as BQuarterEnd +cdef class BQuarterBegin(QuarterOffset): + _outputName = "BusinessQuarterBegin" + # I suspect this is wrong for *all* of them. + # TODO: What does the above comment refer to? + _default_startingMonth = 3 + _from_name_startingMonth = 1 + _prefix = "BQS" + _day_opt = "business_start" + + +cdef class QuarterEnd(QuarterOffset): + """ + DateOffset increments between business Quarter dates. + + startingMonth = 1 corresponds to dates like 1/31/2007, 4/30/2007, ... + startingMonth = 2 corresponds to dates like 2/28/2007, 5/31/2007, ... + startingMonth = 3 corresponds to dates like 3/31/2007, 6/30/2007, ... + """ + _outputName = "QuarterEnd" + _default_startingMonth = 3 + _prefix = "Q" + _day_opt = "end" + + +cdef class QuarterBegin(QuarterOffset): + _outputName = "QuarterBegin" + _default_startingMonth = 3 + _from_name_startingMonth = 1 + _prefix = "QS" + _day_opt = "start" + + +# ---------------------------------------------------------------------- +# Month-Based Offset Classes + cdef class MonthOffset(SingleConstructorOffset): def is_on_offset(self, dt) -> bool: if self.normalize and not is_normalized(dt): @@ -1329,6 +1456,38 @@ cdef class MonthOffset(SingleConstructorOffset): return type(dtindex)._simple_new(shifted, dtype=dtindex.dtype) +cdef class MonthEnd(MonthOffset): + """ + DateOffset of one month end. + """ + _prefix = "M" + _day_opt = "end" + + +cdef class MonthBegin(MonthOffset): + """ + DateOffset of one month at beginning. + """ + _prefix = "MS" + _day_opt = "start" + + +cdef class BusinessMonthEnd(MonthOffset): + """ + DateOffset increments between business EOM dates. + """ + _prefix = "BM" + _day_opt = "business_end" + + +cdef class BusinessMonthBegin(MonthOffset): + """ + DateOffset of one business month at beginning. + """ + _prefix = "BMS" + _day_opt = "business_start" + + # --------------------------------------------------------------------- # Special Offset Classes diff --git a/pandas/tseries/offsets.py b/pandas/tseries/offsets.py index b8e95a9c72118..cd8f956e2747f 100644 --- a/pandas/tseries/offsets.py +++ b/pandas/tseries/offsets.py @@ -1,6 +1,5 @@ from datetime import date, datetime, timedelta import operator -from typing import Optional from dateutil.easter import easter import numpy as np @@ -16,17 +15,29 @@ from pandas._libs.tslibs.offsets import ( # noqa:F401 ApplyTypeError, BaseOffset, + BQuarterBegin, + BQuarterEnd, BusinessMixin, + BusinessMonthBegin, + BusinessMonthEnd, + BYearBegin, + BYearEnd, CustomMixin, Day, Hour, Micro, Milli, Minute, + MonthBegin, + MonthEnd, Nano, + QuarterBegin, + QuarterEnd, Second, SingleConstructorOffset, Tick, + YearBegin, + YearEnd, apply_index_wraps, apply_wraps, as_datetime, @@ -790,42 +801,6 @@ def __init__( # Month-Based Offset Classes -class MonthEnd(liboffsets.MonthOffset): - """ - DateOffset of one month end. - """ - - _prefix = "M" - _day_opt = "end" - - -class MonthBegin(liboffsets.MonthOffset): - """ - DateOffset of one month at beginning. - """ - - _prefix = "MS" - _day_opt = "start" - - -class BusinessMonthEnd(liboffsets.MonthOffset): - """ - DateOffset increments between business EOM dates. - """ - - _prefix = "BM" - _day_opt = "business_end" - - -class BusinessMonthBegin(liboffsets.MonthOffset): - """ - DateOffset of one business month at beginning. - """ - - _prefix = "BMS" - _day_opt = "business_start" - - @doc(bound="bound") class _CustomBusinessMonth(CustomMixin, BusinessMixin, liboffsets.MonthOffset): """ @@ -1398,115 +1373,6 @@ def _from_name(cls, suffix=None): return cls(weekday=weekday) -# --------------------------------------------------------------------- -# Quarter-Based Offset Classes - - -class QuarterOffset(liboffsets.QuarterOffset): - """ - Quarter representation. - """ - - _default_startingMonth: Optional[int] = None - _from_name_startingMonth: Optional[int] = None - - -class BQuarterEnd(QuarterOffset): - """ - DateOffset increments between business Quarter dates. - - startingMonth = 1 corresponds to dates like 1/31/2007, 4/30/2007, ... - startingMonth = 2 corresponds to dates like 2/28/2007, 5/31/2007, ... - startingMonth = 3 corresponds to dates like 3/30/2007, 6/29/2007, ... - """ - - _outputName = "BusinessQuarterEnd" - _default_startingMonth = 3 - _from_name_startingMonth = 12 - _prefix = "BQ" - _day_opt = "business_end" - - -# TODO: This is basically the same as BQuarterEnd -class BQuarterBegin(QuarterOffset): - _outputName = "BusinessQuarterBegin" - # I suspect this is wrong for *all* of them. - # TODO: What does the above comment refer to? - _default_startingMonth = 3 - _from_name_startingMonth = 1 - _prefix = "BQS" - _day_opt = "business_start" - - -class QuarterEnd(QuarterOffset): - """ - DateOffset increments between business Quarter dates. - - startingMonth = 1 corresponds to dates like 1/31/2007, 4/30/2007, ... - startingMonth = 2 corresponds to dates like 2/28/2007, 5/31/2007, ... - startingMonth = 3 corresponds to dates like 3/31/2007, 6/30/2007, ... - """ - - _outputName = "QuarterEnd" - _default_startingMonth = 3 - _prefix = "Q" - _day_opt = "end" - - -class QuarterBegin(QuarterOffset): - _outputName = "QuarterBegin" - _default_startingMonth = 3 - _from_name_startingMonth = 1 - _prefix = "QS" - _day_opt = "start" - - -# --------------------------------------------------------------------- -# Year-Based Offset Classes - - -class BYearEnd(liboffsets.YearOffset): - """ - DateOffset increments between business EOM dates. - """ - - _outputName = "BusinessYearEnd" - _default_month = 12 - _prefix = "BA" - _day_opt = "business_end" - - -class BYearBegin(liboffsets.YearOffset): - """ - DateOffset increments between business year begin dates. - """ - - _outputName = "BusinessYearBegin" - _default_month = 1 - _prefix = "BAS" - _day_opt = "business_start" - - -class YearEnd(liboffsets.YearOffset): - """ - DateOffset increments between calendar year ends. - """ - - _default_month = 12 - _prefix = "A" - _day_opt = "end" - - -class YearBegin(liboffsets.YearOffset): - """ - DateOffset increments between calendar year begin dates. - """ - - _default_month = 1 - _prefix = "AS" - _day_opt = "start" - - # --------------------------------------------------------------------- # Special Offset Classes From cce16a1b62f148d41a317c1222227ea903a2aa13 Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Sat, 23 May 2020 13:50:05 -0700 Subject: [PATCH 2/2] remove type ignore --- pandas/tests/tseries/offsets/test_offsets_properties.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/tests/tseries/offsets/test_offsets_properties.py b/pandas/tests/tseries/offsets/test_offsets_properties.py index e6c1ef01bb0ca..81465e733da85 100644 --- a/pandas/tests/tseries/offsets/test_offsets_properties.py +++ b/pandas/tests/tseries/offsets/test_offsets_properties.py @@ -62,7 +62,7 @@ # enough runtime information (e.g. type hints) to infer how to build them. gen_yqm_offset = st.one_of( *map( - st.from_type, # type: ignore + st.from_type, [ MonthBegin, MonthEnd,