diff --git a/pandas/_libs/tslibs/__init__.py b/pandas/_libs/tslibs/__init__.py index 25e2d8ba477e0..6dbb4ce7bc974 100644 --- a/pandas/_libs/tslibs/__init__.py +++ b/pandas/_libs/tslibs/__init__.py @@ -1,4 +1,5 @@ __all__ = [ + "dtypes", "localize_pydatetime", "NaT", "NaTType", @@ -17,7 +18,7 @@ "to_offset", ] - +from . import dtypes # type: ignore from .conversion import localize_pydatetime from .nattype import NaT, NaTType, iNaT, is_null_datetimelike, nat_strings from .np_datetime import OutOfBoundsDatetime diff --git a/pandas/_libs/tslibs/dtypes.pxd b/pandas/_libs/tslibs/dtypes.pxd index 23c473726e5a9..b6373550b1c78 100644 --- a/pandas/_libs/tslibs/dtypes.pxd +++ b/pandas/_libs/tslibs/dtypes.pxd @@ -50,7 +50,9 @@ cdef enum PeriodDtypeCode: U = 11000 # Microsecondly N = 12000 # Nanosecondly + UNDEFINED = -10_000 -cdef class PeriodPseudoDtype: + +cdef class PeriodDtypeBase: cdef readonly: PeriodDtypeCode dtype_code diff --git a/pandas/_libs/tslibs/dtypes.pyx b/pandas/_libs/tslibs/dtypes.pyx index d0d4e579a456b..047f942178179 100644 --- a/pandas/_libs/tslibs/dtypes.pyx +++ b/pandas/_libs/tslibs/dtypes.pyx @@ -2,7 +2,7 @@ # originals -cdef class PeriodPseudoDtype: +cdef class PeriodDtypeBase: """ Similar to an actual dtype, this contains all of the information describing a PeriodDtype in an integer code. @@ -14,9 +14,9 @@ cdef class PeriodPseudoDtype: self.dtype_code = code def __eq__(self, other): - if not isinstance(other, PeriodPseudoDtype): + if not isinstance(other, PeriodDtypeBase): return False - if not isinstance(self, PeriodPseudoDtype): + if not isinstance(self, PeriodDtypeBase): # cython semantics, this is a reversed op return False return self.dtype_code == other.dtype_code diff --git a/pandas/_libs/tslibs/offsets.pyx b/pandas/_libs/tslibs/offsets.pyx index 28ead3593cf85..0967fa00e9e62 100644 --- a/pandas/_libs/tslibs/offsets.pyx +++ b/pandas/_libs/tslibs/offsets.pyx @@ -2528,12 +2528,10 @@ cdef class Week(SingleConstructorOffset): ------- result : DatetimeIndex """ - from .frequencies import get_freq_code # TODO: avoid circular import - i8other = dtindex.asi8 off = (i8other % DAY_NANOS).view("timedelta64[ns]") - base, mult = get_freq_code(self.freqstr) + base = self._period_dtype_code base_period = dtindex.to_period(base) if self.n > 0: diff --git a/pandas/_libs/tslibs/period.pyx b/pandas/_libs/tslibs/period.pyx index 5c890c7fbf59d..a436bd7f0c9ed 100644 --- a/pandas/_libs/tslibs/period.pyx +++ b/pandas/_libs/tslibs/period.pyx @@ -56,7 +56,7 @@ from pandas._libs.tslibs.ccalendar cimport ( ) from pandas._libs.tslibs.ccalendar cimport c_MONTH_NUMBERS -from pandas._libs.tslibs.dtypes cimport PeriodPseudoDtype +from pandas._libs.tslibs.dtypes cimport PeriodDtypeBase from pandas._libs.tslibs.frequencies cimport ( attrname_to_abbrevs, @@ -1517,7 +1517,7 @@ cdef class _Period: cdef readonly: int64_t ordinal - PeriodPseudoDtype _dtype + PeriodDtypeBase _dtype BaseOffset freq def __cinit__(self, int64_t ordinal, BaseOffset freq): @@ -1526,7 +1526,7 @@ cdef class _Period: # Note: this is more performant than PeriodDtype.from_date_offset(freq) # because from_date_offset cannot be made a cdef method (until cython # supported cdef classmethods) - self._dtype = PeriodPseudoDtype(freq._period_dtype_code) + self._dtype = PeriodDtypeBase(freq._period_dtype_code) @classmethod def _maybe_convert_freq(cls, object freq) -> BaseOffset: @@ -2463,7 +2463,7 @@ class Period(_Period): raise ValueError(msg) if ordinal is None: - base, mult = get_freq_code(freq) + base, _ = get_freq_code(freq) ordinal = period_ordinal(dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second, dt.microsecond, 0, base) diff --git a/pandas/core/arrays/period.py b/pandas/core/arrays/period.py index 1b8a0b2780a7d..b16a3df003512 100644 --- a/pandas/core/arrays/period.py +++ b/pandas/core/arrays/period.py @@ -52,7 +52,7 @@ def _field_accessor(name: str, docstring=None): def f(self): - base, _ = libfrequencies.get_freq_code(self.freq) + base = self.freq._period_dtype_code result = get_period_field_arr(name, self.asi8, base) return result @@ -440,12 +440,12 @@ def to_timestamp(self, freq=None, how="start"): return (self + self.freq).to_timestamp(how="start") - adjust if freq is None: - base, mult = libfrequencies.get_freq_code(self.freq) + base = self.freq._period_dtype_code freq = libfrequencies.get_to_timestamp_base(base) else: freq = Period._maybe_convert_freq(freq) - base, mult = libfrequencies.get_freq_code(freq) + base, _ = libfrequencies.get_freq_code(freq) new_data = self.asfreq(freq, how=how) new_data = libperiod.periodarr_to_dt64arr(new_data.asi8, base) @@ -523,14 +523,14 @@ def asfreq(self, freq=None, how: str = "E") -> "PeriodArray": freq = Period._maybe_convert_freq(freq) - base1, mult1 = libfrequencies.get_freq_code(self.freq) - base2, mult2 = libfrequencies.get_freq_code(freq) + base1 = self.freq._period_dtype_code + base2 = freq._period_dtype_code asi8 = self.asi8 - # mult1 can't be negative or 0 + # self.freq.n can't be negative or 0 end = how == "E" if end: - ordinal = asi8 + mult1 - 1 + ordinal = asi8 + self.freq.n - 1 else: ordinal = asi8 @@ -950,7 +950,7 @@ def dt64arr_to_periodarr(data, freq, tz=None): if isinstance(data, (ABCIndexClass, ABCSeries)): data = data._values - base, mult = libfrequencies.get_freq_code(freq) + base = freq._period_dtype_code return libperiod.dt64arr_to_periodarr(data.view("i8"), base, tz), freq diff --git a/pandas/core/dtypes/dtypes.py b/pandas/core/dtypes/dtypes.py index 84284c581c9e5..b9d16ac5959e3 100644 --- a/pandas/core/dtypes/dtypes.py +++ b/pandas/core/dtypes/dtypes.py @@ -20,7 +20,7 @@ import pytz from pandas._libs.interval import Interval -from pandas._libs.tslibs import NaT, Period, Timestamp, timezones, to_offset +from pandas._libs.tslibs import NaT, Period, Timestamp, dtypes, timezones, to_offset from pandas._libs.tslibs.offsets import BaseOffset from pandas._typing import DtypeObj, Ordered @@ -848,7 +848,7 @@ def __setstate__(self, state) -> None: @register_extension_dtype -class PeriodDtype(PandasExtensionDtype): +class PeriodDtype(dtypes.PeriodDtypeBase, PandasExtensionDtype): """ An ExtensionDtype for Period data. @@ -896,7 +896,8 @@ def __new__(cls, freq=None): elif freq is None: # empty constructor for pickle compat - u = object.__new__(cls) + # -10_000 corresponds to PeriodDtypeCode.UNDEFINED + u = dtypes.PeriodDtypeBase.__new__(cls, -10_000) u._freq = None return u @@ -906,11 +907,15 @@ def __new__(cls, freq=None): try: return cls._cache[freq.freqstr] except KeyError: - u = object.__new__(cls) + dtype_code = freq._period_dtype_code + u = dtypes.PeriodDtypeBase.__new__(cls, dtype_code) u._freq = freq cls._cache[freq.freqstr] = u return u + def __reduce__(self): + return type(self), (self.freq,) + @property def freq(self): """ @@ -977,7 +982,7 @@ def __eq__(self, other: Any) -> bool: return isinstance(other, PeriodDtype) and self.freq == other.freq def __setstate__(self, state): - # for pickle compat. __get_state__ is defined in the + # for pickle compat. __getstate__ is defined in the # PandasExtensionDtype superclass and uses the public properties to # pickle -> need to set the settable private ones here (see GH26067) self._freq = state["freq"] diff --git a/pandas/plotting/_matplotlib/timeseries.py b/pandas/plotting/_matplotlib/timeseries.py index 475452c71db58..a9cca32271b9f 100644 --- a/pandas/plotting/_matplotlib/timeseries.py +++ b/pandas/plotting/_matplotlib/timeseries.py @@ -6,7 +6,7 @@ import numpy as np from pandas._libs.tslibs import Period, to_offset -from pandas._libs.tslibs.frequencies import FreqGroup, base_and_stride, get_freq_code +from pandas._libs.tslibs.frequencies import FreqGroup, base_and_stride from pandas._typing import FrameOrSeriesUnion from pandas.core.dtypes.generic import ( @@ -213,7 +213,7 @@ def _use_dynamic_x(ax, data: "FrameOrSeriesUnion") -> bool: # FIXME: hack this for 0.10.1, creating more technical debt...sigh if isinstance(data.index, ABCDatetimeIndex): - base = get_freq_code(freq)[0] + base = to_offset(freq)._period_dtype_code x = data.index if base <= FreqGroup.FR_DAY: return x[:1].is_normalized diff --git a/pandas/tests/dtypes/test_dtypes.py b/pandas/tests/dtypes/test_dtypes.py index 2684aa2c590d9..3b9d3dc0b91f6 100644 --- a/pandas/tests/dtypes/test_dtypes.py +++ b/pandas/tests/dtypes/test_dtypes.py @@ -67,7 +67,11 @@ def test_pickle(self, dtype): # force back to the cache result = tm.round_trip_pickle(dtype) - assert not len(dtype._cache) + if not isinstance(dtype, PeriodDtype): + # Because PeriodDtype has a cython class as a base class, + # it has different pickle semantics, and its cache is re-populated + # on un-pickling. + assert not len(dtype._cache) assert result == dtype