Skip to content

Commit 1638331

Browse files
jbrockmendeljreback
authored andcommitted
make DateOffset immutable (#21341)
1 parent e24da6c commit 1638331

File tree

4 files changed

+75
-69
lines changed

4 files changed

+75
-69
lines changed

doc/source/whatsnew/v0.24.0.txt

+1-1
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ Datetimelike API Changes
6767
^^^^^^^^^^^^^^^^^^^^^^^^
6868

6969
- 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`)
70+
- :class:`DateOffset` objects are now immutable. Attempting to alter one of these will now raise ``AttributeError`` (:issue:`21341`)
7071

7172
.. _whatsnew_0240.api.other:
7273

@@ -176,7 +177,6 @@ Timezones
176177
Offsets
177178
^^^^^^^
178179

179-
-
180180
-
181181
-
182182

pandas/_libs/tslibs/offsets.pyx

+13-3
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,15 @@ class _BaseOffset(object):
304304
_day_opt = None
305305
_attributes = frozenset(['n', 'normalize'])
306306

307+
def __init__(self, n=1, normalize=False):
308+
n = self._validate_n(n)
309+
object.__setattr__(self, "n", n)
310+
object.__setattr__(self, "normalize", normalize)
311+
object.__setattr__(self, "_cache", {})
312+
313+
def __setattr__(self, name, value):
314+
raise AttributeError("DateOffset objects are immutable.")
315+
307316
@property
308317
def kwds(self):
309318
# for backwards-compatibility
@@ -395,13 +404,14 @@ class _BaseOffset(object):
395404
kwds = {key: odict[key] for key in odict if odict[key]}
396405
state.update(kwds)
397406

398-
self.__dict__ = state
407+
self.__dict__.update(state)
408+
399409
if 'weekmask' in state and 'holidays' in state:
400410
calendar, holidays = _get_calendar(weekmask=self.weekmask,
401411
holidays=self.holidays,
402412
calendar=None)
403-
self.calendar = calendar
404-
self.holidays = holidays
413+
object.__setattr__(self, "calendar", calendar)
414+
object.__setattr__(self, "holidays", holidays)
405415

406416
def __getstate__(self):
407417
"""Return a pickleable state"""

pandas/tests/tseries/offsets/test_offsets.py

+8
Original file line numberDiff line numberDiff line change
@@ -234,6 +234,14 @@ class TestCommon(Base):
234234
'Nano': Timestamp(np_datetime64_compat(
235235
'2011-01-01T09:00:00.000000001Z'))}
236236

237+
def test_immutable(self, offset_types):
238+
# GH#21341 check that __setattr__ raises
239+
offset = self._get_offset(offset_types)
240+
with pytest.raises(AttributeError):
241+
offset.normalize = True
242+
with pytest.raises(AttributeError):
243+
offset.n = 91
244+
237245
def test_return_type(self, offset_types):
238246
offset = self._get_offset(offset_types)
239247

pandas/tseries/offsets.py

+53-65
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
ApplyTypeError,
2424
as_datetime, _is_normalized,
2525
_get_calendar, _to_dt64,
26-
_determine_offset,
2726
apply_index_wraps,
2827
roll_yearday,
2928
shift_month,
@@ -192,11 +191,14 @@ def __add__(date):
192191
normalize = False
193192

194193
def __init__(self, n=1, normalize=False, **kwds):
195-
self.n = self._validate_n(n)
196-
self.normalize = normalize
194+
BaseOffset.__init__(self, n, normalize)
197195

198-
self._offset, self._use_relativedelta = _determine_offset(kwds)
199-
self.__dict__.update(kwds)
196+
off, use_rd = liboffsets._determine_offset(kwds)
197+
object.__setattr__(self, "_offset", off)
198+
object.__setattr__(self, "_use_relativedelta", use_rd)
199+
for key in kwds:
200+
val = kwds[key]
201+
object.__setattr__(self, key, val)
200202

201203
@apply_wraps
202204
def apply(self, other):
@@ -446,9 +448,9 @@ def __init__(self, weekmask, holidays, calendar):
446448
# following two attributes. See DateOffset._params()
447449
# holidays, weekmask
448450

449-
self.weekmask = weekmask
450-
self.holidays = holidays
451-
self.calendar = calendar
451+
object.__setattr__(self, "weekmask", weekmask)
452+
object.__setattr__(self, "holidays", holidays)
453+
object.__setattr__(self, "calendar", calendar)
452454

453455

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

482484
def __init__(self, n=1, normalize=False, offset=timedelta(0)):
483-
self.n = self._validate_n(n)
484-
self.normalize = normalize
485-
self._offset = offset
485+
BaseOffset.__init__(self, n, normalize)
486+
object.__setattr__(self, "_offset", offset)
486487

487488
def _offset_str(self):
488489
def get_str(td):
@@ -578,9 +579,11 @@ class BusinessHourMixin(BusinessMixin):
578579

579580
def __init__(self, start='09:00', end='17:00', offset=timedelta(0)):
580581
# must be validated here to equality check
581-
self.start = liboffsets._validate_business_time(start)
582-
self.end = liboffsets._validate_business_time(end)
583-
self._offset = offset
582+
start = liboffsets._validate_business_time(start)
583+
object.__setattr__(self, "start", start)
584+
end = liboffsets._validate_business_time(end)
585+
object.__setattr__(self, "end", end)
586+
object.__setattr__(self, "_offset", offset)
584587

585588
@cache_readonly
586589
def next_bday(self):
@@ -807,8 +810,7 @@ class BusinessHour(BusinessHourMixin, SingleConstructorOffset):
807810

808811
def __init__(self, n=1, normalize=False, start='09:00',
809812
end='17:00', offset=timedelta(0)):
810-
self.n = self._validate_n(n)
811-
self.normalize = normalize
813+
BaseOffset.__init__(self, n, normalize)
812814
super(BusinessHour, self).__init__(start=start, end=end, offset=offset)
813815

814816

@@ -837,9 +839,8 @@ class CustomBusinessDay(_CustomMixin, BusinessDay):
837839

838840
def __init__(self, n=1, normalize=False, weekmask='Mon Tue Wed Thu Fri',
839841
holidays=None, calendar=None, offset=timedelta(0)):
840-
self.n = self._validate_n(n)
841-
self.normalize = normalize
842-
self._offset = offset
842+
BaseOffset.__init__(self, n, normalize)
843+
object.__setattr__(self, "_offset", offset)
843844

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

@@ -898,9 +899,8 @@ class CustomBusinessHour(_CustomMixin, BusinessHourMixin,
898899
def __init__(self, n=1, normalize=False, weekmask='Mon Tue Wed Thu Fri',
899900
holidays=None, calendar=None,
900901
start='09:00', end='17:00', offset=timedelta(0)):
901-
self.n = self._validate_n(n)
902-
self.normalize = normalize
903-
self._offset = offset
902+
BaseOffset.__init__(self, n, normalize)
903+
object.__setattr__(self, "_offset", offset)
904904

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

917-
def __init__(self, n=1, normalize=False):
918-
self.n = self._validate_n(n)
919-
self.normalize = normalize
917+
__init__ = BaseOffset.__init__
920918

921919
@property
922920
def name(self):
@@ -995,9 +993,8 @@ class _CustomBusinessMonth(_CustomMixin, BusinessMixin, MonthOffset):
995993

996994
def __init__(self, n=1, normalize=False, weekmask='Mon Tue Wed Thu Fri',
997995
holidays=None, calendar=None, offset=timedelta(0)):
998-
self.n = self._validate_n(n)
999-
self.normalize = normalize
1000-
self._offset = offset
996+
BaseOffset.__init__(self, n, normalize)
997+
object.__setattr__(self, "_offset", offset)
1001998

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

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

10761073
def __init__(self, n=1, normalize=False, day_of_month=None):
1074+
BaseOffset.__init__(self, n, normalize)
1075+
10771076
if day_of_month is None:
1078-
self.day_of_month = self._default_day_of_month
1077+
object.__setattr__(self, "day_of_month",
1078+
self._default_day_of_month)
10791079
else:
1080-
self.day_of_month = int(day_of_month)
1080+
object.__setattr__(self, "day_of_month", int(day_of_month))
10811081
if not self._min_day_of_month <= self.day_of_month <= 27:
10821082
msg = 'day_of_month must be {min}<=day_of_month<=27, got {day}'
10831083
raise ValueError(msg.format(min=self._min_day_of_month,
10841084
day=self.day_of_month))
10851085

1086-
self.n = self._validate_n(n)
1087-
self.normalize = normalize
1088-
10891086
@classmethod
10901087
def _from_name(cls, suffix=None):
10911088
return cls(day_of_month=suffix)
@@ -1291,9 +1288,8 @@ class Week(DateOffset):
12911288
_attributes = frozenset(['n', 'normalize', 'weekday'])
12921289

12931290
def __init__(self, n=1, normalize=False, weekday=None):
1294-
self.n = self._validate_n(n)
1295-
self.normalize = normalize
1296-
self.weekday = weekday
1291+
BaseOffset.__init__(self, n, normalize)
1292+
object.__setattr__(self, "weekday", weekday)
12971293

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

14231419
def __init__(self, n=1, normalize=False, week=0, weekday=0):
1424-
self.n = self._validate_n(n)
1425-
self.normalize = normalize
1426-
self.weekday = weekday
1427-
self.week = week
1420+
BaseOffset.__init__(self, n, normalize)
1421+
object.__setattr__(self, "weekday", weekday)
1422+
object.__setattr__(self, "week", week)
14281423

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

14951490
def __init__(self, n=1, normalize=False, weekday=0):
1496-
self.n = self._validate_n(n)
1497-
self.normalize = normalize
1498-
self.weekday = weekday
1491+
BaseOffset.__init__(self, n, normalize)
1492+
object.__setattr__(self, "weekday", weekday)
14991493

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

15551549
def __init__(self, n=1, normalize=False, startingMonth=None):
1556-
self.n = self._validate_n(n)
1557-
self.normalize = normalize
1550+
BaseOffset.__init__(self, n, normalize)
1551+
15581552
if startingMonth is None:
15591553
startingMonth = self._default_startingMonth
1560-
self.startingMonth = startingMonth
1554+
object.__setattr__(self, "startingMonth", startingMonth)
15611555

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

16811675
def __init__(self, n=1, normalize=False, month=None):
1682-
self.n = self._validate_n(n)
1683-
self.normalize = normalize
1676+
BaseOffset.__init__(self, n, normalize)
16841677

16851678
month = month if month is not None else self._default_month
1686-
self.month = month
1679+
object.__setattr__(self, "month", month)
16871680

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

17771770
def __init__(self, n=1, normalize=False, weekday=0, startingMonth=1,
17781771
variation="nearest"):
1779-
self.n = self._validate_n(n)
1780-
self.normalize = normalize
1781-
self.startingMonth = startingMonth
1782-
self.weekday = weekday
1772+
BaseOffset.__init__(self, n, normalize)
1773+
object.__setattr__(self, "startingMonth", startingMonth)
1774+
object.__setattr__(self, "weekday", weekday)
17831775

1784-
self.variation = variation
1776+
object.__setattr__(self, "variation", variation)
17851777

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

19771969
def __init__(self, n=1, normalize=False, weekday=0, startingMonth=1,
19781970
qtr_with_extra_week=1, variation="nearest"):
1979-
self.n = self._validate_n(n)
1980-
self.normalize = normalize
1971+
BaseOffset.__init__(self, n, normalize)
19811972

1982-
self.weekday = weekday
1983-
self.startingMonth = startingMonth
1984-
self.qtr_with_extra_week = qtr_with_extra_week
1985-
self.variation = variation
1973+
object.__setattr__(self, "startingMonth", startingMonth)
1974+
object.__setattr__(self, "weekday", weekday)
1975+
object.__setattr__(self, "qtr_with_extra_week", qtr_with_extra_week)
1976+
object.__setattr__(self, "variation", variation)
19861977

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

2132-
def __init__(self, n=1, normalize=False):
2133-
self.n = self._validate_n(n)
2134-
self.normalize = normalize
2123+
__init__ = BaseOffset.__init__
21352124

21362125
@apply_wraps
21372126
def apply(self, other):
@@ -2177,11 +2166,10 @@ class Tick(SingleConstructorOffset):
21772166
_attributes = frozenset(['n', 'normalize'])
21782167

21792168
def __init__(self, n=1, normalize=False):
2180-
self.n = self._validate_n(n)
2169+
BaseOffset.__init__(self, n, normalize)
21812170
if normalize:
21822171
raise ValueError("Tick offset with `normalize=True` are not "
21832172
"allowed.") # GH#21427
2184-
self.normalize = normalize
21852173

21862174
__gt__ = _tick_comp(operator.gt)
21872175
__ge__ = _tick_comp(operator.ge)

0 commit comments

Comments
 (0)