44import weakref
55
66
7+ def warn_classmethod (stacklevel ):
8+ from warnings import warn
9+
10+ warn (
11+ "decorating class methods with @cachedmethod is deprecated" ,
12+ DeprecationWarning ,
13+ stacklevel = stacklevel ,
14+ )
15+
16+
17+ class WrapperBase :
18+ def __init__ (self , obj , method , cache , key , lock = None , cond = None ):
19+ if type (obj ) is type :
20+ warn_classmethod (stacklevel = 5 )
21+ functools .update_wrapper (self , method )
22+ self ._obj = obj # protected
23+ self .__cache = cache
24+ self .__key = key
25+ self .__lock = lock
26+ self .__cond = cond
27+
28+ def __call__ (self , * args , ** kwargs ):
29+ raise NotImplementedError () # pragma: no cover
30+
31+ def cache_clear (self ):
32+ raise NotImplementedError () # pragma: no cover
33+
34+ @property
35+ def cache (self ):
36+ return self .__cache (self ._obj )
37+
38+ @property
39+ def cache_key (self ):
40+ return self .__key # TODO: how to handle self?
41+
42+ @property
43+ def cache_lock (self ):
44+ return None if self .__lock is None else self .__lock (self ._obj )
45+
46+ @property
47+ def cache_condition (self ):
48+ return None if self .__cond is None else self .__cond (self ._obj )
49+
50+
51+ class DescriptorBase :
52+ def __init__ (self , wrapper , cache_clear ):
53+ self .__attrname = None
54+ self .__wrapper = wrapper
55+ self .__cache_clear = cache_clear
56+
57+ def __set_name__ (self , owner , name ):
58+ if self .__attrname is None :
59+ self .__attrname = name
60+ elif name != self .__attrname : # pragma: no cover
61+ raise TypeError (
62+ "Cannot assign the same @cachedmethod to two different names "
63+ f"({ self .__attrname !r} and { name !r} )."
64+ )
65+
66+ def __get__ (self , obj , objtype = None ):
67+ if obj is None :
68+ return self # deprecated @classmethod
69+ wrapper = self .Wrapper (obj )
70+ if self .__attrname is not None :
71+ try :
72+ wrapper = obj .__dict__ .setdefault (self .__attrname , wrapper )
73+ except AttributeError : # pragma: no cover
74+ # not all objects have __dict__ (e.g. class defines slots)
75+ msg = (
76+ f"No '__dict__' attribute on { type (obj ).__name__ !r} "
77+ f"instance to cache { self .__attrname !r} property."
78+ )
79+ raise TypeError (msg ) from None
80+ except TypeError : # pragma: no cover
81+ msg = (
82+ f"The '__dict__' attribute on { type (obj ).__name__ !r} "
83+ f"instance does not support item assignment for "
84+ f"caching { self .__attrname !r} property."
85+ )
86+ raise TypeError (msg ) from None
87+ return wrapper
88+
89+ # called for @classmethod with Python >= 3.13
90+ def __call__ (self , * args , ** kwargs ):
91+ warn_classmethod (stacklevel = 3 )
92+ return self .__wrapper (* args , ** kwargs )
93+
94+ # backward-compatible @classmethod handling with Python >= 3.13
95+ def cache_clear (self , objtype ):
96+ warn_classmethod (stacklevel = 3 )
97+ return self .__cache_clear (objtype )
98+
99+
7100def _condition (method , cache , key , lock , cond ):
101+ # backward-compatible weakref dictionary for Python >= 3.13
8102 pending = weakref .WeakKeyDictionary ()
9103
10- def wrapper (self , * args , ** kwargs ):
104+ def wrapper (self , pending , * args , ** kwargs ):
11105 c = cache (self )
12106 k = key (self , * args , ** kwargs )
13107 with lock (self ):
14- p = pending .setdefault (self , set ())
15- cond (self ).wait_for (lambda : k not in p )
108+ cond (self ).wait_for (lambda : k not in pending )
16109 try :
17110 return c [k ]
18111 except KeyError :
19- p .add (k )
112+ pending .add (k )
20113 try :
21114 v = method (self , * args , ** kwargs )
22115 with lock (self ):
@@ -27,16 +120,32 @@ def wrapper(self, *args, **kwargs):
27120 return v
28121 finally :
29122 with lock (self ):
30- pending [ self ] .remove (k )
123+ pending .remove (k )
31124 cond (self ).notify_all ()
32125
33126 def cache_clear (self ):
34127 c = cache (self )
35128 with lock (self ):
36129 c .clear ()
37130
38- wrapper .cache_clear = cache_clear
39- return wrapper
131+ def classmethod_wrapper (self , * args , ** kwargs ):
132+ p = pending .setdefault (self , set ())
133+ return wrapper (self , p , * args , ** kwargs )
134+
135+ class Descriptor (DescriptorBase ):
136+ class Wrapper (WrapperBase ):
137+ def __init__ (self , obj ):
138+ super ().__init__ (obj , method , cache , key , lock , cond )
139+ self .__pending = set ()
140+
141+ def __call__ (self , * args , ** kwargs ):
142+ return wrapper (self ._obj , self .__pending , * args , ** kwargs )
143+
144+ # objtype: backward-compatible @classmethod handling with Python < 3.13
145+ def cache_clear (self , _objtype = None ):
146+ return cache_clear (self ._obj )
147+
148+ return Descriptor (classmethod_wrapper , cache_clear )
40149
41150
42151def _locked (method , cache , key , lock ):
@@ -61,8 +170,19 @@ def cache_clear(self):
61170 with lock (self ):
62171 c .clear ()
63172
64- wrapper .cache_clear = cache_clear
65- return wrapper
173+ class Descriptor (DescriptorBase ):
174+ class Wrapper (WrapperBase ):
175+ def __init__ (self , obj ):
176+ super ().__init__ (obj , method , cache , key , lock )
177+
178+ def __call__ (self , * args , ** kwargs ):
179+ return wrapper (self ._obj , * args , ** kwargs )
180+
181+ # objtype: backward-compatible @classmethod handling with Python < 3.13
182+ def cache_clear (self , _objtype = None ):
183+ return cache_clear (self ._obj )
184+
185+ return Descriptor (wrapper , cache_clear )
66186
67187
68188def _unlocked (method , cache , key ):
@@ -84,8 +204,19 @@ def cache_clear(self):
84204 c = cache (self )
85205 c .clear ()
86206
87- wrapper .cache_clear = cache_clear
88- return wrapper
207+ class Descriptor (DescriptorBase ):
208+ class Wrapper (WrapperBase ):
209+ def __init__ (self , obj ):
210+ super ().__init__ (obj , method , cache , key )
211+
212+ def __call__ (self , * args , ** kwargs ):
213+ return wrapper (self ._obj , * args , ** kwargs )
214+
215+ # objtype: backward-compatible @classmethod handling with Python < 3.13
216+ def cache_clear (self , _objtype = None ):
217+ return cache_clear (self ._obj )
218+
219+ return Descriptor (wrapper , cache_clear )
89220
90221
91222def _wrapper (method , cache , key , lock = None , cond = None ):
@@ -98,6 +229,7 @@ def _wrapper(method, cache, key, lock=None, cond=None):
98229 else :
99230 wrapper = _unlocked (method , cache , key )
100231
232+ # backward-compatible properties for @classmethod
101233 wrapper .cache = cache
102234 wrapper .cache_key = key
103235 wrapper .cache_lock = lock if lock is not None else cond
0 commit comments