Skip to content

Commit 7ccc9ca

Browse files
committed
bug: Ensure cache_readonly is only applied on instance
1 parent ce082fb commit 7ccc9ca

File tree

7 files changed

+58
-9
lines changed

7 files changed

+58
-9
lines changed

doc/source/whatsnew/v1.3.0.rst

+1
Original file line numberDiff line numberDiff line change
@@ -601,6 +601,7 @@ ExtensionArray
601601

602602
- Bug in :meth:`DataFrame.where` when ``other`` is a :class:`Series` with :class:`ExtensionArray` dtype (:issue:`38729`)
603603
- Fixed bug where :meth:`Series.idxmax`, :meth:`Series.idxmin` and ``argmax/min`` fail when the underlying data is :class:`ExtensionArray` (:issue:`32749`, :issue:`33719`, :issue:`36566`)
604+
- Bug where some properties of subclasses of :class:``PandasExtensionDtype`` where improperly cached :issue:`40329`
604605
-
605606

606607
Other

pandas/_libs/properties.pxd

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
2+
cdef class PropertyCacheMixin:
3+
4+
cdef public:
5+
dict _cache_property

pandas/_libs/properties.pyx

+12-6
Original file line numberDiff line numberDiff line change
@@ -10,25 +10,27 @@ from cpython.dict cimport (
1010
cdef class CachedProperty:
1111

1212
cdef readonly:
13-
object func, name, __doc__
13+
object func, name
1414

1515
def __init__(self, func):
1616
self.func = func
1717
self.name = func.__name__
18-
self.__doc__ = getattr(func, '__doc__', None)
1918

2019
def __get__(self, obj, typ):
2120
if obj is None:
2221
# accessed on the class, not the instance
23-
return self
22+
return self.func
2423

2524
# Get the cache or set a default one if needed
26-
cache = getattr(obj, '_cache', None)
25+
cache = getattr(obj, '_cache_property', None)
2726
if cache is None:
2827
try:
29-
cache = obj._cache = {}
28+
cache = obj._cache_property = {}
3029
except (AttributeError):
31-
return self
30+
raise TypeError(
31+
f"Cython extension type {type(obj)} must declare attribute "
32+
"`_cache_property` to use ``@cache_readonly``."
33+
)
3234

3335
if PyDict_Contains(cache, self.name):
3436
# not necessary to Py_INCREF
@@ -41,6 +43,10 @@ cdef class CachedProperty:
4143
def __set__(self, obj, value):
4244
raise AttributeError("Can't set attribute")
4345

46+
cdef class PropertyCacheMixin:
47+
48+
def __cinit__(self):
49+
self._cache_property = {}
4450

4551
cache_readonly = CachedProperty
4652

pandas/_libs/tslibs/offsets.pxd

+3-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
from numpy cimport int64_t
22

3+
from pandas._libs.properties cimport PropertyCacheMixin
4+
35

46
cpdef to_offset(object obj)
57
cdef bint is_offset_object(object obj)
68
cdef bint is_tick_object(object obj)
79

8-
cdef class BaseOffset:
10+
cdef class BaseOffset(PropertyCacheMixin):
911
cdef readonly:
1012
int64_t n
1113
bint normalize

pandas/_libs/tslibs/offsets.pyx

+2-1
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ cnp.import_array()
3333

3434
# TODO: formalize having _libs.properties "above" tslibs in the dependency structure
3535

36+
from pandas._libs.properties cimport PropertyCacheMixin
3637
from pandas._libs.properties import cache_readonly
3738

3839
from pandas._libs.tslibs cimport util
@@ -343,7 +344,7 @@ class ApplyTypeError(TypeError):
343344
# ---------------------------------------------------------------------
344345
# Base Classes
345346

346-
cdef class BaseOffset:
347+
cdef class BaseOffset(PropertyCacheMixin):
347348
"""
348349
Base class for DateOffset methods that are not overridden by subclasses.
349350
"""

pandas/core/series.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ class Series(base.IndexOpsMixin, generic.NDFrame):
268268

269269
# Override cache_readonly bc Series is mutable
270270
hasnans = property(
271-
base.IndexOpsMixin.hasnans.func, doc=base.IndexOpsMixin.hasnans.__doc__
271+
base.IndexOpsMixin.hasnans, doc=base.IndexOpsMixin.hasnans.__doc__
272272
)
273273
__hash__ = generic.NDFrame.__hash__
274274
_mgr: SingleManager

pandas/tests/libs/test_lib.py

+34
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
from pandas import Index
1111
import pandas._testing as tm
12+
from pandas.util import cache_readonly
1213

1314

1415
class TestMisc:
@@ -206,3 +207,36 @@ def test_get_reverse_indexer(self):
206207
def test_cache_readonly_preserve_docstrings():
207208
# GH18197
208209
assert Index.hasnans.__doc__ is not None
210+
211+
212+
def test_cache_readonly():
213+
"""
214+
Test that we do not accidentally cache the properties in a class attribute
215+
"""
216+
217+
class ClassWithoutCache:
218+
def __init__(self, prop):
219+
self.counter = 0
220+
self._prop = prop
221+
222+
@cache_readonly
223+
def expensive_property(self):
224+
self.counter += 1
225+
if self.counter > 1:
226+
raise RuntimeError("Called too many times")
227+
return self._prop
228+
229+
class ClassWithCache(ClassWithoutCache):
230+
_cache = {}
231+
232+
for typ in [ClassWithCache, ClassWithoutCache]:
233+
234+
instanceA = typ("A")
235+
236+
instanceB = typ("B")
237+
238+
assert instanceA.expensive_property == "A"
239+
assert instanceA.expensive_property == "A"
240+
assert instanceA.counter == 1
241+
242+
assert instanceB.expensive_property == "B"

0 commit comments

Comments
 (0)