Skip to content

Commit

Permalink
make DateOffset immutable (pandas-dev#21341)
Browse files Browse the repository at this point in the history
  • Loading branch information
jbrockmendel authored and victor committed Sep 30, 2018
1 parent f4ef546 commit cbb7cc0
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 69 deletions.
2 changes: 1 addition & 1 deletion doc/source/whatsnew/v0.24.0.txt
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ Datetimelike API Changes
^^^^^^^^^^^^^^^^^^^^^^^^

- For :class:`DatetimeIndex` and :class:`TimedeltaIndex` with non-``None`` ``freq`` attribute, addition or subtraction of integer-dtyped array or ``Index`` will return an object of the same class (:issue:`19959`)
- :class:`DateOffset` objects are now immutable. Attempting to alter one of these will now raise ``AttributeError`` (:issue:`21341`)

.. _whatsnew_0240.api.other:

Expand Down Expand Up @@ -176,7 +177,6 @@ Timezones
Offsets
^^^^^^^

-
-
-

Expand Down
16 changes: 13 additions & 3 deletions pandas/_libs/tslibs/offsets.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,15 @@ class _BaseOffset(object):
_day_opt = None
_attributes = frozenset(['n', 'normalize'])

def __init__(self, n=1, normalize=False):
n = self._validate_n(n)
object.__setattr__(self, "n", n)
object.__setattr__(self, "normalize", normalize)
object.__setattr__(self, "_cache", {})

def __setattr__(self, name, value):
raise AttributeError("DateOffset objects are immutable.")

@property
def kwds(self):
# for backwards-compatibility
Expand Down Expand Up @@ -395,13 +404,14 @@ class _BaseOffset(object):
kwds = {key: odict[key] for key in odict if odict[key]}
state.update(kwds)

self.__dict__ = state
self.__dict__.update(state)

if 'weekmask' in state and 'holidays' in state:
calendar, holidays = _get_calendar(weekmask=self.weekmask,
holidays=self.holidays,
calendar=None)
self.calendar = calendar
self.holidays = holidays
object.__setattr__(self, "calendar", calendar)
object.__setattr__(self, "holidays", holidays)

def __getstate__(self):
"""Return a pickleable state"""
Expand Down
8 changes: 8 additions & 0 deletions pandas/tests/tseries/offsets/test_offsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,14 @@ class TestCommon(Base):
'Nano': Timestamp(np_datetime64_compat(
'2011-01-01T09:00:00.000000001Z'))}

def test_immutable(self, offset_types):
# GH#21341 check that __setattr__ raises
offset = self._get_offset(offset_types)
with pytest.raises(AttributeError):
offset.normalize = True
with pytest.raises(AttributeError):
offset.n = 91

def test_return_type(self, offset_types):
offset = self._get_offset(offset_types)

Expand Down
118 changes: 53 additions & 65 deletions pandas/tseries/offsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
ApplyTypeError,
as_datetime, _is_normalized,
_get_calendar, _to_dt64,
_determine_offset,
apply_index_wraps,
roll_yearday,
shift_month,
Expand Down Expand Up @@ -192,11 +191,14 @@ def __add__(date):
normalize = False

def __init__(self, n=1, normalize=False, **kwds):
self.n = self._validate_n(n)
self.normalize = normalize
BaseOffset.__init__(self, n, normalize)

self._offset, self._use_relativedelta = _determine_offset(kwds)
self.__dict__.update(kwds)
off, use_rd = liboffsets._determine_offset(kwds)
object.__setattr__(self, "_offset", off)
object.__setattr__(self, "_use_relativedelta", use_rd)
for key in kwds:
val = kwds[key]
object.__setattr__(self, key, val)

@apply_wraps
def apply(self, other):
Expand Down Expand Up @@ -446,9 +448,9 @@ def __init__(self, weekmask, holidays, calendar):
# following two attributes. See DateOffset._params()
# holidays, weekmask

self.weekmask = weekmask
self.holidays = holidays
self.calendar = calendar
object.__setattr__(self, "weekmask", weekmask)
object.__setattr__(self, "holidays", holidays)
object.__setattr__(self, "calendar", calendar)


class BusinessMixin(object):
Expand Down Expand Up @@ -480,9 +482,8 @@ class BusinessDay(BusinessMixin, SingleConstructorOffset):
_attributes = frozenset(['n', 'normalize', 'offset'])

def __init__(self, n=1, normalize=False, offset=timedelta(0)):
self.n = self._validate_n(n)
self.normalize = normalize
self._offset = offset
BaseOffset.__init__(self, n, normalize)
object.__setattr__(self, "_offset", offset)

