Skip to content

Commit 26b0049

Browse files
committed
General improvements.
1 parent 1c825fe commit 26b0049

File tree

3 files changed

+62
-35
lines changed

3 files changed

+62
-35
lines changed

src/cachetools/_cached.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,14 @@
11
"""Function decorator helpers."""
22

3+
__all__ = ()
4+
35
import functools
46

7+
# At least for now, the implementation prefers clarity and performance
8+
# over ease of maintenance, thus providing separate wrappers for
9+
# all valid combinations of decorator parameters lock, condition and
10+
# info.
11+
512

613
def _condition_info(func, cache, key, lock, cond, info):
714
hits = misses = 0
@@ -178,7 +185,9 @@ def wrapper(*args, **kwargs):
178185
v = func(*args, **kwargs)
179186
with lock:
180187
try:
181-
# possible race condition: see above
188+
# In case of a race condition, i.e. if another thread
189+
# stored a value for this key while we were calling
190+
# func(), prefer the cached value.
182191
return cache.setdefault(k, v)
183192
except ValueError:
184193
return v # value too large

src/cachetools/_cachedmethod.py

Lines changed: 51 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,34 @@
11
"""Method decorator helpers."""
22

3+
__all__ = ()
4+
35
import functools
46
import warnings
57
import weakref
68

79

8-
def warn_classmethod(stacklevel):
10+
def _warn_classmethod(stacklevel):
911
warnings.warn(
1012
"decorating class methods with @cachedmethod is deprecated",
1113
DeprecationWarning,
1214
stacklevel=stacklevel,
1315
)
1416

1517

16-
def warn_instance_dict(msg, stacklevel):
18+
def _warn_instance_dict(msg, stacklevel):
1719
warnings.warn(
1820
msg,
1921
DeprecationWarning,
2022
stacklevel=stacklevel,
2123
)
2224

2325

24-
class WrapperBase:
26+
class _WrapperBase:
27+
"""Wrapper base class providing default implementations for properties."""
28+
2529
def __init__(self, obj, method, cache, key, lock=None, cond=None):
2630
if isinstance(obj, type):
27-
warn_classmethod(stacklevel=5)
31+
_warn_classmethod(stacklevel=5)
2832
functools.update_wrapper(self, method)
2933
self._obj = obj # protected
3034
self.__cache = cache
@@ -55,10 +59,12 @@ def cache_condition(self):
5559
return None if self.__cond is None else self.__cond(self._obj)
5660

5761

58-
class DescriptorBase:
59-
def __init__(self, warndict=False):
62+
class _DescriptorBase:
63+
"""Descriptor base class implementing the basic descriptor protocol."""
64+
65+
def __init__(self, deprecated=False):
6066
self.__attrname = None
61-
self.__warndict = warndict
67+
self.__deprecated = deprecated
6268

6369
def __set_name__(self, owner, name):
6470
if self.__attrname is None:
@@ -72,16 +78,19 @@ def __set_name__(self, owner, name):
7278
def __get__(self, obj, objtype=None):
7379
wrapper = self.Wrapper(obj)
7480
if self.__attrname is not None:
81+
# replace descriptor instance with wrapper in instance dict
7582
try:
83+
# In case of a race condition where another thread already replaced
84+
# the descriptor, prefer the initial wrapper.
7685
wrapper = obj.__dict__.setdefault(self.__attrname, wrapper)
7786
except AttributeError:
7887
# not all objects have __dict__ (e.g. class defines slots)
7988
msg = (
8089
f"No '__dict__' attribute on {type(obj).__name__!r} "
8190
f"instance to cache {self.__attrname!r} property."
8291
)
83-
if self.__warndict:
84-
warn_instance_dict(msg, 3)
92+
if self.__deprecated:
93+
_warn_instance_dict(msg, 3)
8594
else:
8695
raise TypeError(msg) from None
8796
except TypeError: # pragma: no cover
@@ -90,39 +99,46 @@ def __get__(self, obj, objtype=None):
9099
f"instance does not support item assignment for "
91100
f"caching {self.__attrname!r} property."
92101
)
93-
if self.__warndict:
94-
warn_instance_dict(msg, 3)
102+
if self.__deprecated:
103+
_warn_instance_dict(msg, 3)
95104
else:
96105
raise TypeError(msg) from None
97-
elif not self.__warndict: # pragma: no cover
98-
msg = (
99-
"Cannot use @cachedmethod instance without "
100-
"calling __set_name__ on it"
101-
)
106+
elif self.__deprecated:
107+
pass # deprecated @classmethod, warning already raised elsewhere
108+
else: # pragma: no cover
109+
msg = "Cannot use @cachedmethod instance without calling __set_name__ on it"
102110
raise TypeError(msg) from None
103111
return wrapper
104112

