Skip to content

Commit e30971a

Browse files
authored
REF: move apply_wraps up into liboffstes (#34062)
1 parent d47e94a commit e30971a

File tree

3 files changed

+104
-89
lines changed

3 files changed

+104
-89
lines changed

pandas/_libs/tslibs/offsets.pyx

+101-2
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import warnings
66
from cpython.datetime cimport (PyDateTime_IMPORT,
77
PyDateTime_Check,
88
PyDelta_Check,
9-
datetime, timedelta,
9+
datetime, timedelta, date,
1010
time as dt_time)
1111
PyDateTime_IMPORT
1212

@@ -29,12 +29,13 @@ from pandas._libs.tslibs.conversion cimport (
2929
convert_datetime_to_tsobject,
3030
localize_pydatetime,
3131
)
32-
from pandas._libs.tslibs.nattype cimport NPY_NAT
32+
from pandas._libs.tslibs.nattype cimport NPY_NAT, c_NaT as NaT
3333
from pandas._libs.tslibs.np_datetime cimport (
3434
npy_datetimestruct, dtstruct_to_dt64, dt64_to_dtstruct)
3535
from pandas._libs.tslibs.timezones cimport utc_pytz as UTC
3636
from pandas._libs.tslibs.tzconversion cimport tz_convert_single
3737

38+
from pandas._libs.tslibs.timestamps import Timestamp
3839

3940
# ---------------------------------------------------------------------
4041
# Constants
@@ -154,6 +155,64 @@ def apply_index_wraps(func):
154155
return wrapper
155156

156157

158+
def apply_wraps(func):
159+
# Note: normally we would use `@functools.wraps(func)`, but this does
160+
# not play nicely with cython class methods
161+
162+
def wrapper(self, other):
163+
if other is NaT:
164+
return NaT
165+
elif isinstance(other, (timedelta, BaseOffset)):
166+
# timedelta path
167+
return func(self, other)
168+
elif isinstance(other, (np.datetime64, datetime, date)):
169+
other = Timestamp(other)
170+
else:
171+
# This will end up returning NotImplemented back in __add__
172+
raise ApplyTypeError
173+
174+
tz = other.tzinfo
175+
nano = other.nanosecond
176+
177+
if self._adjust_dst:
178+
other = other.tz_localize(None)
179+
180+
result = func(self, other)
181+
182+
result = Timestamp(result)
183+
if self._adjust_dst:
184+
result = result.tz_localize(tz)
185+
186+
if self.normalize:
187+
result = result.normalize()
188+
189+
# nanosecond may be deleted depending on offset process
190+
if not self.normalize and nano != 0:
191+
if result.nanosecond != nano:
192+
if result.tz is not None:
193+
# convert to UTC
194+
value = result.tz_localize(None).value
195+
else:
196+
value = result.value
197+
result = Timestamp(value + nano)
198+
199+
if tz is not None and result.tzinfo is None:
200+
result = result.tz_localize(tz)
201+
202+
return result
203+
204+
# do @functools.wraps(func) manually since it doesn't work on cdef funcs
205+
wrapper.__name__ = func.__name__
206+
wrapper.__doc__ = func.__doc__
207+
try:
208+
wrapper.__module__ = func.__module__
209+
except AttributeError:
210+
# AttributeError: 'method_descriptor' object has no
211+
# attribute '__module__'
212+
pass
213+
return wrapper
214+
215+
157216
cdef _wrap_timedelta_result(result):
158217
"""
159218
Tick operations dispatch to their Timedelta counterparts. Wrap the result
@@ -348,6 +407,10 @@ class _BaseOffset:
348407
_typ = "dateoffset"
349408
_day_opt = None
350409
_attributes = frozenset(['n', 'normalize'])
410+
_use_relativedelta = False
411+
_adjust_dst = False
412+
_deprecations = frozenset(["isAnchored", "onOffset"])
413+
normalize = False # default for prior pickles
351414

352415
def __init__(self, n=1, normalize=False):
353416
n = self._validate_n(n)
@@ -503,11 +566,41 @@ class _BaseOffset:
503566

504567
# ------------------------------------------------------------------
505568

569+
def rollback(self, dt):
570+
"""
571+
Roll provided date backward to next offset only if not on offset.
572+
573+
Returns
574+
-------
575+
TimeStamp
576+
Rolled timestamp if not on offset, otherwise unchanged timestamp.
577+
"""
578+
dt = Timestamp(dt)
579+
if not self.is_on_offset(dt):
580+
dt = dt - type(self)(1, normalize=self.normalize, **self.kwds)
581+
return dt
582+
583+
def rollforward(self, dt):
584+
"""
585+
Roll provided date forward to next offset only if not on offset.
586+
587+
Returns
588+
-------
589+
TimeStamp
590+
Rolled timestamp if not on offset, otherwise unchanged timestamp.
591+
"""
592+
dt = Timestamp(dt)
593+
if not self.is_on_offset(dt):
594+
dt = dt + type(self)(1, normalize=self.normalize, **self.kwds)
595+
return dt
596+
506597
def _get_offset_day(self, datetime other):
507598
# subclass must implement `_day_opt`; calling from the base class
508599
# will raise NotImplementedError.
509600
return get_day_of_month(other, self._day_opt)
510601

602+
# ------------------------------------------------------------------
603+
511604
def _validate_n(self, n):
512605
"""
513606
Require that `n` be an integer.
@@ -620,6 +713,12 @@ cdef class _Tick(ABCTick):
620713
# ensure that reversed-ops with numpy scalars return NotImplemented
621714
__array_priority__ = 1000
622715

716+
def is_on_offset(self, dt) -> bool:
717+
return True
718+
719+
def is_anchored(self) -> bool:
720+
return False
721+
623722
def __truediv__(self, other):
624723
if not isinstance(self, _Tick):
625724
# cython semantics mean the args are sometimes swapped

pandas/_libs/tslibs/timedeltas.pyx

+1-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ from pandas._libs.tslibs.np_datetime cimport (
3131
from pandas._libs.tslibs.nattype import nat_strings
3232
from pandas._libs.tslibs.nattype cimport (
3333
checknull_with_nat, NPY_NAT, c_NaT as NaT)
34-
from pandas._libs.tslibs.offsets cimport to_offset
3534

3635
# ----------------------------------------------------------------------
3736
# Constants
@@ -1273,6 +1272,7 @@ class Timedelta(_Timedelta):
12731272
cdef:
12741273
int64_t result, unit
12751274

1275+
from pandas.tseries.frequencies import to_offset
12761276
unit = to_offset(freq).nanos
12771277
result = unit * rounder(self.value / float(unit))
12781278
return Timedelta(result, unit='ns')

pandas/tseries/offsets.py

+2-86
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
from datetime import date, datetime, timedelta
2-
import functools
32
import operator
43
from typing import Any, Optional
54

@@ -23,6 +22,7 @@
2322
BusinessMixin,
2423
CustomMixin,
2524
apply_index_wraps,
25+
apply_wraps,
2626
as_datetime,
2727
is_normalized,
2828
roll_yearday,
@@ -74,53 +74,6 @@
7474
]
7575

7676

77-
def apply_wraps(func):
78-
@functools.wraps(func)
79-
def wrapper(self, other):
80-
if other is NaT:
81-
return NaT
82-
elif isinstance(other, (timedelta, DateOffset)):
83-
# timedelta path
84-
return func(self, other)
85-
elif isinstance(other, (np.datetime64, datetime, date)):
86-
other = Timestamp(other)
87-
else:
88-
# This will end up returning NotImplemented back in __add__
89-
raise ApplyTypeError
90-
91-
tz = other.tzinfo
92-
nano = other.nanosecond
93-
94-
if self._adjust_dst:
95-
other = other.tz_localize(None)
96-
97-
result = func(self, other)
98-
99-
result = Timestamp(result)
100-
if self._adjust_dst:
101-
result = result.tz_localize(tz)
102-
103-
if self.normalize:
104-
result = result.normalize()
105-
106-
# nanosecond may be deleted depending on offset process
107-
if not self.normalize and nano != 0:
108-
if not isinstance(self, Nano) and result.nanosecond != nano:
109-
if result.tz is not None:
110-
# convert to UTC
111-
value = result.tz_localize(None).value
112-
else:
113-
value = result.value
114-
result = Timestamp(value + nano)
115-
116-
if tz is not None and result.tzinfo is None:
117-
result = result.tz_localize(tz)
118-
119-
return result
120-
121-
return wrapper
122-
123-
12477
# ---------------------------------------------------------------------
12578
# DateOffset
12679

@@ -222,13 +175,7 @@ def __add__(date):
222175

223176
_params = cache_readonly(BaseOffset._params.fget)
224177
freqstr = cache_readonly(BaseOffset.freqstr.fget)
225-
_use_relativedelta = False
226-
_adjust_dst = False
227178
_attributes = frozenset(["n", "normalize"] + list(liboffsets.relativedelta_kwds))
228-
_deprecations = frozenset(["isAnchored", "onOffset"])
229-
230-
# default for prior pickles
231-
normalize = False
232179

233180
def __init__(self, n=1, normalize=False, **kwds):
234181
BaseOffset.__init__(self, n, normalize)
@@ -357,39 +304,11 @@ def _repr_attrs(self) -> str:
357304
out += ": " + ", ".join(attrs)
358305
return out
359306

360-
def rollback(self, dt):
361-
"""
362-
Roll provided date backward to next offset only if not on offset.
363-
364-
Returns
365-
-------
366-
TimeStamp
367-
Rolled timestamp if not on offset, otherwise unchanged timestamp.
368-
"""
369-
dt = Timestamp(dt)
370-
if not self.is_on_offset(dt):
371-
dt = dt - type(self)(1, normalize=self.normalize, **self.kwds)
372-
return dt
373-
374-
def rollforward(self, dt):
375-
"""
376-
Roll provided date forward to next offset only if not on offset.
377-
378-
Returns
379-
-------
380-
TimeStamp
381-
Rolled timestamp if not on offset, otherwise unchanged timestamp.
382-
"""
383-
dt = Timestamp(dt)
384-
if not self.is_on_offset(dt):
385-
dt = dt + type(self)(1, normalize=self.normalize, **self.kwds)
386-
return dt
387-
388307
def is_on_offset(self, dt):
389308
if self.normalize and not is_normalized(dt):
390309
return False
391310
# TODO, see #1395
392-
if type(self) == DateOffset or isinstance(self, Tick):
311+
if type(self) is DateOffset:
393312
return True
394313

395314
# Default (slow) method for determining if some date is a member of the
@@ -2486,9 +2405,6 @@ def apply(self, other):
24862405

24872406
raise ApplyTypeError(f"Unhandled type: {type(other).__name__}")
24882407

2489-
def is_anchored(self) -> bool:
2490-
return False
2491-
24922408

24932409
def delta_to_tick(delta: timedelta) -> Tick:
24942410
if delta.microseconds == 0 and getattr(delta, "nanoseconds", 0) == 0:

0 commit comments

Comments
 (0)