11"""Method decorator helpers."""
22
33import functools
4+ import warnings
45import weakref
56
67
78def warn_classmethod (stacklevel ):
8- from warnings import warn
9-
10- warn (
9+ warnings .warn (
1110 "decorating class methods with @cachedmethod is deprecated" ,
1211 DeprecationWarning ,
1312 stacklevel = stacklevel ,
1413 )
1514
1615
16+ def warn_instance_dict (msg , stacklevel ):
17+ warnings .warn (
18+ msg ,
19+ DeprecationWarning ,
20+ stacklevel = stacklevel ,
21+ )
22+
23+
1724class WrapperBase :
1825 def __init__ (self , obj , method , cache , key , lock = None , cond = None ):
19- if type (obj ) is type :
26+ if isinstance (obj , type ) :
2027 warn_classmethod (stacklevel = 5 )
2128 functools .update_wrapper (self , method )
2229 self ._obj = obj # protected
@@ -37,7 +44,7 @@ def cache(self):
3744
3845 @property
3946 def cache_key (self ):
40- return self .__key # TODO: how to handle self?
47+ return self .__key
4148
4249 @property
4350 def cache_lock (self ):
@@ -49,43 +56,58 @@ def cache_condition(self):
4956
5057
5158class DescriptorBase :
52- def __init__ (self , wrapper , cache_clear ):
59+ def __init__ (self , warndict = False ):
5360 self .__attrname = None
54- self .__wrapper = wrapper
55- self .__cache_clear = cache_clear
61+ self .__warndict = warndict
5662
5763 def __set_name__ (self , owner , name ):
5864 if self .__attrname is None :
5965 self .__attrname = name
60- elif name != self .__attrname : # pragma: no cover
66+ elif name != self .__attrname :
6167 raise TypeError (
6268 "Cannot assign the same @cachedmethod to two different names "
6369 f"({ self .__attrname !r} and { name !r} )."
6470 )
6571
6672 def __get__ (self , obj , objtype = None ):
67- if obj is None :
68- return self # deprecated @classmethod
6973 wrapper = self .Wrapper (obj )
7074 if self .__attrname is not None :
7175 try :
7276 wrapper = obj .__dict__ .setdefault (self .__attrname , wrapper )
73- except AttributeError : # pragma: no cover
77+ except AttributeError :
7478 # not all objects have __dict__ (e.g. class defines slots)
7579 msg = (
7680 f"No '__dict__' attribute on { type (obj ).__name__ !r} "
7781 f"instance to cache { self .__attrname !r} property."
7882 )
79- raise TypeError (msg ) from None
80- except TypeError : # pragma: no cover
83+ if self .__warndict :
84+ warn_instance_dict (msg , 3 )
85+ else :
86+ raise TypeError (msg ) from None
87+ except TypeError :
8188 msg = (
8289 f"The '__dict__' attribute on { type (obj ).__name__ !r} "
8390 f"instance does not support item assignment for "
8491 f"caching { self .__attrname !r} property."
8592 )
86- raise TypeError (msg ) from None
93+ if self .__warndict :
94+ warn_instance_dict (msg , 3 )
95+ else :
96+ raise TypeError (msg ) from None
97+ elif self .__deprecated :
98+ pass # deprecated @classmethod, warning already raised elsewhere
99+ else :
100+ msg = "Cannot use @cachedmethod instance without calling __set_name__ on it"
101+ raise TypeError (msg ) from None
87102 return wrapper
88103
104+
105+ class DeprecatedDescriptorBase (DescriptorBase ):
106+ def __init__ (self , wrapper , cache_clear ):
107+ super ().__init__ (True )
108+ self .__wrapper = wrapper
109+ self .__cache_clear = cache_clear
110+
89111 # called for @classmethod with Python >= 3.13
90112 def __call__ (self , * args , ** kwargs ):
91113 warn_classmethod (stacklevel = 3 )
@@ -97,6 +119,127 @@ def cache_clear(self, objtype):
97119 return self .__cache_clear (objtype )
98120
99121
122+ def _condition_info (method , cache , key , lock , cond , info ):
123+ class Descriptor (DescriptorBase ):
124+ class Wrapper (WrapperBase ):
125+ def __init__ (self , obj ):
126+ super ().__init__ (obj , method , cache , key , lock , cond )
127+ self .__hits = self .__misses = 0
128+ self .__pending = set ()
129+
130+ def __call__ (self , * args , ** kwargs ):
131+ cache = self .cache
132+ lock = self .cache_lock
133+ cond = self .cache_condition
134+ key = self .cache_key (self ._obj , * args , ** kwargs )
135+
136+ with lock :
137+ cond .wait_for (lambda : key not in self .__pending )
138+ try :
139+ result = cache [key ]
140+ self .__hits += 1
141+ return result
142+ except KeyError :
143+ self .__pending .add (key )
144+ self .__misses += 1
145+ try :
146+ val = method (self ._obj , * args , ** kwargs )
147+ with lock :
148+ try :
149+ cache [key ] = val
150+ except ValueError :
151+ pass # value too large
152+ return val
153+ finally :
154+ with lock :
155+ self .__pending .remove (key )
156+ cond .notify_all ()
157+
158+ def cache_clear (self ):
159+ with self .cache_lock :
160+ self .cache .clear ()
161+ self .__hits = self .__misses = 0
162+
163+ def cache_info (self ):
164+ with self .cache_lock :
165+ return info (self .cache , self .__hits , self .__misses )
166+
167+ return Descriptor ()
168+
169+
170+ def _locked_info (method , cache , key , lock , info ):
171+ class Descriptor (DescriptorBase ):
172+ class Wrapper (WrapperBase ):
173+ def __init__ (self , obj ):
174+ super ().__init__ (obj , method , cache , key , lock )
175+ self .__hits = self .__misses = 0
176+
177+ def __call__ (self , * args , ** kwargs ):
178+ cache = self .cache
179+ lock = self .cache_lock
180+ key = self .cache_key (self ._obj , * args , ** kwargs )
181+ with lock :
182+ try :
183+ result = cache [key ]
184+ self .__hits += 1
185+ return result
186+ except KeyError :
187+ self .__misses += 1
188+ val = method (self ._obj , * args , ** kwargs )
189+ with lock :
190+ try :
191+ # In case of a race condition, i.e. if another thread
192+ # stored a value for this key while we were calling
193+ # method(), prefer the cached value.
194+ return cache .setdefault (key , val )
195+ except ValueError :
196+ return val # value too large
197+
198+ def cache_clear (self ):
199+ with self .cache_lock :
200+ self .cache .clear ()
201+ self .__hits = self .__misses = 0
202+
203+ def cache_info (self ):
204+ with self .cache_lock :
205+ return info (self .cache , self .__hits , self .__misses )
206+
207+ return Descriptor ()
208+
209+
210+ def _unlocked_info (method , cache , key , info ):
211+ class Descriptor (DescriptorBase ):
212+ class Wrapper (WrapperBase ):
213+ def __init__ (self , obj ):
214+ super ().__init__ (obj , method , cache , key )
215+ self .__hits = self .__misses = 0
216+
217+ def __call__ (self , * args , ** kwargs ):
218+ cache = self .cache
219+ key = self .cache_key (self ._obj , * args , ** kwargs )
220+ try :
221+ result = cache [key ]
222+ self .__hits += 1
223+ return result
224+ except KeyError :
225+ self .__misses += 1
226+ val = method (self ._obj , * args , ** kwargs )
227+ try :
228+ cache [key ] = val
229+ except ValueError :
230+ pass # value too large
231+ return val
232+
233+ def cache_clear (self ):
234+ self .cache .clear ()
235+ self .__hits = self .__misses = 0
236+
237+ def cache_info (self ):
238+ return info (self .cache , self .__hits , self .__misses )
239+
240+ return Descriptor ()
241+
242+
100243def _condition (method , cache , key , lock , cond ):
101244 # backward-compatible weakref dictionary for Python >= 3.13
102245 pending = weakref .WeakKeyDictionary ()
@@ -132,7 +275,7 @@ def classmethod_wrapper(self, *args, **kwargs):
132275 p = pending .setdefault (self , set ())
133276 return wrapper (self , p , * args , ** kwargs )
134277
135- class Descriptor (DescriptorBase ):
278+ class Descriptor (DeprecatedDescriptorBase ):
136279 class Wrapper (WrapperBase ):
137280 def __init__ (self , obj ):
138281 super ().__init__ (obj , method , cache , key , lock , cond )
@@ -158,9 +301,9 @@ def wrapper(self, *args, **kwargs):
158301 except KeyError :
159302 pass # key not found
160303 v = method (self , * args , ** kwargs )
161- # in case of a race, prefer the item already in the cache
162304 with lock (self ):
163305 try :
306+ # possible race condition: see above
164307 return c .setdefault (k , v )
165308 except ValueError :
166309 return v # value too large
@@ -170,7 +313,7 @@ def cache_clear(self):
170313 with lock (self ):
171314 c .clear ()
172315
173- class Descriptor (DescriptorBase ):
316+ class Descriptor (DeprecatedDescriptorBase ):
174317 class Wrapper (WrapperBase ):
175318 def __init__ (self , obj ):
176319 super ().__init__ (obj , method , cache , key , lock )
@@ -204,7 +347,7 @@ def cache_clear(self):
204347 c = cache (self )
205348 c .clear ()
206349
207- class Descriptor (DescriptorBase ):
350+ class Descriptor (DeprecatedDescriptorBase ):
208351 class Wrapper (WrapperBase ):
209352 def __init__ (self , obj ):
210353 super ().__init__ (obj , method , cache , key )
@@ -219,17 +362,27 @@ def cache_clear(self, _objtype=None):
219362 return Descriptor (wrapper , cache_clear )
220363
221364
222- def _wrapper (method , cache , key , lock = None , cond = None ):
223- if cond is not None and lock is not None :
224- wrapper = _condition (method , cache , key , lock , cond )
225- elif cond is not None :
226- wrapper = _condition (method , cache , key , cond , cond )
227- elif lock is not None :
228- wrapper = _locked (method , cache , key , lock )
365+ def _wrapper (method , cache , key , lock = None , cond = None , info = None ):
366+ if info is not None :
367+ if cond is not None and lock is not None :
368+ wrapper = _condition_info (method , cache , key , lock , cond , info )
369+ elif cond is not None :
370+ wrapper = _condition_info (method , cache , key , cond , cond , info )
371+ elif lock is not None :
372+ wrapper = _locked_info (method , cache , key , lock , info )
373+ else :
374+ wrapper = _unlocked_info (method , cache , key , info )
229375 else :
230- wrapper = _unlocked (method , cache , key )
231-
232- # backward-compatible properties for @classmethod
376+ if cond is not None and lock is not None :
377+ wrapper = _condition (method , cache , key , lock , cond )
378+ elif cond is not None :
379+ wrapper = _condition (method , cache , key , cond , cond )
380+ elif lock is not None :
381+ wrapper = _locked (method , cache , key , lock )
382+ else :
383+ wrapper = _unlocked (method , cache , key )
384+
385+ # backward-compatible properties for deprecated @classmethod use
233386 wrapper .cache = cache
234387 wrapper .cache_key = key
235388 wrapper .cache_lock = lock if lock is not None else cond
0 commit comments