Skip to content

Commit b7b5903

Browse files
committed
fix cache_readonly
1 parent baa8539 commit b7b5903

File tree

3 files changed

+33
-16
lines changed

3 files changed

+33
-16
lines changed

pandas/_libs/properties.pyx

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,28 @@ cdef class CachedProperty:
2424

2525
# Get the cache or set a default one if needed
2626
cache = getattr(obj, '_cache', None)
27+
28+
if cache is not None:
29+
# When accessing cython extension types, the attribute is already
30+
# registered and known to the class, unlike for python object. To
31+
# ensure we're not accidentally using a global scope / class level
32+
# cache we'll need to check whether the instance and class
33+
# attribute is identical
34+
cache_class = getattr(typ, "_cache", None)
35+
if cache_class is not None and cache_class is cache:
36+
raise TypeError(
37+
f"Class {typ} defines a `_cache` attribute on class level "
38+
"which is forbidden in combination with @cache_readonly."
39+
)
40+
2741
if cache is None:
2842
try:
2943
cache = obj._cache = {}
3044
except (AttributeError):
31-
return self
45+
raise TypeError(
46+
f"Cython extension type {type(obj)} must declare attribute "
47+
"`_cache` to use @cache_readonly."
48+
)
3249

3350
if PyDict_Contains(cache, self.name):
3451
# not necessary to Py_INCREF

pandas/core/dtypes/dtypes.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ class PandasExtensionDtype(ExtensionDtype):
8686
base: Optional[DtypeObj] = None
8787
isbuiltin = 0
8888
isnative = 0
89-
_cache: Dict[str_type, PandasExtensionDtype] = {}
89+
_cache_dtypes: Dict[str_type, PandasExtensionDtype] = {}
9090

9191
def __str__(self) -> str_type:
9292
"""
@@ -110,7 +110,7 @@ def __getstate__(self) -> Dict[str_type, Any]:
110110
@classmethod
111111
def reset_cache(cls) -> None:
112112
""" clear the cache """
113-
cls._cache = {}
113+
cls._cache_dtypes = {}
114114

115115

116116
class CategoricalDtypeType(type):
@@ -182,7 +182,7 @@ class CategoricalDtype(PandasExtensionDtype, ExtensionDtype):
182182
str = "|O08"
183183
base = np.dtype("O")
184184
_metadata = ("categories", "ordered")
185-
_cache: Dict[str_type, PandasExtensionDtype] = {}
185+
_cache_dtypes: Dict[str_type, PandasExtensionDtype] = {}
186186

187187
def __init__(self, categories=None, ordered: Ordered = False):
188188
self._finalize(categories, ordered, fastpath=False)
@@ -678,7 +678,7 @@ class DatetimeTZDtype(PandasExtensionDtype):
678678
na_value = NaT
679679
_metadata = ("unit", "tz")
680680
_match = re.compile(r"(datetime64|M8)\[(?P<unit>.+), (?P<tz>.+)\]")
681-
_cache: Dict[str_type, PandasExtensionDtype] = {}
681+
_cache_dtypes: Dict[str_type, PandasExtensionDtype] = {}
682682

683683
def __init__(self, unit: Union[str_type, DatetimeTZDtype] = "ns", tz=None):
684684
if isinstance(unit, DatetimeTZDtype):
@@ -844,7 +844,7 @@ class PeriodDtype(dtypes.PeriodDtypeBase, PandasExtensionDtype):
844844
num = 102
845845
_metadata = ("freq",)
846846
_match = re.compile(r"(P|p)eriod\[(?P<freq>.+)\]")
847-
_cache: Dict[str_type, PandasExtensionDtype] = {}
847+
_cache_dtypes: Dict[str_type, PandasExtensionDtype] = {}
848848

849849
def __new__(cls, freq=None):
850850
"""
@@ -866,12 +866,12 @@ def __new__(cls, freq=None):
866866
freq = cls._parse_dtype_strict(freq)
867867

868868
try:
869-
return cls._cache[freq.freqstr]
869+
return cls._cache_dtypes[freq.freqstr]
870870
except KeyError:
871871
dtype_code = freq._period_dtype_code
872872
u = dtypes.PeriodDtypeBase.__new__(cls, dtype_code)
873873
u._freq = freq
874-
cls._cache[freq.freqstr] = u
874+
cls._cache_dtypes[freq.freqstr] = u
875875
return u
876876

877877
def __reduce__(self):
@@ -1049,7 +1049,7 @@ class IntervalDtype(PandasExtensionDtype):
10491049
_match = re.compile(
10501050
r"(I|i)nterval\[(?P<subtype>[^,]+)(, (?P<closed>(right|left|both|neither)))?\]"
10511051
)
1052-
_cache: Dict[str_type, PandasExtensionDtype] = {}
1052+
_cache_dtypes: Dict[str_type, PandasExtensionDtype] = {}
10531053

10541054
def __new__(cls, subtype=None, closed: Optional[str_type] = None):
10551055
from pandas.core.dtypes.common import (
@@ -1106,12 +1106,12 @@ def __new__(cls, subtype=None, closed: Optional[str_type] = None):
11061106

11071107
key = str(subtype) + str(closed)
11081108
try:
1109-
return cls._cache[key]
1109+
return cls._cache_dtypes[key]
11101110
except KeyError:
11111111
u = object.__new__(cls)
11121112
u._subtype = subtype
11131113
u._closed = closed
1114-
cls._cache[key] = u
1114+
cls._cache_dtypes[key] = u
11151115
return u
11161116

11171117
@property

pandas/tests/dtypes/test_dtypes.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -66,15 +66,15 @@ def test_pickle(self, dtype):
6666

6767
# clear the cache
6868
type(dtype).reset_cache()
69-
assert not len(dtype._cache)
69+
assert not len(dtype._cache_dtypes)
7070

7171
# force back to the cache
7272
result = tm.round_trip_pickle(dtype)
7373
if not isinstance(dtype, PeriodDtype):
7474
# Because PeriodDtype has a cython class as a base class,
7575
# it has different pickle semantics, and its cache is re-populated
7676
# on un-pickling.
77-
assert not len(dtype._cache)
77+
assert not len(dtype._cache_dtypes)
7878
assert result == dtype
7979

8080

@@ -791,14 +791,14 @@ def test_basic_dtype(self):
791791
def test_caching(self):
792792
IntervalDtype.reset_cache()
793793
dtype = IntervalDtype("int64", "right")
794-
assert len(IntervalDtype._cache) == 1
794+
assert len(IntervalDtype._cache_dtypes) == 1
795795

796796
IntervalDtype("interval")
797-
assert len(IntervalDtype._cache) == 2
797+
assert len(IntervalDtype._cache_dtypes) == 2
798798

799799
IntervalDtype.reset_cache()
800800
tm.round_trip_pickle(dtype)
801-
assert len(IntervalDtype._cache) == 0
801+
assert len(IntervalDtype._cache_dtypes) == 0
802802

803803
def test_not_string(self):
804804
# GH30568: though IntervalDtype has object kind, it cannot be string

0 commit comments

Comments
 (0)