diff --git a/ci/deps/actions-310-numpydev.yaml b/ci/deps/actions-310-numpydev.yaml index 401be14aaca02..c2cdd38e8599f 100644 --- a/ci/deps/actions-310-numpydev.yaml +++ b/ci/deps/actions-310-numpydev.yaml @@ -16,7 +16,8 @@ dependencies: - pytz - pip - pip: - - cython==0.29.24 # GH#34014 + #- cython # TODO: don't install from master after Cython 3.0.0a11 is released + - "git+https://github.com/cython/cython.git@master" - "--extra-index-url https://pypi.anaconda.org/scipy-wheels-nightly/simple" - "--pre" - "numpy" diff --git a/pandas/_libs/interval.pyx b/pandas/_libs/interval.pyx index 4c8419b78e2b8..67be66af16ba2 100644 --- a/pandas/_libs/interval.pyx +++ b/pandas/_libs/interval.pyx @@ -404,6 +404,8 @@ cdef class Interval(IntervalMixin): ): return Interval(self.left + y, self.right + y, closed=self.closed) elif ( + # __radd__ pattern + # TODO(cython3): remove this isinstance(y, Interval) and ( isinstance(self, numbers.Number) @@ -414,6 +416,15 @@ cdef class Interval(IntervalMixin): return Interval(y.left + self, y.right + self, closed=y.closed) return NotImplemented + def __radd__(self, other): + if ( + isinstance(other, numbers.Number) + or PyDelta_Check(other) + or is_timedelta64_object(other) + ): + return Interval(self.left + other, self.right + other, closed=self.closed) + return NotImplemented + def __sub__(self, y): if ( isinstance(y, numbers.Number) @@ -427,9 +438,16 @@ cdef class Interval(IntervalMixin): if isinstance(y, numbers.Number): return Interval(self.left * y, self.right * y, closed=self.closed) elif isinstance(y, Interval) and isinstance(self, numbers.Number): + # __radd__ semantics + # TODO(cython3): remove this return Interval(y.left * self, y.right * self, closed=y.closed) return NotImplemented + def __rmul__(self, other): + if isinstance(other, numbers.Number): + return Interval(self.left * other, self.right * other, closed=self.closed) + return NotImplemented + def __truediv__(self, y): if isinstance(y, numbers.Number): return Interval(self.left / y, self.right / y, closed=self.closed) diff --git a/pandas/_libs/tslibs/nattype.pyx b/pandas/_libs/tslibs/nattype.pyx index 8ddbbfba49614..93687abdf9153 100644 --- a/pandas/_libs/tslibs/nattype.pyx +++ b/pandas/_libs/tslibs/nattype.pyx @@ -143,6 +143,7 @@ cdef class _NaT(datetime): def __add__(self, other): if self is not c_NaT: + # TODO(cython3): remove this it moved to __radd__ # cython __radd__ semantics self, other = other, self @@ -169,6 +170,9 @@ cdef class _NaT(datetime): # Includes Period, DateOffset going through here return NotImplemented + def __radd__(self, other): + return self.__add__(other) + def __sub__(self, other): # Duplicate some logic from _Timestamp.__sub__ to avoid needing # to subclass; allows us to @final(_Timestamp.__sub__) @@ -177,6 +181,7 @@ cdef class _NaT(datetime): if self is not c_NaT: # cython __rsub__ semantics + # TODO(cython3): remove __rsub__ logic from here self, other = other, self is_rsub = True @@ -200,6 +205,8 @@ cdef class _NaT(datetime): result.fill("NaT") return result + # __rsub__ logic here + # TODO(cython3): remove this, move above code out of ``if not is_rsub`` block # timedelta64 - NaT we have to treat NaT as timedelta64 # for this to be meaningful, and the result is timedelta64 result = np.empty(other.shape, dtype="timedelta64[ns]") @@ -220,6 +227,24 @@ cdef class _NaT(datetime): # Includes Period, DateOffset going through here return NotImplemented + def __rsub__(self, other): + if util.is_array(other): + if other.dtype.kind == "m": + # timedelta64 - NaT we have to treat NaT as timedelta64 + # for this to be meaningful, and the result is timedelta64 + result = np.empty(other.shape, dtype="timedelta64[ns]") + result.fill("NaT") + return result + + elif other.dtype.kind == "M": + # We treat NaT as a datetime, so regardless of whether this is + # NaT - other or other - NaT, the result is timedelta64 + result = np.empty(other.shape, dtype="timedelta64[ns]") + result.fill("NaT") + return result + # other cases are same, swap operands is allowed even though we subtract because this is NaT + return self.__sub__(other) + def __pos__(self): return NaT diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx index fef98199d3dbc..5a7f931292e7b 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -450,6 +450,7 @@ cdef class BaseOffset: def __add__(self, other): if not isinstance(self, BaseOffset): # cython semantics; this is __radd__ + # TODO(cython3): remove this, this moved to __radd__ return other.__add__(self) elif util.is_array(other) and other.dtype == object: @@ -460,6 +461,9 @@ cdef class BaseOffset: except ApplyTypeError: return NotImplemented + def __radd__(self, other): + return self.__add__(other) + def __sub__(self, other): if PyDateTime_Check(other): raise TypeError('Cannot subtract datetime from offset.') @@ -467,12 +471,16 @@ cdef class BaseOffset: return type(self)(self.n - other.n, normalize=self.normalize, **self.kwds) elif not isinstance(self, BaseOffset): + # TODO(cython3): remove, this moved to __rsub__ # cython semantics, this is __rsub__ return (-other).__add__(self) else: # e.g. PeriodIndex return NotImplemented + def __rsub__(self, other): + return (-self).__add__(other) + def __call__(self, other): warnings.warn( "DateOffset.__call__ is deprecated and will be removed in a future " @@ -499,10 +507,14 @@ cdef class BaseOffset: return type(self)(n=other * self.n, normalize=self.normalize, **self.kwds) elif not isinstance(self, BaseOffset): + # TODO(cython3): remove this, this moved to __rmul__ # cython semantics, this is __rmul__ return other.__mul__(self) return NotImplemented + def __rmul__(self, other): + return self.__mul__(other) + def __neg__(self): # Note: we are deferring directly to __mul__ instead of __rmul__, as # that allows us to use methods that can go in a `cdef class` @@ -887,6 +899,7 @@ cdef class Tick(SingleConstructorOffset): def __mul__(self, other): if not isinstance(self, Tick): + # TODO(cython3), remove this, this moved to __rmul__ # cython semantics, this is __rmul__ return other.__mul__(self) if is_float_object(other): @@ -900,6 +913,9 @@ cdef class Tick(SingleConstructorOffset): return new_self * other return BaseOffset.__mul__(self, other) + def __rmul__(self, other): + return self.__mul__(other) + def __truediv__(self, other): if not isinstance(self, Tick): # cython semantics mean the args are sometimes swapped @@ -908,9 +924,14 @@ cdef class Tick(SingleConstructorOffset): result = self.delta.__truediv__(other) return _wrap_timedelta_result(result) + def __rtruediv__(self, other): + result = self.delta.__rtruediv__(other) + return _wrap_timedelta_result(result) + def __add__(self, other): if not isinstance(self, Tick): # cython semantics; this is __radd__ + # TODO(cython3): remove this, this moved to __radd__ return other.__add__(self) if isinstance(other, Tick): @@ -928,6 +949,9 @@ cdef class Tick(SingleConstructorOffset): f"the add operation between {self} and {other} will overflow" ) from err + def __radd__(self, other): + return self.__add__(other) + def _apply(self, other): # Timestamp can handle tz and nano sec, thus no need to use apply_wraps if isinstance(other, _Timestamp): diff --git a/pandas/_libs/tslibs/period.pyx b/pandas/_libs/tslibs/period.pyx index 986bbd8c8f856..c18d71e324a28 100644 --- a/pandas/_libs/tslibs/period.pyx +++ b/pandas/_libs/tslibs/period.pyx @@ -1710,6 +1710,7 @@ cdef class _Period(PeriodMixin): def __add__(self, other): if not is_period_object(self): # cython semantics; this is analogous to a call to __radd__ + # TODO(cython3): remove this if self is NaT: return NaT return other.__add__(self) @@ -1734,9 +1735,13 @@ cdef class _Period(PeriodMixin): return NotImplemented + def __radd__(self, other): + return self.__add__(other) + def __sub__(self, other): if not is_period_object(self): # cython semantics; this is like a call to __rsub__ + # TODO(cython3): remove this if self is NaT: return NaT return NotImplemented @@ -1760,6 +1765,11 @@ cdef class _Period(PeriodMixin): return NotImplemented + def __rsub__(self, other): + if other is NaT: + return NaT + return NotImplemented + def asfreq(self, freq, how='E') -> "Period": """ Convert Period to desired frequency, at the start or end of the interval. diff --git a/pandas/_libs/tslibs/timestamps.pyx b/pandas/_libs/tslibs/timestamps.pyx index a0958e11e28b3..5ba28a9fae429 100644 --- a/pandas/_libs/tslibs/timestamps.pyx +++ b/pandas/_libs/tslibs/timestamps.pyx @@ -280,13 +280,19 @@ cdef class _Timestamp(ABCTimestamp): return other.tzinfo is not None return other.tzinfo is None + @cython.overflowcheck(True) def __add__(self, other): cdef: int64_t nanos = 0 if is_any_td_scalar(other): nanos = delta_to_nanoseconds(other) - result = type(self)(self.value + nanos, tz=self.tzinfo) + try: + result = type(self)(self.value + nanos, tz=self.tzinfo) + except OverflowError: + # Use Python ints + # Hit in test_tdi_add_overflow + result = type(self)(int(self.value) + int(nanos), tz=self.tzinfo) if result is not NaT: result._set_freq(self._freq) # avoid warning in constructor return result @@ -307,9 +313,16 @@ cdef class _Timestamp(ABCTimestamp): elif not isinstance(self, _Timestamp): # cython semantics, args have been switched and this is __radd__ + # TODO(cython3): remove this it moved to __radd__ return other.__add__(self) return NotImplemented + def __radd__(self, other): + # Have to duplicate checks to avoid infinite recursion due to NotImplemented + if is_any_td_scalar(other) or is_integer_object(other) or is_array(other): + return self.__add__(other) + return NotImplemented + def __sub__(self, other): if is_any_td_scalar(other) or is_integer_object(other): @@ -336,6 +349,7 @@ cdef class _Timestamp(ABCTimestamp): and (PyDateTime_Check(other) or is_datetime64_object(other))): # both_timestamps is to determine whether Timedelta(self - other) # should raise the OOB error, or fall back returning a timedelta. + # TODO(cython3): clean out the bits that moved to __rsub__ both_timestamps = (isinstance(other, _Timestamp) and isinstance(self, _Timestamp)) if isinstance(self, _Timestamp): @@ -366,10 +380,22 @@ cdef class _Timestamp(ABCTimestamp): elif is_datetime64_object(self): # GH#28286 cython semantics for __rsub__, `other` is actually # the Timestamp + # TODO(cython3): remove this, this moved to __rsub__ return type(other)(self) - other return NotImplemented + def __rsub__(self, other): + if PyDateTime_Check(other): + try: + return type(self)(other) - self + except (OverflowError, OutOfBoundsDatetime) as err: + # We get here in stata tests, fall back to stdlib datetime + # method and return stdlib timedelta object + pass + elif is_datetime64_object(other): + return type(self)(other) - self + return NotImplemented # ----------------------------------------------------------------- cdef int64_t _maybe_convert_value_to_local(self):