105113

106-
class DeprecatedDescriptorBase(DescriptorBase):
114+
class _DeprecatedDescriptorBase(_DescriptorBase):
115+
"""Descriptor base class supporting deprecated @classmethod use."""
116+
107117
def __init__(self, wrapper, cache_clear):
108-
super().__init__(True)
118+
super().__init__(deprecated=True)
109119
self.__wrapper = wrapper
110120
self.__cache_clear = cache_clear
111121

112122
# called for @classmethod with Python >= 3.13
113123
def __call__(self, *args, **kwargs):
114-
warn_classmethod(stacklevel=3)
124+
_warn_classmethod(stacklevel=3)
115125
return self.__wrapper(*args, **kwargs)
116126

117127
# backward-compatible @classmethod handling with Python >= 3.13
118128
def cache_clear(self, objtype):
119-
warn_classmethod(stacklevel=3)
129+
_warn_classmethod(stacklevel=3)
120130
return self.__cache_clear(objtype)
121131

122132

133+
# At least for now, the implementation prefers clarity and performance
134+
# over ease of maintenance, thus providing separate descriptors for
135+
# all valid combinations of decorator parameters lock, condition and
136+
# info.
137+
138+
123139
def _condition_info(method, cache, key, lock, cond, info):
124-
class Descriptor(DescriptorBase):
125-
class Wrapper(WrapperBase):
140+
class Descriptor(_DescriptorBase):
141+
class Wrapper(_WrapperBase):
126142
def __init__(self, obj):
127143
super().__init__(obj, method, cache, key, lock, cond)
128144
self.__hits = self.__misses = 0
@@ -169,8 +185,8 @@ def cache_info(self):
169185

170186

171187
def _locked_info(method, cache, key, lock, info):
172-
class Descriptor(DescriptorBase):
173-
class Wrapper(WrapperBase):
188+
class Descriptor(_DescriptorBase):
189+
class Wrapper(_WrapperBase):
174190
def __init__(self, obj):
175191
super().__init__(obj, method, cache, key, lock)
176192
self.__hits = self.__misses = 0
@@ -209,8 +225,8 @@ def cache_info(self):
209225

210226

211227
def _unlocked_info(method, cache, key, info):
212-
class Descriptor(DescriptorBase):
213-
class Wrapper(WrapperBase):
228+
class Descriptor(_DescriptorBase):
229+
class Wrapper(_WrapperBase):
214230
def __init__(self, obj):
215231
super().__init__(obj, method, cache, key)
216232
self.__hits = self.__misses = 0
@@ -276,8 +292,8 @@ def classmethod_wrapper(self, *args, **kwargs):
276292
p = pending.setdefault(self, set())
277293
return wrapper(self, p, *args, **kwargs)
278294

279-
class Descriptor(DeprecatedDescriptorBase):
280-
class Wrapper(WrapperBase):
295+
class Descriptor(_DeprecatedDescriptorBase):
296+
class Wrapper(_WrapperBase):
281297
def __init__(self, obj):
282298
super().__init__(obj, method, cache, key, lock, cond)
283299
self.__pending = set()
@@ -304,7 +320,9 @@ def wrapper(self, *args, **kwargs):
304320
v = method(self, *args, **kwargs)
305321
with lock(self):
306322
try:
307-
# possible race condition: see above
323+
# In case of a race condition, i.e. if another thread
324+
# stored a value for this key while we were calling
325+
# method(), prefer the cached value.
308326
return c.setdefault(k, v)
309327
except ValueError:
310328
return v # value too large
@@ -314,8 +332,8 @@ def cache_clear(self):
314332
with lock(self):
315333
c.clear()
316334

317-
class Descriptor(DeprecatedDescriptorBase):
318-
class Wrapper(WrapperBase):
335+
class Descriptor(_DeprecatedDescriptorBase):
336+
class Wrapper(_WrapperBase):
319337
def __init__(self, obj):
320338
super().__init__(obj, method, cache, key, lock)
321339

@@ -348,8 +366,8 @@ def cache_clear(self):
348366
c = cache(self)
349367
c.clear()
350368

351-
class Descriptor(DeprecatedDescriptorBase):
352-
class Wrapper(WrapperBase):
369+
class Descriptor(_DeprecatedDescriptorBase):
370+
class Wrapper(_WrapperBase):
353371
def __init__(self, obj):
354372
super().__init__(obj, method, cache, key)
355373

src/cachetools/keys.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ class _HashedTuple(tuple):
1111
1212
"""
1313

14-
__hashvalue = None
14+
__hashvalue = None # default value, set in instance on first use
1515

1616
def __hash__(self, hash=tuple.__hash__):
1717
hashvalue = self.__hashvalue

0 commit comments

Comments
 (0)