From 1018c7baeeee854e3d9151f8f7b01c391c7a4ded Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Tue, 19 May 2020 10:43:41 -0700 Subject: [PATCH 1/3] REF: Make WeekOfMonthMixin a cdef class --- pandas/_libs/tslibs/offsets.pyx | 16 ++++-- pandas/tests/tseries/offsets/test_offsets.py | 18 +++++++ pandas/tseries/offsets.py | 56 ++++++++++++++++++-- 3 files changed, 82 insertions(+), 8 deletions(-) diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx index 17ea389611b84..4d8ae565e82eb 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -455,6 +455,11 @@ cdef class BaseOffset: all_paras = self.__dict__.copy() all_paras["n"] = self.n all_paras["normalize"] = self.normalize + for key in self._attributes: + if key not in all_paras: + # cython attributes are not in __dict__ + all_paras[key] = getattr(self, key) + if 'holidays' in all_paras and not all_paras['holidays']: all_paras.pop('holidays') exclude = ['kwds', 'name', 'calendar'] @@ -551,7 +556,8 @@ cdef class BaseOffset: def _repr_attrs(self) -> str: exclude = {"n", "inc", "normalize"} attrs = [] - for attr in sorted(self.__dict__): + for attr in sorted(self._attributes): + # _attributes instead of __dict__ because cython attrs are not in __dict__ if attr.startswith("_") or attr == "kwds": continue elif attr not in exclude: @@ -1113,13 +1119,17 @@ class CustomMixin: object.__setattr__(self, "calendar", calendar) -class WeekOfMonthMixin(BaseOffset): +cdef class WeekOfMonthMixin(BaseOffset): """ Mixin for methods common to WeekOfMonth and LastWeekOfMonth. """ + + cdef readonly: + int weekday + def __init__(self, n=1, normalize=False, weekday=0): BaseOffset.__init__(self, n, normalize) - object.__setattr__(self, "weekday", weekday) + self.weekday = weekday if weekday < 0 or weekday > 6: raise ValueError(f"Day must be 0<=day<=6, got {weekday}") diff --git a/pandas/tests/tseries/offsets/test_offsets.py b/pandas/tests/tseries/offsets/test_offsets.py index f0dcef4dbc967..48d94974f4828 100644 --- a/pandas/tests/tseries/offsets/test_offsets.py +++ b/pandas/tests/tseries/offsets/test_offsets.py @@ -655,6 +655,19 @@ def test_pickle_v0_15_2(self, datapath): # tm.assert_dict_equal(offsets, read_pickle(pickle_path)) + def test_pickle_roundtrip(self, offset_types): + off = self._get_offset(offset_types) + res = tm.round_trip_pickle(off) + assert off == res + if type(off) is not DateOffset: + for attr in off._attributes: + if attr == "calendar": + # np.busdaycalendar __eq__ will return False; + # we check holidays and weekmask attrs so are OK + continue + # Make sure nothings got lost from _params (which __eq__) is based on + assert getattr(off, attr) == getattr(res, attr) + def test_onOffset_deprecated(self, offset_types): # GH#30340 use idiomatic naming off = self._get_offset(offset_types) @@ -3463,6 +3476,11 @@ def test_is_on_offset(self, case): offset = LastWeekOfMonth(weekday=weekday) assert offset.is_on_offset(dt) == expected + def test_repr(self): + assert ( + repr(LastWeekOfMonth(n=2, weekday=1)) == "<2 * LastWeekOfMonths: weekday=1>" + ) + class TestSemiMonthEnd(Base): _offset = SemiMonthEnd diff --git a/pandas/tseries/offsets.py b/pandas/tseries/offsets.py index 3dd5f2a2fc4c8..e684cc4e9161c 100644 --- a/pandas/tseries/offsets.py +++ b/pandas/tseries/offsets.py @@ -299,16 +299,56 @@ def is_on_offset(self, dt): # TODO, see #1395 return True - @cache_readonly - def _params(self): - # TODO: see if we can just write cache_readonly(BaseOffset._params.__get__) - return BaseOffset._params.__get__(self) - @cache_readonly def freqstr(self): # TODO: see if we can just write cache_readonly(BaseOffset.freqstr.__get__) return BaseOffset.freqstr.__get__(self) + def _repr_attrs(self) -> str: + # The DateOffset class differs from other classes in that members + # of self._attributes may not be defined, so we have to use __dict__ + # instead. + exclude = {"n", "inc", "normalize"} + attrs = [] + for attr in sorted(self.__dict__): + if attr.startswith("_") or attr == "kwds": + continue + elif attr not in exclude: + value = getattr(self, attr) + attrs.append(f"{attr}={value}") + + out = "" + if attrs: + out += ": " + ", ".join(attrs) + return out + + @cache_readonly + def _params(self): + """ + Returns a tuple containing all of the attributes needed to evaluate + equality between two DateOffset objects. + """ + # The DateOffset class differs from other classes in that members + # of self._attributes may not be defined, so we have to use __dict__ + # instead. + all_paras = self.__dict__.copy() + all_paras["n"] = self.n + all_paras["normalize"] = self.normalize + for key in self.__dict__: + if key not in all_paras: + # cython attributes are not in __dict__ + all_paras[key] = getattr(self, key) + + if "holidays" in all_paras and not all_paras["holidays"]: + all_paras.pop("holidays") + exclude = ["kwds", "name", "calendar"] + attrs = [ + (k, v) for k, v in all_paras.items() if (k not in exclude) and (k[0] != "_") + ] + attrs = sorted(set(attrs)) + params = tuple([str(type(self))] + attrs) + return params + class SingleConstructorMixin: @classmethod @@ -1356,6 +1396,9 @@ def __init__(self, n=1, normalize=False, week=0, weekday=0): if self.week < 0 or self.week > 3: raise ValueError(f"Week must be 0<=week<=3, got {self.week}") + def __reduce__(self): + return type(self), (self.n, self.normalize, self.week, self.weekday) + def _get_offset_day(self, other: datetime) -> int: """ Find the day in the same month as other that has the same @@ -1415,6 +1458,9 @@ def __init__(self, n=1, normalize=False, weekday=0): raise ValueError("N cannot be 0") object.__setattr__(self, "week", -1) + def __reduce__(self): + return type(self), (self.n, self.normalize, self.weekday) + def _get_offset_day(self, other: datetime) -> int: """ Find the day in the same month as other that has the same From 86966e89bbb22785bafd3ebd3c2260148550012e Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Tue, 19 May 2020 16:17:13 -0700 Subject: [PATCH 2/3] update doc --- doc/source/reference/offset_frequency.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/source/reference/offset_frequency.rst b/doc/source/reference/offset_frequency.rst index fb262af1e63f6..10f3d1620f2e1 100644 --- a/doc/source/reference/offset_frequency.rst +++ b/doc/source/reference/offset_frequency.rst @@ -579,6 +579,7 @@ Methods WeekOfMonth.is_anchored WeekOfMonth.is_on_offset WeekOfMonth.__call__ + WeekOfMonth.weekday LastWeekOfMonth --------------- @@ -599,6 +600,7 @@ Properties LastWeekOfMonth.normalize LastWeekOfMonth.rule_code LastWeekOfMonth.n + LastWeekOfMonth.weekday Methods ~~~~~~~ From 7562af636d2a8527a70b267cf17916adc6b1b77a Mon Sep 17 00:00:00 2001 From: jbrockmendel Date: Sat, 23 May 2020 11:52:21 -0700 Subject: [PATCH 3/3] Troubleshoot CI --- pandas/tseries/offsets.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/pandas/tseries/offsets.py b/pandas/tseries/offsets.py index 63e25ba1d5617..ead15fa5c0883 100644 --- a/pandas/tseries/offsets.py +++ b/pandas/tseries/offsets.py @@ -830,6 +830,22 @@ def __init__( BusinessHour.__init__(self, n, normalize, start=start, end=end, offset=offset) CustomMixin.__init__(self, weekmask, holidays, calendar) + def __reduce__(self): + # None for self.calendar bc np.busdaycalendar doesnt pickle nicely + return ( + type(self), + ( + self.n, + self.normalize, + self.weekmask, + self.holidays, + None, + self.start, + self.end, + self.offset, + ), + ) + # --------------------------------------------------------------------- # Month-Based Offset Classes