From b9fe60e26156b6884bc5c0c72a96662f4317040c Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Fri, 10 Nov 2017 20:08:06 -0800 Subject: [PATCH 01/24] Patch __init__ to prevent passing invalid kwds --- pandas/tests/tseries/test_offsets.py | 27 ++++++++++++++++++++++++++- pandas/tseries/offsets.py | 23 +++++++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/pandas/tests/tseries/test_offsets.py b/pandas/tests/tseries/test_offsets.py index 4fd3bba01602f..4df41a6b3d4a7 100644 --- a/pandas/tests/tseries/test_offsets.py +++ b/pandas/tests/tseries/test_offsets.py @@ -1,3 +1,4 @@ +import inspect import os from distutils.version import LooseVersion from datetime import date, datetime, timedelta @@ -17,7 +18,7 @@ get_offset, get_standard_freq) from pandas.core.indexes.datetimes import ( _to_m8, DatetimeIndex, _daterange_cache) -from pandas._libs.tslibs.offsets import WeekDay, CacheableOffset +from pandas._libs.tslibs.offsets import WeekDay, CacheableOffset, _rd_kwds from pandas.tseries.offsets import (BDay, CDay, BQuarterEnd, BMonthEnd, BusinessHour, WeekOfMonth, CBMonthEnd, CustomBusinessHour, @@ -4899,3 +4900,27 @@ def test_all_offset_classes(self): first = Timestamp(test_values[0], tz='US/Eastern') + offset() second = Timestamp(test_values[1], tz='US/Eastern') assert first == second + + +# --------------------------------------------------------------------- + +offset_classes = [getattr(offsets, x) for x in dir(offsets)] +offset_classes = [x for x in offset_classes if inspect.isclass(x) and + issubclass(x, DateOffset)] + + +def test_valid_attributes(): + # check that we cannot create e.g. MonthEnd(weeks=3) + month_classes = [x for x in offset_classes if + issubclass(x, offsets.MonthOffset)] + + for cls in month_classes: + for kwd in _rd_kwds: + with pytest.raises(TypeError): + cls(**{kwd: 3}) + + tick_classes = [x for x in offset_classes if issubclass(x, offsets.Tick)] + for cls in tick_classes: + for kwd in _rd_kwds: + with pytest.raises(TypeError): + cls(**{kwd: 4}) diff --git a/pandas/tseries/offsets.py b/pandas/tseries/offsets.py index 4dc26f4dd69e2..9b7835e339f86 100644 --- a/pandas/tseries/offsets.py +++ b/pandas/tseries/offsets.py @@ -913,6 +913,13 @@ def next_bday(self): class MonthOffset(SingleConstructorOffset): _adjust_dst = True + def __init__(self, n=1, normalize=False): + self.n = n + self.normalize = normalize + self._offset = timedelta(1) + self._use_relativedelta = False + self.kwds = {} + @property def name(self): if self.isAnchored: @@ -2471,6 +2478,13 @@ class Easter(DateOffset): """ _adjust_dst = True + def __init__(self, n=1, normalize=False): + self.n = n + self.normalize = normalize + self._offset = timedelta(1) + self._use_relativedelta = False + self.kwds = {} + @apply_wraps def apply(self, other): currentEaster = easter(other.year) @@ -2515,6 +2529,14 @@ class Tick(SingleConstructorOffset): _inc = Timedelta(microseconds=1000) _prefix = 'undefined' + def __init__(self, n=1, normalize=False): + # TODO: do Tick classes with normalize=True make sense? + self.n = n + self.normalize = normalize + self._offset = timedelta(1) + self._use_relativedelta = False + self.kwds = {} + __gt__ = _tick_comp(operator.gt) __ge__ = _tick_comp(operator.ge) __lt__ = _tick_comp(operator.lt) @@ -2573,6 +2595,7 @@ def delta(self): def nanos(self): return delta_to_nanoseconds(self.delta) + # TODO: Should Tick have its own apply_index? def apply(self, other): # Timestamp can handle tz and nano sec, thus no need to use apply_wraps if isinstance(other, Timestamp): From f10a1c673391dbba4277478fa467e444cba46ed2 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Sat, 11 Nov 2017 09:27:46 -0800 Subject: [PATCH 02/24] cast n to integer, assert equality --- pandas/tseries/offsets.py | 36 +++++++++++++++++++++++++++--------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/pandas/tseries/offsets.py b/pandas/tseries/offsets.py index 9b7835e339f86..71c7c5877c3c7 100644 --- a/pandas/tseries/offsets.py +++ b/pandas/tseries/offsets.py @@ -164,6 +164,7 @@ def __add__(date): normalize = False def __init__(self, n=1, normalize=False, **kwds): + assert n == int(n) self.n = int(n) self.normalize = normalize self.kwds = kwds @@ -471,6 +472,7 @@ class BusinessDay(BusinessMixin, SingleConstructorOffset): _adjust_dst = True def __init__(self, n=1, normalize=False, offset=timedelta(0)): + assert n == int(n) self.n = int(n) self.normalize = normalize self.kwds = {'offset': offset} @@ -780,6 +782,7 @@ class BusinessHour(BusinessHourMixin, SingleConstructorOffset): def __init__(self, n=1, normalize=False, start='09:00', end='17:00', offset=timedelta(0)): + assert n == int(n) self.n = int(n) self.normalize = normalize super(BusinessHour, self).__init__(start=start, end=end, offset=offset) @@ -817,6 +820,7 @@ class CustomBusinessDay(BusinessDay): def __init__(self, n=1, normalize=False, weekmask='Mon Tue Wed Thu Fri', holidays=None, calendar=None, offset=timedelta(0)): + assert n == int(n) self.n = int(n) self.normalize = normalize self._offset = offset @@ -885,6 +889,7 @@ class CustomBusinessHour(BusinessHourMixin, SingleConstructorOffset): def __init__(self, n=1, normalize=False, weekmask='Mon Tue Wed Thu Fri', holidays=None, calendar=None, start='09:00', end='17:00', offset=timedelta(0)): + assert n == int(n) self.n = int(n) self.normalize = normalize super(CustomBusinessHour, self).__init__(start=start, @@ -914,7 +919,8 @@ class MonthOffset(SingleConstructorOffset): _adjust_dst = True def __init__(self, n=1, normalize=False): - self.n = n + assert n == int(n) + self.n = int(n) self.normalize = normalize self._offset = timedelta(1) self._use_relativedelta = False @@ -994,6 +1000,8 @@ def __init__(self, n=1, normalize=False, day_of_month=None): msg = 'day_of_month must be {min}<=day_of_month<=27, got {day}' raise ValueError(msg.format(min=self._min_day_of_month, day=self.day_of_month)) + + assert n == int(n) self.n = int(n) self.normalize = normalize self.kwds = {'day_of_month': self.day_of_month} @@ -1270,6 +1278,7 @@ class CustomBusinessMonthEnd(BusinessMixin, MonthOffset): def __init__(self, n=1, normalize=False, weekmask='Mon Tue Wed Thu Fri', holidays=None, calendar=None, offset=timedelta(0)): + assert n == int(n) self.n = int(n) self.normalize = normalize self._offset = offset @@ -1341,6 +1350,7 @@ class CustomBusinessMonthBegin(BusinessMixin, MonthOffset): def __init__(self, n=1, normalize=False, weekmask='Mon Tue Wed Thu Fri', holidays=None, calendar=None, offset=timedelta(0)): + assert n == int(n) self.n = int(n) self.normalize = normalize self._offset = offset @@ -1405,7 +1415,8 @@ class Week(EndMixin, DateOffset): _prefix = 'W' def __init__(self, n=1, normalize=False, weekday=None): - self.n = n + assert n == int(n) + self.n = int(n) self.normalize = normalize self.weekday = weekday @@ -1496,7 +1507,8 @@ class WeekOfMonth(DateOffset): _adjust_dst = True def __init__(self, n=1, normalize=False, week=None, weekday=None): - self.n = n + assert n == int(n) + self.n = int(n) self.normalize = normalize self.weekday = weekday self.week = week @@ -1592,7 +1604,8 @@ class LastWeekOfMonth(DateOffset): _prefix = 'LWOM' def __init__(self, n=1, normalize=False, weekday=None): - self.n = n + assert n == int(n) + self.n = int(n) self.normalize = normalize self.weekday = weekday @@ -1665,7 +1678,8 @@ class QuarterOffset(DateOffset): # point def __init__(self, n=1, normalize=False, startingMonth=None): - self.n = n + assert n == int(n) + self.n = int(n) self.normalize = normalize if startingMonth is None: startingMonth = self._default_startingMonth @@ -2101,7 +2115,8 @@ class FY5253(DateOffset): def __init__(self, n=1, normalize=False, weekday=0, startingMonth=1, variation="nearest"): - self.n = n + assert n == int(n) + self.n = int(n) self.normalize = normalize self.startingMonth = startingMonth self.weekday = weekday @@ -2351,7 +2366,8 @@ class FY5253Quarter(DateOffset): def __init__(self, n=1, normalize=False, weekday=0, startingMonth=1, qtr_with_extra_week=1, variation="nearest"): - self.n = n + assert n == int(n) + self.n = int(n) self.normalize = normalize self.weekday = weekday @@ -2479,7 +2495,8 @@ class Easter(DateOffset): _adjust_dst = True def __init__(self, n=1, normalize=False): - self.n = n + assert n == int(n) + self.n = int(n) self.normalize = normalize self._offset = timedelta(1) self._use_relativedelta = False @@ -2531,7 +2548,8 @@ class Tick(SingleConstructorOffset): def __init__(self, n=1, normalize=False): # TODO: do Tick classes with normalize=True make sense? - self.n = n + assert n == int(n) + self.n = int(n) self.normalize = normalize self._offset = timedelta(1) self._use_relativedelta = False From 807d769065d619c6a674eb1415461ed5eea290e8 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Sat, 11 Nov 2017 09:30:01 -0800 Subject: [PATCH 03/24] whatsnew note --- doc/source/whatsnew/v0.22.0.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/source/whatsnew/v0.22.0.txt b/doc/source/whatsnew/v0.22.0.txt index 50f10efb07484..633d706249038 100644 --- a/doc/source/whatsnew/v0.22.0.txt +++ b/doc/source/whatsnew/v0.22.0.txt @@ -45,6 +45,7 @@ Other API Changes - :class:`Timestamp` will no longer silently ignore unused or invalid ``tz`` or ``tzinfo`` keyword arguments (:issue:`17690`) - :class:`Timestamp` will no longer silently ignore invalid ``freq`` arguments (:issue:`5168`) - :class:`CacheableOffset` and :class:`WeekDay` are no longer available in the ``pandas.tseries.offsets`` module (:issue:`17830`) +- Restricted DateOffset keyword arguments. Previously, ``DateOffset`` subclasses allowed arbitrary keyword arguments which could lead to unexpected behavior. Now, only valid arguments will be accepted. (:issue:`17176`,:issue:`18226`). .. _whatsnew_0220.deprecations: From 687e3b7946a6d8554872ce347c7773ece0aa80a6 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Sat, 11 Nov 2017 10:00:10 -0800 Subject: [PATCH 04/24] parameterize tests, define fixture where it is used --- pandas/tests/tseries/conftest.py | 6 ----- pandas/tests/tseries/test_offsets.py | 36 +++++++++++++++------------- 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/pandas/tests/tseries/conftest.py b/pandas/tests/tseries/conftest.py index 25446c24b28c0..fc1ecf21c5446 100644 --- a/pandas/tests/tseries/conftest.py +++ b/pandas/tests/tseries/conftest.py @@ -1,10 +1,4 @@ import pytest -import pandas.tseries.offsets as offsets - - -@pytest.fixture(params=[getattr(offsets, o) for o in offsets.__all__]) -def offset_types(request): - return request.param @pytest.fixture(params=[None, 'UTC', 'Asia/Tokyo', 'US/Eastern', diff --git a/pandas/tests/tseries/test_offsets.py b/pandas/tests/tseries/test_offsets.py index 4df41a6b3d4a7..c6303184d64cd 100644 --- a/pandas/tests/tseries/test_offsets.py +++ b/pandas/tests/tseries/test_offsets.py @@ -42,6 +42,16 @@ from pandas.tseries.holiday import USFederalHolidayCalendar +offset_classes = [getattr(offsets, x) for x in dir(offsets)] +offset_classes = [x for x in offset_classes if inspect.isclass(x) and + issubclass(x, DateOffset)] + + +@pytest.fixture(params=offset_classes) +def offset_types(request): + return request.param + + def test_monthrange(): import calendar for y in range(2000, 2013): @@ -4904,23 +4914,15 @@ def test_all_offset_classes(self): # --------------------------------------------------------------------- -offset_classes = [getattr(offsets, x) for x in dir(offsets)] -offset_classes = [x for x in offset_classes if inspect.isclass(x) and - issubclass(x, DateOffset)] +month_classes = [x for x in offset_classes if + issubclass(x, offsets.MonthOffset)] + +tick_classes = [x for x in offset_classes if issubclass(x, offsets.Tick)] -def test_valid_attributes(): +@pytest.parametrize('cls', month_classes+tick_classes) +@pytest.parametrize('kwd', _rd_kwds) +def test_valid_attributes(kwd, cls): # check that we cannot create e.g. MonthEnd(weeks=3) - month_classes = [x for x in offset_classes if - issubclass(x, offsets.MonthOffset)] - - for cls in month_classes: - for kwd in _rd_kwds: - with pytest.raises(TypeError): - cls(**{kwd: 3}) - - tick_classes = [x for x in offset_classes if issubclass(x, offsets.Tick)] - for cls in tick_classes: - for kwd in _rd_kwds: - with pytest.raises(TypeError): - cls(**{kwd: 4}) + with pytest.raises(TypeError): + cls(**{kwd: 3}) From d5443cac8637a56b8be1cf143469fd5fe9448ff0 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Sat, 11 Nov 2017 11:41:04 -0800 Subject: [PATCH 05/24] fixup --- pandas/tests/tseries/test_offsets.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pandas/tests/tseries/test_offsets.py b/pandas/tests/tseries/test_offsets.py index c6303184d64cd..e0c523f83c74b 100644 --- a/pandas/tests/tseries/test_offsets.py +++ b/pandas/tests/tseries/test_offsets.py @@ -4920,8 +4920,8 @@ def test_all_offset_classes(self): tick_classes = [x for x in offset_classes if issubclass(x, offsets.Tick)] -@pytest.parametrize('cls', month_classes+tick_classes) -@pytest.parametrize('kwd', _rd_kwds) +@pytest.mark.parametrize('cls', month_classes+tick_classes) +@pytest.mark.parametrize('kwd', _rd_kwds) def test_valid_attributes(kwd, cls): # check that we cannot create e.g. MonthEnd(weeks=3) with pytest.raises(TypeError): From 5e5e0c059e8aead213da3ee7b0a2029eeda029d1 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Sat, 11 Nov 2017 14:00:50 -0800 Subject: [PATCH 06/24] exclude base classes from testing --- pandas/tests/tseries/test_offsets.py | 6 +++++- pandas/tseries/offsets.py | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/pandas/tests/tseries/test_offsets.py b/pandas/tests/tseries/test_offsets.py index e0c523f83c74b..d7a0dc18d6f5c 100644 --- a/pandas/tests/tseries/test_offsets.py +++ b/pandas/tests/tseries/test_offsets.py @@ -44,7 +44,11 @@ offset_classes = [getattr(offsets, x) for x in dir(offsets)] offset_classes = [x for x in offset_classes if inspect.isclass(x) and - issubclass(x, DateOffset)] + issubclass(x, DateOffset) and + x not in [offsets.YearOffset, offsets.Tick, + offsets.SingleConstructorOffset, + offsets.SemiMonthOffset, + offsets.QuarterOffset, offsets.MonthOffset]] @pytest.fixture(params=offset_classes) diff --git a/pandas/tseries/offsets.py b/pandas/tseries/offsets.py index 71c7c5877c3c7..c1afca46a4feb 100644 --- a/pandas/tseries/offsets.py +++ b/pandas/tseries/offsets.py @@ -1686,6 +1686,8 @@ def __init__(self, n=1, normalize=False, startingMonth=None): self.startingMonth = startingMonth self.kwds = {'startingMonth': startingMonth} + self._offset = timedelta(1) + self._use_relativedelta = False def isAnchored(self): return (self.n == 1 and self.startingMonth is not None) From 17f7b5aa77921054ad907a562496e7868787f028 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Sat, 11 Nov 2017 14:01:46 -0800 Subject: [PATCH 07/24] exclude base classes from testing --- pandas/tests/tseries/test_offsets.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/tests/tseries/test_offsets.py b/pandas/tests/tseries/test_offsets.py index d7a0dc18d6f5c..78cec0ecb9210 100644 --- a/pandas/tests/tseries/test_offsets.py +++ b/pandas/tests/tseries/test_offsets.py @@ -4924,7 +4924,7 @@ def test_all_offset_classes(self): tick_classes = [x for x in offset_classes if issubclass(x, offsets.Tick)] -@pytest.mark.parametrize('cls', month_classes+tick_classes) +@pytest.mark.parametrize('cls', month_classes + tick_classes) @pytest.mark.parametrize('kwd', _rd_kwds) def test_valid_attributes(kwd, cls): # check that we cannot create e.g. MonthEnd(weeks=3) From 3834ef87f3f0dc7ec4567f4a977e049c553ca1d2 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Sat, 11 Nov 2017 21:12:00 -0800 Subject: [PATCH 08/24] dummy commit to force CI --- pandas/tseries/offsets.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pandas/tseries/offsets.py b/pandas/tseries/offsets.py index c1afca46a4feb..cc9bd3e473f2c 100644 --- a/pandas/tseries/offsets.py +++ b/pandas/tseries/offsets.py @@ -1,5 +1,8 @@ # -*- coding: utf-8 -*- from datetime import date, datetime, timedelta +import functools +import operator + from pandas.compat import range from pandas import compat import numpy as np @@ -26,8 +29,6 @@ BeginMixin, EndMixin, BaseOffset) -import functools -import operator __all__ = ['Day', 'BusinessDay', 'BDay', 'CustomBusinessDay', 'CDay', 'CBMonthEnd', 'CBMonthBegin', From 1c54e962d2ceac419c0d8e79003e532989fb3ba2 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Sun, 12 Nov 2017 08:33:06 -0800 Subject: [PATCH 09/24] edits per reviewer suggestions --- pandas/tests/tseries/conftest.py | 6 ++++++ pandas/tests/tseries/test_offsets.py | 22 ++++++++-------------- pandas/tseries/offsets.py | 17 ----------------- 3 files changed, 14 insertions(+), 31 deletions(-) diff --git a/pandas/tests/tseries/conftest.py b/pandas/tests/tseries/conftest.py index fc1ecf21c5446..25446c24b28c0 100644 --- a/pandas/tests/tseries/conftest.py +++ b/pandas/tests/tseries/conftest.py @@ -1,4 +1,10 @@ import pytest +import pandas.tseries.offsets as offsets + + +@pytest.fixture(params=[getattr(offsets, o) for o in offsets.__all__]) +def offset_types(request): + return request.param @pytest.fixture(params=[None, 'UTC', 'Asia/Tokyo', 'US/Eastern', diff --git a/pandas/tests/tseries/test_offsets.py b/pandas/tests/tseries/test_offsets.py index 78cec0ecb9210..e0c12aad70b14 100644 --- a/pandas/tests/tseries/test_offsets.py +++ b/pandas/tests/tseries/test_offsets.py @@ -42,20 +42,6 @@ from pandas.tseries.holiday import USFederalHolidayCalendar -offset_classes = [getattr(offsets, x) for x in dir(offsets)] -offset_classes = [x for x in offset_classes if inspect.isclass(x) and - issubclass(x, DateOffset) and - x not in [offsets.YearOffset, offsets.Tick, - offsets.SingleConstructorOffset, - offsets.SemiMonthOffset, - offsets.QuarterOffset, offsets.MonthOffset]] - - -@pytest.fixture(params=offset_classes) -def offset_types(request): - return request.param - - def test_monthrange(): import calendar for y in range(2000, 2013): @@ -4918,6 +4904,14 @@ def test_all_offset_classes(self): # --------------------------------------------------------------------- +offset_classes = [getattr(offsets, x) for x in dir(offsets)] +offset_classes = [x for x in offset_classes if inspect.isclass(x) and + issubclass(x, DateOffset) and + x not in [offsets.YearOffset, offsets.Tick, + offsets.SingleConstructorOffset, + offsets.SemiMonthOffset, + offsets.QuarterOffset, offsets.MonthOffset]] + month_classes = [x for x in offset_classes if issubclass(x, offsets.MonthOffset)] diff --git a/pandas/tseries/offsets.py b/pandas/tseries/offsets.py index cc9bd3e473f2c..6ffdafa53e843 100644 --- a/pandas/tseries/offsets.py +++ b/pandas/tseries/offsets.py @@ -165,7 +165,6 @@ def __add__(date): normalize = False def __init__(self, n=1, normalize=False, **kwds): - assert n == int(n) self.n = int(n) self.normalize = normalize self.kwds = kwds @@ -473,7 +472,6 @@ class BusinessDay(BusinessMixin, SingleConstructorOffset): _adjust_dst = True def __init__(self, n=1, normalize=False, offset=timedelta(0)): - assert n == int(n) self.n = int(n) self.normalize = normalize self.kwds = {'offset': offset} @@ -783,7 +781,6 @@ class BusinessHour(BusinessHourMixin, SingleConstructorOffset): def __init__(self, n=1, normalize=False, start='09:00', end='17:00', offset=timedelta(0)): - assert n == int(n) self.n = int(n) self.normalize = normalize super(BusinessHour, self).__init__(start=start, end=end, offset=offset) @@ -821,7 +818,6 @@ class CustomBusinessDay(BusinessDay): def __init__(self, n=1, normalize=False, weekmask='Mon Tue Wed Thu Fri', holidays=None, calendar=None, offset=timedelta(0)): - assert n == int(n) self.n = int(n) self.normalize = normalize self._offset = offset @@ -890,7 +886,6 @@ class CustomBusinessHour(BusinessHourMixin, SingleConstructorOffset): def __init__(self, n=1, normalize=False, weekmask='Mon Tue Wed Thu Fri', holidays=None, calendar=None, start='09:00', end='17:00', offset=timedelta(0)): - assert n == int(n) self.n = int(n) self.normalize = normalize super(CustomBusinessHour, self).__init__(start=start, @@ -920,7 +915,6 @@ class MonthOffset(SingleConstructorOffset): _adjust_dst = True def __init__(self, n=1, normalize=False): - assert n == int(n) self.n = int(n) self.normalize = normalize self._offset = timedelta(1) @@ -1002,7 +996,6 @@ def __init__(self, n=1, normalize=False, day_of_month=None): raise ValueError(msg.format(min=self._min_day_of_month, day=self.day_of_month)) - assert n == int(n) self.n = int(n) self.normalize = normalize self.kwds = {'day_of_month': self.day_of_month} @@ -1279,7 +1272,6 @@ class CustomBusinessMonthEnd(BusinessMixin, MonthOffset): def __init__(self, n=1, normalize=False, weekmask='Mon Tue Wed Thu Fri', holidays=None, calendar=None, offset=timedelta(0)): - assert n == int(n) self.n = int(n) self.normalize = normalize self._offset = offset @@ -1351,7 +1343,6 @@ class CustomBusinessMonthBegin(BusinessMixin, MonthOffset): def __init__(self, n=1, normalize=False, weekmask='Mon Tue Wed Thu Fri', holidays=None, calendar=None, offset=timedelta(0)): - assert n == int(n) self.n = int(n) self.normalize = normalize self._offset = offset @@ -1416,7 +1407,6 @@ class Week(EndMixin, DateOffset): _prefix = 'W' def __init__(self, n=1, normalize=False, weekday=None): - assert n == int(n) self.n = int(n) self.normalize = normalize self.weekday = weekday @@ -1508,7 +1498,6 @@ class WeekOfMonth(DateOffset): _adjust_dst = True def __init__(self, n=1, normalize=False, week=None, weekday=None): - assert n == int(n) self.n = int(n) self.normalize = normalize self.weekday = weekday @@ -1605,7 +1594,6 @@ class LastWeekOfMonth(DateOffset): _prefix = 'LWOM' def __init__(self, n=1, normalize=False, weekday=None): - assert n == int(n) self.n = int(n) self.normalize = normalize self.weekday = weekday @@ -1679,7 +1667,6 @@ class QuarterOffset(DateOffset): # point def __init__(self, n=1, normalize=False, startingMonth=None): - assert n == int(n) self.n = int(n) self.normalize = normalize if startingMonth is None: @@ -2118,7 +2105,6 @@ class FY5253(DateOffset): def __init__(self, n=1, normalize=False, weekday=0, startingMonth=1, variation="nearest"): - assert n == int(n) self.n = int(n) self.normalize = normalize self.startingMonth = startingMonth @@ -2369,7 +2355,6 @@ class FY5253Quarter(DateOffset): def __init__(self, n=1, normalize=False, weekday=0, startingMonth=1, qtr_with_extra_week=1, variation="nearest"): - assert n == int(n) self.n = int(n) self.normalize = normalize @@ -2498,7 +2483,6 @@ class Easter(DateOffset): _adjust_dst = True def __init__(self, n=1, normalize=False): - assert n == int(n) self.n = int(n) self.normalize = normalize self._offset = timedelta(1) @@ -2551,7 +2535,6 @@ class Tick(SingleConstructorOffset): def __init__(self, n=1, normalize=False): # TODO: do Tick classes with normalize=True make sense? - assert n == int(n) self.n = int(n) self.normalize = normalize self._offset = timedelta(1) From 0e37a24f63296206bea66d94be5798d44e1383b4 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Sun, 12 Nov 2017 08:34:17 -0800 Subject: [PATCH 10/24] whatsnew whitespace --- doc/source/whatsnew/v0.22.0.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/source/whatsnew/v0.22.0.txt b/doc/source/whatsnew/v0.22.0.txt index 633d706249038..e1a911bedf6db 100644 --- a/doc/source/whatsnew/v0.22.0.txt +++ b/doc/source/whatsnew/v0.22.0.txt @@ -45,7 +45,7 @@ Other API Changes - :class:`Timestamp` will no longer silently ignore unused or invalid ``tz`` or ``tzinfo`` keyword arguments (:issue:`17690`) - :class:`Timestamp` will no longer silently ignore invalid ``freq`` arguments (:issue:`5168`) - :class:`CacheableOffset` and :class:`WeekDay` are no longer available in the ``pandas.tseries.offsets`` module (:issue:`17830`) -- Restricted DateOffset keyword arguments. Previously, ``DateOffset`` subclasses allowed arbitrary keyword arguments which could lead to unexpected behavior. Now, only valid arguments will be accepted. (:issue:`17176`,:issue:`18226`). +- Restricted DateOffset keyword arguments. Previously, ``DateOffset`` subclasses allowed arbitrary keyword arguments which could lead to unexpected behavior. Now, only valid arguments will be accepted. (:issue:`17176`, :issue:`18226`). .. _whatsnew_0220.deprecations: From d0ff381cff59c3caca273f30000367a6896a3e90 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Mon, 13 Nov 2017 12:06:38 -0800 Subject: [PATCH 11/24] whitespace fixup --- pandas/tests/tseries/offsets/test_offsets.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pandas/tests/tseries/offsets/test_offsets.py b/pandas/tests/tseries/offsets/test_offsets.py index fe8115a1e9085..016697199ba7a 100644 --- a/pandas/tests/tseries/offsets/test_offsets.py +++ b/pandas/tests/tseries/offsets/test_offsets.py @@ -4685,6 +4685,7 @@ def test_all_offset_classes(self, tup): second = Timestamp(test_values[1], tz='US/Eastern') assert first == second + # --------------------------------------------------------------------- offset_classes = [getattr(offsets, x) for x in dir(offsets)] From 6a9233ecebd0741400d6d61fda4fb91dd77112eb Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Wed, 15 Nov 2017 09:12:49 -0800 Subject: [PATCH 12/24] break up hour test to debug appveyor error (segfault?) --- pandas/tests/tseries/offsets/test_ticks.py | 48 +++++++++++++++------- 1 file changed, 34 insertions(+), 14 deletions(-) diff --git a/pandas/tests/tseries/offsets/test_ticks.py b/pandas/tests/tseries/offsets/test_ticks.py index 24033d4ff6cbd..69cf39feaf78a 100644 --- a/pandas/tests/tseries/offsets/test_ticks.py +++ b/pandas/tests/tseries/offsets/test_ticks.py @@ -38,20 +38,40 @@ def test_delta_to_tick(): # --------------------------------------------------------------------- -def test_Hour(): - assert_offset_equal(Hour(), - datetime(2010, 1, 1), datetime(2010, 1, 1, 1)) - assert_offset_equal(Hour(-1), - datetime(2010, 1, 1, 1), datetime(2010, 1, 1)) - assert_offset_equal(2 * Hour(), - datetime(2010, 1, 1), datetime(2010, 1, 1, 2)) - assert_offset_equal(-1 * Hour(), - datetime(2010, 1, 1, 1), datetime(2010, 1, 1)) - - assert Hour(3) + Hour(2) == Hour(5) - assert Hour(3) - Hour(2) == Hour() - - assert Hour(4) != Hour(1) +class TestHour(object): + def test_hour_add_hour(self): + assert Hour(3) + Hour(2) == Hour(5) + + def test_hour_sub_hour(self): + assert Hour(3) - Hour(2) == Hour() + + def test_hour_neq_hours(self): + assert Hour(4) != Hour(1) + + def test_neg_hour(self): + assert_offset_equal(-1 * Hour(), + datetime(2010, 1, 1, 1), datetime(2010, 1, 1)) + + def test_negative_n(self): + assert_offset_equal(Hour(-1), + datetime(2010, 1, 1, 1), + datetime(2010, 1, 1)) + + def test_one_hour(self): + v1 = Hour() + v2 = Hour(n=1) + assert v1 == v2 + assert v1 + datetime(2010, 1, 1) == v2 + datetime(2010, 1, 1) + assert_offset_equal(Hour(), + datetime(2010, 1, 1), datetime(2010, 1, 1, 1)) + + def test_multiple_hours(self): + v1 = Hour(n=2) + v2 = 2 * Hour() + assert v1 == v2 + assert v1 + datetime(2010, 1, 1) == v2 + datetime(2010, 1, 1) + assert_offset_equal(2 * Hour(), + datetime(2010, 1, 1), datetime(2010, 1, 1, 2)) def test_Minute(): From 4406df87ffe9afe49cae51ba98646c2b090fe926 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Wed, 15 Nov 2017 21:19:34 -0800 Subject: [PATCH 13/24] break down segfaulting test to debug --- pandas/tests/tseries/offsets/test_ticks.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/pandas/tests/tseries/offsets/test_ticks.py b/pandas/tests/tseries/offsets/test_ticks.py index 69cf39feaf78a..9a3856e614eab 100644 --- a/pandas/tests/tseries/offsets/test_ticks.py +++ b/pandas/tests/tseries/offsets/test_ticks.py @@ -39,8 +39,24 @@ def test_delta_to_tick(): class TestHour(object): - def test_hour_add_hour(self): - assert Hour(3) + Hour(2) == Hour(5) + def test_construct_hour3(self): + # smoketest to debug appveyor segfault + Hour(3) + + def test_construct_hour2(self): + # smoketest to debug appveyor segfault + Hour(2) + + def test_construct_hour5(self): + # smoketest to debug appveyor segfault + Hour(5) + + def test_hour3_add_hour2(self): + # smoketest to debug appveyor segfault + Hour(3) + Hour(2) + + # def test_hour_add_hour(self): + # assert Hour(3) + Hour(2) == Hour(5) def test_hour_sub_hour(self): assert Hour(3) - Hour(2) == Hour() From 573abb6ae5f21cc6223d0ca1cd646da79d75bb12 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Thu, 16 Nov 2017 08:22:57 -0800 Subject: [PATCH 14/24] fixturize --- pandas/tests/tseries/offsets/conftest.py | 13 ++++++++++ pandas/tests/tseries/offsets/test_offsets.py | 26 +++++++++----------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/pandas/tests/tseries/offsets/conftest.py b/pandas/tests/tseries/offsets/conftest.py index 25446c24b28c0..76f24123ea0e1 100644 --- a/pandas/tests/tseries/offsets/conftest.py +++ b/pandas/tests/tseries/offsets/conftest.py @@ -7,6 +7,19 @@ def offset_types(request): return request.param +@pytest.fixture(params=[getattr(offsets, o) for o in offsets.__all__ if + issubclass(getattr(offsets, o), offsets.MonthOffset) + and o != 'MonthOffset']) +def month_classes(request): + return request.param + + +@pytest.fixture(params=[getattr(offsets, o) for o in offsets.__all__ if + issubclass(getattr(offsets, o), offsets.Tick)]) +def tick_classes(request): + return request.param + + @pytest.fixture(params=[None, 'UTC', 'Asia/Tokyo', 'US/Eastern', 'dateutil/Asia/Tokyo', 'dateutil/US/Pacific']) def tz(request): diff --git a/pandas/tests/tseries/offsets/test_offsets.py b/pandas/tests/tseries/offsets/test_offsets.py index c4d03d07fcfbc..21d5b4c20ee81 100644 --- a/pandas/tests/tseries/offsets/test_offsets.py +++ b/pandas/tests/tseries/offsets/test_offsets.py @@ -1,4 +1,3 @@ -import inspect import os from distutils.version import LooseVersion from datetime import date, datetime, timedelta @@ -4688,24 +4687,21 @@ def test_all_offset_classes(self, tup): # --------------------------------------------------------------------- -offset_classes = [getattr(offsets, x) for x in dir(offsets)] -offset_classes = [x for x in offset_classes if inspect.isclass(x) and - issubclass(x, DateOffset) and - x not in [offsets.YearOffset, offsets.Tick, - offsets.SingleConstructorOffset, - offsets.SemiMonthOffset, - offsets.QuarterOffset, offsets.MonthOffset]] -month_classes = [x for x in offset_classes if - issubclass(x, offsets.MonthOffset)] - -tick_classes = [x for x in offset_classes if issubclass(x, offsets.Tick)] +@pytest.mark.parametrize('kwd', _rd_kwds) +def test_valid_month_attributes(kwd, month_classes): + # GH#18226 + cls = month_classes + # check that we cannot create e.g. MonthEnd(weeks=3) + with pytest.raises(TypeError): + cls(**{kwd: 3}) -@pytest.mark.parametrize('cls', month_classes + tick_classes) @pytest.mark.parametrize('kwd', _rd_kwds) -def test_valid_attributes(kwd, cls): - # check that we cannot create e.g. MonthEnd(weeks=3) +def test_valid_tick_attributes(kwd, tick_classes): + # GH#18226 + cls = tick_classes + # check that we cannot create e.g. Hour(weeks=3) with pytest.raises(TypeError): cls(**{kwd: 3}) From c6cc8bca3c38a15c0c61e44211d5dedabf99ce18 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Thu, 16 Nov 2017 17:52:01 -0800 Subject: [PATCH 15/24] troubleshoot segfault by moving __eq__ to _BaseOffset --- pandas/_libs/tslibs/offsets.pyx | 17 +++++++++++++++++ pandas/tseries/offsets.py | 17 ----------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx index 2b896bcc930a7..decbe6f1eff0b 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -394,6 +394,23 @@ class _BaseOffset(object): out = '<%s' % n_str + className + plural + self._repr_attrs() + '>' return out + def __eq__(self, other): + if other is None: + return False + + if is_string_object(other): + from pandas.tseries.frequencies import to_offset + + other = to_offset(other) + + if not isinstance(other, _BaseOffset): + return False + + return self._params() == other._params() + + def __hash__(self): + return hash(self._params()) + class BaseOffset(_BaseOffset): # Here we add __rfoo__ methods that don't play well with cdef classes diff --git a/pandas/tseries/offsets.py b/pandas/tseries/offsets.py index 44d21646e5c26..f0448343ee066 100644 --- a/pandas/tseries/offsets.py +++ b/pandas/tseries/offsets.py @@ -300,26 +300,9 @@ def _repr_attrs(self): def name(self): return self.rule_code - def __eq__(self, other): - if other is None: - return False - - if isinstance(other, compat.string_types): - from pandas.tseries.frequencies import to_offset - - other = to_offset(other) - - if not isinstance(other, DateOffset): - return False - - return self._params() == other._params() - def __ne__(self, other): return not self == other - def __hash__(self): - return hash(self._params()) - def __add__(self, other): if isinstance(other, (ABCDatetimeIndex, ABCSeries)): return other + self From c8224c1b91e82f391b965bc630f4fc501b930547 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Fri, 17 Nov 2017 07:01:28 -0800 Subject: [PATCH 16/24] try sorting rd_kwds to fix segfault, revert other troubleshooting guesses --- pandas/_libs/tslibs/offsets.pyx | 17 ------ pandas/tests/tseries/offsets/test_offsets.py | 4 +- pandas/tests/tseries/offsets/test_ticks.py | 64 +++++--------------- pandas/tseries/offsets.py | 17 ++++++ 4 files changed, 33 insertions(+), 69 deletions(-) diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx index decbe6f1eff0b..2b896bcc930a7 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -394,23 +394,6 @@ class _BaseOffset(object): out = '<%s' % n_str + className + plural + self._repr_attrs() + '>' return out - def __eq__(self, other): - if other is None: - return False - - if is_string_object(other): - from pandas.tseries.frequencies import to_offset - - other = to_offset(other) - - if not isinstance(other, _BaseOffset): - return False - - return self._params() == other._params() - - def __hash__(self): - return hash(self._params()) - class BaseOffset(_BaseOffset): # Here we add __rfoo__ methods that don't play well with cdef classes diff --git a/pandas/tests/tseries/offsets/test_offsets.py b/pandas/tests/tseries/offsets/test_offsets.py index 21d5b4c20ee81..59b82208e342e 100644 --- a/pandas/tests/tseries/offsets/test_offsets.py +++ b/pandas/tests/tseries/offsets/test_offsets.py @@ -4688,7 +4688,7 @@ def test_all_offset_classes(self, tup): # --------------------------------------------------------------------- -@pytest.mark.parametrize('kwd', _rd_kwds) +@pytest.mark.parametrize('kwd', sorted(list(_rd_kwds))) def test_valid_month_attributes(kwd, month_classes): # GH#18226 cls = month_classes @@ -4697,7 +4697,7 @@ def test_valid_month_attributes(kwd, month_classes): cls(**{kwd: 3}) -@pytest.mark.parametrize('kwd', _rd_kwds) +@pytest.mark.parametrize('kwd', sorted(list(_rd_kwds))) def test_valid_tick_attributes(kwd, tick_classes): # GH#18226 cls = tick_classes diff --git a/pandas/tests/tseries/offsets/test_ticks.py b/pandas/tests/tseries/offsets/test_ticks.py index 9a3856e614eab..24033d4ff6cbd 100644 --- a/pandas/tests/tseries/offsets/test_ticks.py +++ b/pandas/tests/tseries/offsets/test_ticks.py @@ -38,56 +38,20 @@ def test_delta_to_tick(): # --------------------------------------------------------------------- -class TestHour(object): - def test_construct_hour3(self): - # smoketest to debug appveyor segfault - Hour(3) - - def test_construct_hour2(self): - # smoketest to debug appveyor segfault - Hour(2) - - def test_construct_hour5(self): - # smoketest to debug appveyor segfault - Hour(5) - - def test_hour3_add_hour2(self): - # smoketest to debug appveyor segfault - Hour(3) + Hour(2) - - # def test_hour_add_hour(self): - # assert Hour(3) + Hour(2) == Hour(5) - - def test_hour_sub_hour(self): - assert Hour(3) - Hour(2) == Hour() - - def test_hour_neq_hours(self): - assert Hour(4) != Hour(1) - - def test_neg_hour(self): - assert_offset_equal(-1 * Hour(), - datetime(2010, 1, 1, 1), datetime(2010, 1, 1)) - - def test_negative_n(self): - assert_offset_equal(Hour(-1), - datetime(2010, 1, 1, 1), - datetime(2010, 1, 1)) - - def test_one_hour(self): - v1 = Hour() - v2 = Hour(n=1) - assert v1 == v2 - assert v1 + datetime(2010, 1, 1) == v2 + datetime(2010, 1, 1) - assert_offset_equal(Hour(), - datetime(2010, 1, 1), datetime(2010, 1, 1, 1)) - - def test_multiple_hours(self): - v1 = Hour(n=2) - v2 = 2 * Hour() - assert v1 == v2 - assert v1 + datetime(2010, 1, 1) == v2 + datetime(2010, 1, 1) - assert_offset_equal(2 * Hour(), - datetime(2010, 1, 1), datetime(2010, 1, 1, 2)) +def test_Hour(): + assert_offset_equal(Hour(), + datetime(2010, 1, 1), datetime(2010, 1, 1, 1)) + assert_offset_equal(Hour(-1), + datetime(2010, 1, 1, 1), datetime(2010, 1, 1)) + assert_offset_equal(2 * Hour(), + datetime(2010, 1, 1), datetime(2010, 1, 1, 2)) + assert_offset_equal(-1 * Hour(), + datetime(2010, 1, 1, 1), datetime(2010, 1, 1)) + + assert Hour(3) + Hour(2) == Hour(5) + assert Hour(3) - Hour(2) == Hour() + + assert Hour(4) != Hour(1) def test_Minute(): diff --git a/pandas/tseries/offsets.py b/pandas/tseries/offsets.py index f0448343ee066..44d21646e5c26 100644 --- a/pandas/tseries/offsets.py +++ b/pandas/tseries/offsets.py @@ -300,9 +300,26 @@ def _repr_attrs(self): def name(self): return self.rule_code + def __eq__(self, other): + if other is None: + return False + + if isinstance(other, compat.string_types): + from pandas.tseries.frequencies import to_offset + + other = to_offset(other) + + if not isinstance(other, DateOffset): + return False + + return self._params() == other._params() + def __ne__(self, other): return not self == other + def __hash__(self): + return hash(self._params()) + def __add__(self, other): if isinstance(other, (ABCDatetimeIndex, ABCSeries)): return other + self From b54e26b4c43fafd01d4ecbb89b03474004091ff6 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Sat, 18 Nov 2017 17:32:26 -0800 Subject: [PATCH 17/24] implement _validate_n method --- pandas/_libs/tslibs/offsets.pyx | 11 ++++++++++- pandas/tseries/offsets.py | 34 ++++++++++++++++----------------- 2 files changed, 27 insertions(+), 18 deletions(-) diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx index 2b896bcc930a7..e3162b42192bd 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -255,7 +255,7 @@ def _validate_business_time(t_input): # --------------------------------------------------------------------- # Constructor Helpers -_rd_kwds = set([ +relativedelta_kwds = set([ 'years', 'months', 'weeks', 'days', 'year', 'month', 'week', 'day', 'weekday', 'hour', 'minute', 'second', 'microsecond', @@ -394,6 +394,15 @@ class _BaseOffset(object): out = '<%s' % n_str + className + plural + self._repr_attrs() + '>' return out + def _validate_n(self, n): + try: + nint = int(n) + except (ValueError, TypeError) + raise ValueError('`n` argument must be an integer') + if n != nint: + raise ValueError('`n` argument must be an integer') + return nint + class BaseOffset(_BaseOffset): # Here we add __rfoo__ methods that don't play well with cdef classes diff --git a/pandas/tseries/offsets.py b/pandas/tseries/offsets.py index 44d21646e5c26..a44569d9f3459 100644 --- a/pandas/tseries/offsets.py +++ b/pandas/tseries/offsets.py @@ -167,7 +167,7 @@ def __add__(date): normalize = False def __init__(self, n=1, normalize=False, **kwds): - self.n = int(n) + self.n = self._validate_n(n) self.normalize = normalize self.kwds = kwds @@ -474,7 +474,7 @@ class BusinessDay(BusinessMixin, SingleConstructorOffset): _adjust_dst = True def __init__(self, n=1, normalize=False, offset=timedelta(0)): - self.n = int(n) + self.n = self._validate_n(n) self.normalize = normalize self.kwds = {'offset': offset} self._offset = offset @@ -783,7 +783,7 @@ class BusinessHour(BusinessHourMixin, SingleConstructorOffset): def __init__(self, n=1, normalize=False, start='09:00', end='17:00', offset=timedelta(0)): - self.n = int(n) + self.n = self._validate_n(n) self.normalize = normalize super(BusinessHour, self).__init__(start=start, end=end, offset=offset) @@ -820,7 +820,7 @@ class CustomBusinessDay(BusinessDay): def __init__(self, n=1, normalize=False, weekmask='Mon Tue Wed Thu Fri', holidays=None, calendar=None, offset=timedelta(0)): - self.n = int(n) + self.n = self._validate_n(n) self.normalize = normalize self._offset = offset self.kwds = {} @@ -888,7 +888,7 @@ class CustomBusinessHour(BusinessHourMixin, SingleConstructorOffset): def __init__(self, n=1, normalize=False, weekmask='Mon Tue Wed Thu Fri', holidays=None, calendar=None, start='09:00', end='17:00', offset=timedelta(0)): - self.n = int(n) + self.n = self._validate_n(n) self.normalize = normalize super(CustomBusinessHour, self).__init__(start=start, end=end, offset=offset) @@ -917,7 +917,7 @@ class MonthOffset(SingleConstructorOffset): _adjust_dst = True def __init__(self, n=1, normalize=False): - self.n = int(n) + self.n = self._validate_n(n) self.normalize = normalize self._offset = timedelta(1) self._use_relativedelta = False @@ -998,7 +998,7 @@ def __init__(self, n=1, normalize=False, day_of_month=None): raise ValueError(msg.format(min=self._min_day_of_month, day=self.day_of_month)) - self.n = int(n) + self.n = self._validate_n(n) self.normalize = normalize self.kwds = {'day_of_month': self.day_of_month} @@ -1251,7 +1251,7 @@ class CustomBusinessMonthEnd(BusinessMixin, MonthOffset): def __init__(self, n=1, normalize=False, weekmask='Mon Tue Wed Thu Fri', holidays=None, calendar=None, offset=timedelta(0)): - self.n = int(n) + self.n = self._validate_n(n) self.normalize = normalize self._offset = offset self.kwds = {} @@ -1322,7 +1322,7 @@ class CustomBusinessMonthBegin(BusinessMixin, MonthOffset): def __init__(self, n=1, normalize=False, weekmask='Mon Tue Wed Thu Fri', holidays=None, calendar=None, offset=timedelta(0)): - self.n = int(n) + self.n = self._validate_n(n) self.normalize = normalize self._offset = offset self.kwds = {} @@ -1386,7 +1386,7 @@ class Week(EndMixin, DateOffset): _prefix = 'W' def __init__(self, n=1, normalize=False, weekday=None): - self.n = int(n) + self.n = self._validate_n(n) self.normalize = normalize self.weekday = weekday @@ -1465,7 +1465,7 @@ class WeekOfMonth(DateOffset): _adjust_dst = True def __init__(self, n=1, normalize=False, week=None, weekday=None): - self.n = int(n) + self.n = self._validate_n(n) self.normalize = normalize self.weekday = weekday self.week = week @@ -1550,7 +1550,7 @@ class LastWeekOfMonth(DateOffset): _prefix = 'LWOM' def __init__(self, n=1, normalize=False, weekday=None): - self.n = int(n) + self.n = self._validate_n(n) self.normalize = normalize self.weekday = weekday @@ -1613,7 +1613,7 @@ class QuarterOffset(DateOffset): # point def __init__(self, n=1, normalize=False, startingMonth=None): - self.n = int(n) + self.n = self._validate_n(n) self.normalize = normalize if startingMonth is None: startingMonth = self._default_startingMonth @@ -1957,7 +1957,7 @@ class FY5253(DateOffset): def __init__(self, n=1, normalize=False, weekday=0, startingMonth=1, variation="nearest"): - self.n = int(n) + self.n = self._validate_n(n) self.normalize = normalize self.startingMonth = startingMonth self.weekday = weekday @@ -2207,7 +2207,7 @@ class FY5253Quarter(DateOffset): def __init__(self, n=1, normalize=False, weekday=0, startingMonth=1, qtr_with_extra_week=1, variation="nearest"): - self.n = int(n) + self.n = self._validate_n(n) self.normalize = normalize self.weekday = weekday @@ -2335,7 +2335,7 @@ class Easter(DateOffset): _adjust_dst = True def __init__(self, n=1, normalize=False): - self.n = int(n) + self.n = self._validate_n(n) self.normalize = normalize self._offset = timedelta(1) self._use_relativedelta = False @@ -2387,7 +2387,7 @@ class Tick(SingleConstructorOffset): def __init__(self, n=1, normalize=False): # TODO: do Tick classes with normalize=True make sense? - self.n = int(n) + self.n = self._validate_n(n) self.normalize = normalize self._offset = timedelta(1) self._use_relativedelta = False From fade4a2accc8c37637ab8ec7c3b46ea21dd893a5 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Sat, 18 Nov 2017 17:34:27 -0800 Subject: [PATCH 18/24] test for _validate_n --- pandas/_libs/tslibs/offsets.pyx | 2 +- pandas/tests/tseries/offsets/test_offsets.py | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx index e3162b42192bd..66bb98ae8dc82 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -397,7 +397,7 @@ class _BaseOffset(object): def _validate_n(self, n): try: nint = int(n) - except (ValueError, TypeError) + except (ValueError, TypeError): raise ValueError('`n` argument must be an integer') if n != nint: raise ValueError('`n` argument must be an integer') diff --git a/pandas/tests/tseries/offsets/test_offsets.py b/pandas/tests/tseries/offsets/test_offsets.py index 17769204a8328..c8199b4f6c1fe 100644 --- a/pandas/tests/tseries/offsets/test_offsets.py +++ b/pandas/tests/tseries/offsets/test_offsets.py @@ -4703,3 +4703,7 @@ def test_valid_tick_attributes(kwd, tick_classes): with pytest.raises(TypeError): cls(**{kwd: 3}) + +def test_validate_n_error(): + with pytest.raises(ValueError): + DateOffset(n='Doh!') From fe895ffca8d8e7a74385661a221499cfe5160b25 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Tue, 21 Nov 2017 19:52:58 -0800 Subject: [PATCH 19/24] Raise TypeError, not ValueError --- pandas/_libs/tslibs/offsets.pyx | 4 ++-- pandas/tests/tseries/offsets/test_offsets.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx index 1bdd486e9fa64..ee27b86b54f7a 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -409,8 +409,8 @@ class _BaseOffset(object): def _validate_n(self, n): try: nint = int(n) - except (ValueError, TypeError): - raise ValueError('`n` argument must be an integer') + except TypeError: + raise TypeError('`n` argument must be an integer') if n != nint: raise ValueError('`n` argument must be an integer') return nint diff --git a/pandas/tests/tseries/offsets/test_offsets.py b/pandas/tests/tseries/offsets/test_offsets.py index 991d506e3f3a2..23ade1ca48099 100644 --- a/pandas/tests/tseries/offsets/test_offsets.py +++ b/pandas/tests/tseries/offsets/test_offsets.py @@ -4711,5 +4711,5 @@ def test_valid_tick_attributes(kwd, tick_classes): def test_validate_n_error(): - with pytest.raises(ValueError): + with pytest.raises(TypeError): DateOffset(n='Doh!') From bc90a193ada627fb432c7f547b9a05cb23f6a11d Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Wed, 22 Nov 2017 07:58:40 -0800 Subject: [PATCH 20/24] Catch ValueError in int(n) --- pandas/_libs/tslibs/offsets.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx index ee27b86b54f7a..b54369503f192 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -409,7 +409,7 @@ class _BaseOffset(object): def _validate_n(self, n): try: nint = int(n) - except TypeError: + except (ValueError, TypeError): raise TypeError('`n` argument must be an integer') if n != nint: raise ValueError('`n` argument must be an integer') From 0b3dca071312985a32c0bb4f3d8ee559eef8fd7e Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Wed, 22 Nov 2017 12:10:32 -0800 Subject: [PATCH 21/24] fixup extra imports --- pandas/tseries/offsets.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pandas/tseries/offsets.py b/pandas/tseries/offsets.py index 7391ea76de20f..67a18af600dce 100644 --- a/pandas/tseries/offsets.py +++ b/pandas/tseries/offsets.py @@ -1,7 +1,4 @@ # -*- coding: utf-8 -*- -import functools -import operator - from datetime import date, datetime, timedelta import functools import operator From adee3951a00cf0bdd073f247fce14ce0c201b52d Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Thu, 23 Nov 2017 08:33:38 -0800 Subject: [PATCH 22/24] remove unnecessary re-definitions, add tests, improve error msg --- pandas/_libs/tslibs/offsets.pyx | 6 ++++-- pandas/tests/tseries/offsets/test_offsets.py | 12 ++++++++++++ pandas/tseries/offsets.py | 8 -------- 3 files changed, 16 insertions(+), 10 deletions(-) diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx index b54369503f192..eeffdf881a540 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -410,9 +410,11 @@ class _BaseOffset(object): try: nint = int(n) except (ValueError, TypeError): - raise TypeError('`n` argument must be an integer') + raise TypeError('`n` argument must be an integer, ' + 'got {ntype}'.format(type(n))) if n != nint: - raise ValueError('`n` argument must be an integer') + raise ValueError('`n` argument must be an integer, ' + 'got {n}'.format(n=n)) return nint diff --git a/pandas/tests/tseries/offsets/test_offsets.py b/pandas/tests/tseries/offsets/test_offsets.py index 23ade1ca48099..357c95282e78d 100644 --- a/pandas/tests/tseries/offsets/test_offsets.py +++ b/pandas/tests/tseries/offsets/test_offsets.py @@ -4713,3 +4713,15 @@ def test_valid_tick_attributes(kwd, tick_classes): def test_validate_n_error(): with pytest.raises(TypeError): DateOffset(n='Doh!') + + with pytest.raises(TypeError): + MonthBegin(n=timedelta(1)) + + with pytest.raises(TypeError): + BDay(n=np.array([1, 2], dtype=np.int64)) + + +def test_require_integers(offset_types): + cls = offset_types + with pytest.raises(ValueError): + cls(n=1.5) diff --git a/pandas/tseries/offsets.py b/pandas/tseries/offsets.py index 67a18af600dce..239866defd757 100644 --- a/pandas/tseries/offsets.py +++ b/pandas/tseries/offsets.py @@ -923,8 +923,6 @@ class MonthOffset(SingleConstructorOffset): def __init__(self, n=1, normalize=False): self.n = self._validate_n(n) self.normalize = normalize - self._offset = timedelta(1) - self._use_relativedelta = False self.kwds = {} @property @@ -1591,8 +1589,6 @@ def __init__(self, n=1, normalize=False, startingMonth=None): self.startingMonth = startingMonth self.kwds = {'startingMonth': startingMonth} - self._offset = timedelta(1) - self._use_relativedelta = False def isAnchored(self): return (self.n == 1 and self.startingMonth is not None) @@ -2214,8 +2210,6 @@ class Easter(DateOffset): def __init__(self, n=1, normalize=False): self.n = self._validate_n(n) self.normalize = normalize - self._offset = timedelta(1) - self._use_relativedelta = False self.kwds = {} @apply_wraps @@ -2266,8 +2260,6 @@ def __init__(self, n=1, normalize=False): # TODO: do Tick classes with normalize=True make sense? self.n = self._validate_n(n) self.normalize = normalize - self._offset = timedelta(1) - self._use_relativedelta = False self.kwds = {} __gt__ = _tick_comp(operator.gt) From 43dc17ed9d132c2a66b5d5ffac825eae9108c1ca Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Thu, 23 Nov 2017 08:42:00 -0800 Subject: [PATCH 23/24] Add docstring to validate_n --- pandas/_libs/tslibs/offsets.pyx | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx index eeffdf881a540..a2e765f14fdae 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -407,6 +407,22 @@ class _BaseOffset(object): return get_day_of_month(other, self._day_opt) def _validate_n(self, n): + """ + Require that `n` be a nonzero integer. + + Parameters + ---------- + n : int + + Returns + ------- + nint : int + + Raises + ------ + TypeError if `int(n)` raises + ValueError if n != int(n) + """ try: nint = int(n) except (ValueError, TypeError): From 4ff8c22e765e158f4c7b4903ce3df34570aa4be9 Mon Sep 17 00:00:00 2001 From: Brock Mendel Date: Thu, 23 Nov 2017 20:19:42 -0800 Subject: [PATCH 24/24] fixup missing format --- pandas/_libs/tslibs/offsets.pyx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx index a2e765f14fdae..c1289d857d3f2 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -427,7 +427,7 @@ class _BaseOffset(object): nint = int(n) except (ValueError, TypeError): raise TypeError('`n` argument must be an integer, ' - 'got {ntype}'.format(type(n))) + 'got {ntype}'.format(ntype=type(n))) if n != nint: raise ValueError('`n` argument must be an integer, ' 'got {n}'.format(n=n))