From 5645814b3885b22156ead05d6cc0dc14aaf10590 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Wed, 6 Sep 2017 11:02:02 -0700 Subject: [PATCH 1/4] Fix bug where offset.copy() != offset Add test that will currently fail Make some attributes into cache_readonlys --- pandas/tests/tseries/test_offsets.py | 4 + pandas/tseries/offsets.py | 179 ++++++++++++++++----------- 2 files changed, 113 insertions(+), 70 deletions(-) diff --git a/pandas/tests/tseries/test_offsets.py b/pandas/tests/tseries/test_offsets.py index e03b3e0a85e5e..f57be1b1ee888 100644 --- a/pandas/tests/tseries/test_offsets.py +++ b/pandas/tests/tseries/test_offsets.py @@ -1952,6 +1952,10 @@ def _check_roundtrip(obj): _check_roundtrip(self._object(2)) _check_roundtrip(self._object() * 2) + def test_copy(self): + off = self._object(weekmask='Mon Wed Fri') + assert off == off.copy() + class TestCustomBusinessMonthEnd(CustomBusinessMonthBase, Base): _object = CBMonthEnd diff --git a/pandas/tseries/offsets.py b/pandas/tseries/offsets.py index 7ccecaa84e6d6..e1387823c0d78 100644 --- a/pandas/tseries/offsets.py +++ b/pandas/tseries/offsets.py @@ -11,6 +11,7 @@ from dateutil.relativedelta import relativedelta, weekday from dateutil.easter import easter from pandas._libs import tslib, Timestamp, OutOfBoundsDatetime, Timedelta +from pandas.utils._decorators import cache_readonly import functools import operator @@ -573,9 +574,9 @@ def __setstate__(self, state): """Reconstruct an instance from a pickled state""" self.__dict__ = state if 'weekmask' in state and 'holidays' in state: - calendar, holidays = self.get_calendar(weekmask=self.weekmask, - holidays=self.holidays, - calendar=None) + calendar, holidays = _get_calendar(weekmask=self.weekmask, + holidays=self.holidays, + calendar=None) self.kwds['calendar'] = self.calendar = calendar self.kwds['holidays'] = self.holidays = holidays self.kwds['weekmask'] = state['weekmask'] @@ -978,9 +979,9 @@ def __init__(self, n=1, normalize=False, weekmask='Mon Tue Wed Thu Fri', self.normalize = normalize self.kwds = kwds self.offset = kwds.get('offset', timedelta(0)) - calendar, holidays = self.get_calendar(weekmask=weekmask, - holidays=holidays, - calendar=calendar) + calendar, holidays = _get_calendar(weekmask=weekmask, + holidays=holidays, + calendar=calendar) # CustomBusinessDay instances are identified by the # following two attributes. See DateOffset._params() # holidays, weekmask @@ -989,36 +990,6 @@ def __init__(self, n=1, normalize=False, weekmask='Mon Tue Wed Thu Fri', self.kwds['holidays'] = self.holidays = holidays self.kwds['calendar'] = self.calendar = calendar - def get_calendar(self, weekmask, holidays, calendar): - """Generate busdaycalendar""" - if isinstance(calendar, np.busdaycalendar): - if not holidays: - holidays = tuple(calendar.holidays) - elif not isinstance(holidays, tuple): - holidays = tuple(holidays) - else: - # trust that calendar.holidays and holidays are - # consistent - pass - return calendar, holidays - - if holidays is None: - holidays = [] - try: - holidays = holidays + calendar.holidays().tolist() - except AttributeError: - pass - holidays = [self._to_dt64(dt, dtype='datetime64[D]') for dt in - holidays] - holidays = tuple(sorted(holidays)) - - kwargs = {'weekmask': weekmask} - if holidays: - kwargs['holidays'] = holidays - - busdaycalendar = np.busdaycalendar(**kwargs) - return busdaycalendar, holidays - @apply_wraps def apply(self, other): if self.n <= 0: @@ -1050,25 +1021,10 @@ def apply(self, other): def apply_index(self, i): raise NotImplementedError - @staticmethod - def _to_dt64(dt, dtype='datetime64'): - # Currently - # > np.datetime64(dt.datetime(2013,5,1),dtype='datetime64[D]') - # numpy.datetime64('2013-05-01T02:00:00.000000+0200') - # Thus astype is needed to cast datetime to datetime64[D] - if getattr(dt, 'tzinfo', None) is not None: - i8 = tslib.pydt_to_i8(dt) - dt = tslib.tz_convert_single(i8, 'UTC', dt.tzinfo) - dt = Timestamp(dt) - dt = np.datetime64(dt) - if dt.dtype.name != dtype: - dt = dt.astype(dtype) - return dt - def onOffset(self, dt): if self.normalize and not _is_normalized(dt): return False - day64 = self._to_dt64(dt, 'datetime64[D]') + day64 = _to_dt64(dt, 'datetime64[D]') return np.is_busday(day64, busdaycal=self.calendar) @@ -1087,19 +1043,25 @@ def __init__(self, n=1, normalize=False, weekmask='Mon Tue Wed Thu Fri', self.n = int(n) self.normalize = normalize super(CustomBusinessHour, self).__init__(**kwds) + + calendar, holidays = _get_calendar(weekmask=weekmask, + holidays=holidays, + calendar=calendar) + self.kwds['weekmask'] = self.weekmask = weekmask + self.kwds['holidays'] = self.holidays = holidays + self.kwds['calendar'] = self.calendar = calendar + + @cache_readonly + def next_bday(self): # used for moving to next businessday if self.n >= 0: nb_offset = 1 else: nb_offset = -1 - self.next_bday = CustomBusinessDay(n=nb_offset, - weekmask=weekmask, - holidays=holidays, - calendar=calendar) - - self.kwds['weekmask'] = self.next_bday.weekmask - self.kwds['holidays'] = self.next_bday.holidays - self.kwds['calendar'] = self.next_bday.calendar + return CustomBusinessDay(n=nb_offset, + weekmask=self.weekmask, + holidays=self.holidays, + calendar=self.calendar) class MonthOffset(SingleConstructorOffset): @@ -1471,11 +1433,25 @@ def __init__(self, n=1, normalize=False, weekmask='Mon Tue Wed Thu Fri', self.normalize = normalize self.kwds = kwds self.offset = kwds.get('offset', timedelta(0)) - self.cbday = CustomBusinessDay(n=self.n, normalize=normalize, - weekmask=weekmask, holidays=holidays, - calendar=calendar, **kwds) - self.m_offset = MonthEnd(n=1, normalize=normalize, **kwds) - self.kwds['calendar'] = self.cbday.calendar # cache numpy calendar + + calendar, holidays = _get_calendar(weekmask=weekmask, + holidays=holidays, + calendar=calendar) + self.kwds['weekmask'] = self.weekmask = weekmask + self.kwds['holidays'] = self.holidays = holidays + self.kwds['calendar'] = self.calendar = calendar + + @cache_readonly + def cbday(self): + kwds = self.kwds + return CustomBusinessDay(n=self.n, normalize=self.normalize, **kwds) + + @cache_readonly + def m_offset(self): + kwds = self.kwds + kwds = {key: kwds[key] for key in kwds + if key not in ['calendar', 'weekmask', 'holidays']} + return MonthEnd(n=1, normalize=self.normalize, **kwds) @apply_wraps def apply(self, other): @@ -1531,11 +1507,27 @@ def __init__(self, n=1, normalize=False, weekmask='Mon Tue Wed Thu Fri', self.normalize = normalize self.kwds = kwds self.offset = kwds.get('offset', timedelta(0)) - self.cbday = CustomBusinessDay(n=self.n, normalize=normalize, - weekmask=weekmask, holidays=holidays, - calendar=calendar, **kwds) - self.m_offset = MonthBegin(n=1, normalize=normalize, **kwds) - self.kwds['calendar'] = self.cbday.calendar # cache numpy calendar + + # _get_calendar does validation and possible transformation + # of calendar and holidays. + calendar, holidays = _get_calendar(weekmask=weekmask, + holidays=holidays, + calendar=calendar) + kwds['calendar'] = self.calendar = calendar + kwds['weekmask'] = self.weekmask = weekmask + kwds['holidays'] = self.holidays = holidays + + @cache_readonly + def cbday(self): + kwds = self.kwds + return CustomBusinessDay(n=self.n, normalize=self.normalize, **kwds) + + @cache_readonly + def m_offset(self): + kwds = self.kwds + kwds = {key: kwds[key] for key in kwds + if key not in ['calendar', 'weekmask', 'holidays']} + return MonthBegin(n=1, normalize=self.normalize, **kwds) @apply_wraps def apply(self, other): @@ -2861,6 +2853,53 @@ class Nano(Tick): CBMonthBegin = CustomBusinessMonthBegin CDay = CustomBusinessDay +# --------------------------------------------------------------------- +# Business Calendar helpers + +def _get_calendar(weekmask, holidays, calendar): + """Generate busdaycalendar""" + if isinstance(calendar, np.busdaycalendar): + if not holidays: + holidays = tuple(calendar.holidays) + elif not isinstance(holidays, tuple): + holidays = tuple(holidays) + else: + # trust that calendar.holidays and holidays are + # consistent + pass + return calendar, holidays + + if holidays is None: + holidays = [] + try: + holidays = holidays + calendar.holidays().tolist() + except AttributeError: + pass + holidays = [_to_dt64(dt, dtype='datetime64[D]') for dt in holidays] + holidays = tuple(sorted(holidays)) + + kwargs = {'weekmask': weekmask} + if holidays: + kwargs['holidays'] = holidays + + busdaycalendar = np.busdaycalendar(**kwargs) + return busdaycalendar, holidays + + +def _to_dt64(dt, dtype='datetime64'): + # Currently + # > np.datetime64(dt.datetime(2013,5,1),dtype='datetime64[D]') + # numpy.datetime64('2013-05-01T02:00:00.000000+0200') + # Thus astype is needed to cast datetime to datetime64[D] + if getattr(dt, 'tzinfo', None) is not None: + i8 = tslib.pydt_to_i8(dt) + dt = tslib.tz_convert_single(i8, 'UTC', dt.tzinfo) + dt = Timestamp(dt) + dt = np.datetime64(dt) + if dt.dtype.name != dtype: + dt = dt.astype(dtype) + return dt + def _get_firstbday(wkday): """ From 50084051e6640bc24483200b18cc6b4e7dc3261b Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Wed, 6 Sep 2017 11:06:09 -0700 Subject: [PATCH 2/4] flake8 whitespace fixup --- pandas/tseries/offsets.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pandas/tseries/offsets.py b/pandas/tseries/offsets.py index e1387823c0d78..ccb8ca8624f6e 100644 --- a/pandas/tseries/offsets.py +++ b/pandas/tseries/offsets.py @@ -2856,6 +2856,7 @@ class Nano(Tick): # --------------------------------------------------------------------- # Business Calendar helpers + def _get_calendar(weekmask, holidays, calendar): """Generate busdaycalendar""" if isinstance(calendar, np.busdaycalendar): From 3b4b854a55c5780ed199d4f7569810c11c76e7ea Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Wed, 6 Sep 2017 20:15:07 -0700 Subject: [PATCH 3/4] Add GH reference to test per reviewer request --- pandas/tests/tseries/test_offsets.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pandas/tests/tseries/test_offsets.py b/pandas/tests/tseries/test_offsets.py index f57be1b1ee888..3239fff22ef50 100644 --- a/pandas/tests/tseries/test_offsets.py +++ b/pandas/tests/tseries/test_offsets.py @@ -1953,6 +1953,7 @@ def _check_roundtrip(obj): _check_roundtrip(self._object() * 2) def test_copy(self): + # GH 17452 off = self._object(weekmask='Mon Wed Fri') assert off == off.copy() From db8c18ea908f2d4a350f42ad070ba1dcc228b6cb Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Thu, 14 Sep 2017 09:17:25 -0700 Subject: [PATCH 4/4] typo fixup broken import --- pandas/tseries/offsets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/tseries/offsets.py b/pandas/tseries/offsets.py index ccb8ca8624f6e..d82a3a209af6b 100644 --- a/pandas/tseries/offsets.py +++ b/pandas/tseries/offsets.py @@ -11,7 +11,7 @@ from dateutil.relativedelta import relativedelta, weekday from dateutil.easter import easter from pandas._libs import tslib, Timestamp, OutOfBoundsDatetime, Timedelta -from pandas.utils._decorators import cache_readonly +from pandas.util._decorators import cache_readonly import functools import operator