Skip to content

Tslibs offsets immutable #18224

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 63 additions & 2 deletions pandas/_libs/tslibs/offsets.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ from dateutil.relativedelta import relativedelta

import numpy as np
cimport numpy as np
from numpy cimport int64_t
np.import_array()


Expand Down Expand Up @@ -315,8 +316,8 @@ class EndMixin(object):

# ---------------------------------------------------------------------
# Base Classes

class _BaseOffset(object):
@cython.auto_pickle(False)
cdef class _BaseOffset(object):
"""
Base class for DateOffset methods that are not overriden by subclasses
and will (after pickle errors are resolved) go into a cdef class.
Expand All @@ -325,6 +326,14 @@ class _BaseOffset(object):
_normalize_cache = True
_cacheable = False

cdef readonly:
int64_t n
bint normalize

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

def __call__(self, other):
return self.apply(other)

Expand Down Expand Up @@ -361,6 +370,58 @@ class _BaseOffset(object):
out = '<%s' % n_str + className + plural + self._repr_attrs() + '>'
return out

def __setstate__(self, state):
"""Reconstruct an instance from a pickled state"""
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no just define dunder reduce
you can’t use getstate/setstate in cython class

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll try again with __reduce__. Don't hold your breath.

If we define __setstate__ in the non-cython class, trying to set self.n raises an AttributeError because it is readonly.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't __reduce__ for pickling? The problem here is un-pickling legacy offsets.

# Note: __setstate__ needs to be defined in the cython class otherwise
# trying to set self.n and self.normalize below will
# raise an AttributeError.
if 'normalize' not in state:
# default for prior pickles
# See GH #7748, #7789
state['normalize'] = False
if '_use_relativedelta' not in state:
state['_use_relativedelta'] = False

if 'offset' in state:
# Older versions Business offsets have offset attribute
# instead of _offset
if '_offset' in state: # pragma: no cover
raise ValueError('Unexpected key `_offset`')
state['_offset'] = state.pop('offset')
state['kwds']['offset'] = state['_offset']

self.n = state.pop('n', 1)
self.normalize = state.pop('normalize', False)
self.__dict__ = state

if 'weekmask' in state and 'holidays' in state:
# Business subclasses
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']

def __getstate__(self):
"""Return a pickleable state"""
state = self.__dict__.copy()

# Add attributes from the C base class that aren't in self.__dict__
state['n'] = self.n
state['normalize'] = self.normalize

# we don't want to actually pickle the calendar object
# as its a np.busyday; we recreate on deserilization
if 'calendar' in state:
del state['calendar']
try:
state['kwds'].pop('calendar')
except KeyError:
pass

return state


class BaseOffset(_BaseOffset):
# Here we add __rfoo__ methods that don't play well with cdef classes
Expand Down
18 changes: 17 additions & 1 deletion pandas/compat/pickle_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Support pre-0.12 series pickle compatibility.
"""

import inspect
import sys
import pandas # noqa
import copy
Expand All @@ -22,7 +23,6 @@ def load_reduce(self):
stack[-1] = func(*args)
return
except Exception as e:

# If we have a deprecated function,
# try to replace and try again.

Expand All @@ -47,6 +47,22 @@ def load_reduce(self):
except:
pass

if (len(args) and inspect.isclass(args[0]) and
getattr(args[0], '_typ', None) == 'dateoffset' and
args[1] is object):
# See GH#17313
from pandas.tseries import offsets
args = (args[0], offsets.BaseOffset,) + args[2:]
if len(args) == 3 and args[2] is None:
args = args[:2] + (1,)
# kludge
try:
stack[-1] = func(*args)
return
except:
pass


# unknown exception, re-raise
if getattr(self, 'is_verbose', None):
print(sys.exc_info())
Expand Down
30 changes: 18 additions & 12 deletions pandas/tests/tseries/test_offsets.py
Original file line number Diff line number Diff line change
Expand Up @@ -539,8 +539,7 @@ def setup_method(self, method):
def test_different_normalize_equals(self):
# equivalent in this special case
offset = BDay()
offset2 = BDay()
offset2.normalize = True
offset2 = BDay(normalize=True)
assert offset == offset2

def test_repr(self):
Expand Down Expand Up @@ -734,8 +733,7 @@ def test_constructor_errors(self):
def test_different_normalize_equals(self):
# equivalent in this special case
offset = self._offset()
offset2 = self._offset()
offset2.normalize = True
offset2 = self._offset(normalize=True)
assert offset == offset2

def test_repr(self):
Expand Down Expand Up @@ -1426,8 +1424,7 @@ def test_constructor_errors(self):
def test_different_normalize_equals(self):
# equivalent in this special case
offset = self._offset()
offset2 = self._offset()
offset2.normalize = True
offset2 = self._offset(normalize=True)
assert offset == offset2

def test_repr(self):
Expand Down Expand Up @@ -1667,8 +1664,7 @@ def setup_method(self, method):
def test_different_normalize_equals(self):
# equivalent in this special case
offset = CDay()
offset2 = CDay()
offset2.normalize = True
offset2 = CDay(normalize=True)
assert offset == offset2

def test_repr(self):
Expand Down Expand Up @@ -1953,8 +1949,7 @@ class TestCustomBusinessMonthEnd(CustomBusinessMonthBase, Base):
def test_different_normalize_equals(self):
# equivalent in this special case
offset = CBMonthEnd()
offset2 = CBMonthEnd()
offset2.normalize = True
offset2 = CBMonthEnd(normalize=True)
assert offset == offset2

def test_repr(self):
Expand Down Expand Up @@ -2067,8 +2062,7 @@ class TestCustomBusinessMonthBegin(CustomBusinessMonthBase, Base):
def test_different_normalize_equals(self):
# equivalent in this special case
offset = CBMonthBegin()
offset2 = CBMonthBegin()
offset2.normalize = True
offset2 = CBMonthBegin(normalize=True)
assert offset == offset2

def test_repr(self):
Expand Down Expand Up @@ -4899,3 +4893,15 @@ 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


def test_date_offset_immutable():
offset = offsets.MonthBegin(n=2, normalize=True)
with pytest.raises(AttributeError):
offset.n = 1

# Check that it didn't get changed
assert offset.n == 2

with pytest.raises(AttributeError):
offset.normalize = False
Loading