def _offset_str(self):
def get_str(td):
Expand Down Expand Up @@ -578,9 +579,11 @@ class BusinessHourMixin(BusinessMixin):

def __init__(self, start='09:00', end='17:00', offset=timedelta(0)):
# must be validated here to equality check
self.start = liboffsets._validate_business_time(start)
self.end = liboffsets._validate_business_time(end)
self._offset = offset
start = liboffsets._validate_business_time(start)
object.__setattr__(self, "start", start)
end = liboffsets._validate_business_time(end)
object.__setattr__(self, "end", end)
object.__setattr__(self, "_offset", offset)

@cache_readonly
def next_bday(self):
Expand Down Expand Up @@ -807,8 +810,7 @@ class BusinessHour(BusinessHourMixin, SingleConstructorOffset):

def __init__(self, n=1, normalize=False, start='09:00',
end='17:00', offset=timedelta(0)):
self.n = self._validate_n(n)
self.normalize = normalize
BaseOffset.__init__(self, n, normalize)
super(BusinessHour, self).__init__(start=start, end=end, offset=offset)


Expand Down Expand Up @@ -837,9 +839,8 @@ class CustomBusinessDay(_CustomMixin, BusinessDay):

def __init__(self, n=1, normalize=False, weekmask='Mon Tue Wed Thu Fri',
holidays=None, calendar=None, offset=timedelta(0)):
self.n = self._validate_n(n)
self.normalize = normalize
self._offset = offset
BaseOffset.__init__(self, n, normalize)
object.__setattr__(self, "_offset", offset)

_CustomMixin.__init__(self, weekmask, holidays, calendar)

Expand Down Expand Up @@ -898,9 +899,8 @@ class CustomBusinessHour(_CustomMixin, BusinessHourMixin,
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 = self._validate_n(n)
self.normalize = normalize
self._offset = offset
BaseOffset.__init__(self, n, normalize)
object.__setattr__(self, "_offset", offset)

_CustomMixin.__init__(self, weekmask, holidays, calendar)
BusinessHourMixin.__init__(self, start=start, end=end, offset=offset)
Expand All @@ -914,9 +914,7 @@ class MonthOffset(SingleConstructorOffset):
_adjust_dst = True
_attributes = frozenset(['n', 'normalize'])

def __init__(self, n=1, normalize=False):
self.n = self._validate_n(n)
self.normalize = normalize
__init__ = BaseOffset.__init__

@property
def name(self):
Expand Down Expand Up @@ -995,9 +993,8 @@ class _CustomBusinessMonth(_CustomMixin, BusinessMixin, MonthOffset):

def __init__(self, n=1, normalize=False, weekmask='Mon Tue Wed Thu Fri',
holidays=None, calendar=None, offset=timedelta(0)):
self.n = self._validate_n(n)
self.normalize = normalize
self._offset = offset
BaseOffset.__init__(self, n, normalize)
object.__setattr__(self, "_offset", offset)

_CustomMixin.__init__(self, weekmask, holidays, calendar)

Expand Down Expand Up @@ -1074,18 +1071,18 @@ class SemiMonthOffset(DateOffset):
_attributes = frozenset(['n', 'normalize', 'day_of_month'])

def __init__(self, n=1, normalize=False, day_of_month=None):
BaseOffset.__init__(self, n, normalize)

if day_of_month is None:
self.day_of_month = self._default_day_of_month
object.__setattr__(self, "day_of_month",
self._default_day_of_month)
else:
self.day_of_month = int(day_of_month)
object.__setattr__(self, "day_of_month", int(day_of_month))
if not self._min_day_of_month <= self.day_of_month <= 27:
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))

self.n = self._validate_n(n)
self.normalize = normalize

@classmethod
def _from_name(cls, suffix=None):
return cls(day_of_month=suffix)
Expand Down Expand Up @@ -1291,9 +1288,8 @@ class Week(DateOffset):
_attributes = frozenset(['n', 'normalize', 'weekday'])

def __init__(self, n=1, normalize=False, weekday=None):
self.n = self._validate_n(n)
self.normalize = normalize
self.weekday = weekday
BaseOffset.__init__(self, n, normalize)
object.__setattr__(self, "weekday", weekday)

if self.weekday is not None:
if self.weekday < 0 or self.weekday > 6:
Expand Down Expand Up @@ -1421,10 +1417,9 @@ class WeekOfMonth(_WeekOfMonthMixin, DateOffset):
_attributes = frozenset(['n', 'normalize', 'week', 'weekday'])

def __init__(self, n=1, normalize=False, week=0, weekday=0):
self.n = self._validate_n(n)
self.normalize = normalize
self.weekday = weekday
self.week = week
BaseOffset.__init__(self, n, normalize)
object.__setattr__(self, "weekday", weekday)
object.__setattr__(self, "week", week)

if self.weekday < 0 or self.weekday > 6:
raise ValueError('Day must be 0<=day<=6, got {day}'
Expand Down Expand Up @@ -1493,9 +1488,8 @@ class LastWeekOfMonth(_WeekOfMonthMixin, DateOffset):
_attributes = frozenset(['n', 'normalize', 'weekday'])

def __init__(self, n=1, normalize=False, weekday=0):
self.n = self._validate_n(n)
self.normalize = normalize
self.weekday = weekday
BaseOffset.__init__(self, n, normalize)
object.__setattr__(self, "weekday", weekday)

if self.n == 0:
raise ValueError('N cannot be 0')
Expand Down Expand Up @@ -1553,11 +1547,11 @@ class QuarterOffset(DateOffset):
# startingMonth vs month attr names are resolved

def __init__(self, n=1, normalize=False, startingMonth=None):
self.n = self._validate_n(n)
self.normalize = normalize
BaseOffset.__init__(self, n, normalize)

if startingMonth is None:
startingMonth = self._default_startingMonth
self.startingMonth = startingMonth
object.__setattr__(self, "startingMonth", startingMonth)

def isAnchored(self):
return (self.n == 1 and self.startingMonth is not None)
Expand Down Expand Up @@ -1679,11 +1673,10 @@ def onOffset(self, dt):
return dt.month == self.month and dt.day == self._get_offset_day(dt)

def __init__(self, n=1, normalize=False, month=None):
self.n = self._validate_n(n)
self.normalize = normalize
BaseOffset.__init__(self, n, normalize)

month = month if month is not None else self._default_month
self.month = month
object.__setattr__(self, "month", month)

if self.month < 1 or self.month > 12:
raise ValueError('Month must go from 1 to 12')
Expand Down Expand Up @@ -1776,12 +1769,11 @@ class FY5253(DateOffset):

def __init__(self, n=1, normalize=False, weekday=0, startingMonth=1,
variation="nearest"):
self.n = self._validate_n(n)
self.normalize = normalize
self.startingMonth = startingMonth
self.weekday = weekday
BaseOffset.__init__(self, n, normalize)
object.__setattr__(self, "startingMonth", startingMonth)
object.__setattr__(self, "weekday", weekday)

self.variation = variation
object.__setattr__(self, "variation", variation)

if self.n == 0:
raise ValueError('N cannot be 0')
Expand Down Expand Up @@ -1976,13 +1968,12 @@ class FY5253Quarter(DateOffset):

def __init__(self, n=1, normalize=False, weekday=0, startingMonth=1,
qtr_with_extra_week=1, variation="nearest"):
self.n = self._validate_n(n)
self.normalize = normalize
BaseOffset.__init__(self, n, normalize)

self.weekday = weekday
self.startingMonth = startingMonth
self.qtr_with_extra_week = qtr_with_extra_week
self.variation = variation
object.__setattr__(self, "startingMonth", startingMonth)
object.__setattr__(self, "weekday", weekday)
object.__setattr__(self, "qtr_with_extra_week", qtr_with_extra_week)
object.__setattr__(self, "variation", variation)

if self.n == 0:
raise ValueError('N cannot be 0')
Expand Down Expand Up @@ -2129,9 +2120,7 @@ class Easter(DateOffset):
_adjust_dst = True
_attributes = frozenset(['n', 'normalize'])

def __init__(self, n=1, normalize=False):
self.n = self._validate_n(n)
self.normalize = normalize
__init__ = BaseOffset.__init__

@apply_wraps
def apply(self, other):
Expand Down Expand Up @@ -2177,11 +2166,10 @@ class Tick(SingleConstructorOffset):
_attributes = frozenset(['n', 'normalize'])

def __init__(self, n=1, normalize=False):
self.n = self._validate_n(n)
BaseOffset.__init__(self, n, normalize)
if normalize:
raise ValueError("Tick offset with `normalize=True` are not "
"allowed.") # GH#21427
self.normalize = normalize

__gt__ = _tick_comp(operator.gt)
__ge__ = _tick_comp(operator.ge)
Expand Down

0 comments on commit cbb7cc0

Please sign in to comment.