77import gc
88import platform
99import sys
10-
1110from os import environ
1211
1312# ============================================================================
@@ -127,21 +126,19 @@ def _get_entire_call_stack_as_qualnames_with_lineno_and_frame() -> (
127126 def _is_compile_perf_trampoline_pre_fork_enabled () -> bool :
128127 return False
129128
130- class async_cached_classproperty :
131- pass
132-
133- class async_cached_property :
134- pass
135-
136- class cached_classproperty :
137- pass
138-
129+ from asyncio import AbstractEventLoop , Future
139130 from typing import (
131+ Awaitable ,
140132 Callable ,
133+ Dict ,
141134 final ,
135+ Generator ,
142136 Generic ,
137+ List ,
138+ NoReturn ,
143139 Optional ,
144140 overload ,
141+ Tuple ,
145142 Type ,
146143 TYPE_CHECKING ,
147144 TypeVar ,
@@ -150,23 +147,11 @@ class cached_classproperty:
150147 _TClass = TypeVar ("_TClass" )
151148 _TReturnType = TypeVar ("_TReturnType" )
152149
153- if TYPE_CHECKING :
154- from abc import ABC
155-
156- @final
157- class Descriptor (ABC , Generic [_TReturnType ]):
158- __name__ : str
159- __objclass__ : Type [object ]
160-
161- def __get__ (
162- self , inst : object , ctx : Optional [Type [object ]] = None
163- ) -> _TReturnType : ...
164-
165- def __set__ (self , inst : object , value : _TReturnType ) -> None :
166- pass
150+ @final
151+ class NoValueSet :
152+ pass
167153
168- def __delete__ (self , inst : object ) -> None :
169- pass
154+ NO_VALUE_SET = NoValueSet ()
170155
171156 class _BaseCachedProperty (Generic [_TClass , _TReturnType ]):
172157 fget : Callable [[_TClass ], _TReturnType ]
@@ -208,6 +193,244 @@ def __get__(
208193 obj .__dict__ [self .__name__ ] = result
209194 return result
210195
196+ class _AsyncLazyValueState :
197+ NotStarted = 0
198+ Running = 1
199+ Done = 2
200+
201+ _T = TypeVar ("_T" , covariant = True )
202+ _TParams = TypeVar ("_TParams" )
203+
204+ # noqa: F401
205+ import asyncio
206+
207+ class _AsyncLazyValue (Awaitable [_T ]):
208+ """
209+ This is a low-level class used mainly for two things:
210+ * It helps to avoid calling a coroutine multiple times, by caching the
211+ result of a previous call
212+ * It ensures that the coroutine is called only once
213+
214+ _AsyncLazyValue has well defined cancellation behavior in these cases:
215+
216+ 1. When we have a single task stack (call stack for you JS folks), which is
217+ awaiting on the AsyncLazyValue
218+ -> In this case, we mimic the behavior of a normal await. i.e: If the
219+ task stack gets cancelled, we cancel the coroutine (by raising a
220+ CancelledError in the underlying future)
221+
222+ 2. When we have multiple task stacks awaiting on the future.
223+ We have two sub cases here.
224+
225+ 2.1. The initial task stack (which resulted in an await of the coroutine)
226+ gets cancelled.
227+ -> In this case, we cancel the coroutine, and all the tasks depending
228+ on it. If we don't do that, we'd have to implement retry logic,
229+ which is a bad idea in such low level code. Even if we do implement
230+ retries, there's no guarantee that they would succeed, so it's better
231+ to just fail here.
232+
233+ Also, the number of times this happens is very small (I don't have
234+ data to prove it, but qualitative arguments suggest this is the
235+ case).
236+
237+ 2.2. One of the many task stacks gets cancelled (but not the one which ended
238+ up awaiting the coroutine)
239+ -> In this case, we just allow the task stack to be cancelled, but
240+ the rest of them are processed without being affected.
241+ """
242+
243+ def __init__ (
244+ self ,
245+ # pyre-fixme[31]: Expression `typing.Callable[(_TParams,
246+ # typing.Awaitable[_T])]` is not a valid type.
247+ coro_func : Callable [_TParams , Awaitable [_T ]],
248+ # pyre-fixme[11]: Annotation `args` is not defined as a type.
249+ * args : _TParams .args ,
250+ # pyre-fixme[11]: Annotation `kwargs` is not defined as a type.
251+ ** kwargs : _TParams .kwargs ,
252+ ) -> None :
253+ global asyncio
254+ # pyre-fixme[31]: Expression `typing.Optional[typing.Callable[(_TParams,
255+ # typing.Awaitable[_T])]]` is not a valid type.
256+ self .coro_func : Optional [Callable [_TParams , Awaitable [_T ]]] = coro_func
257+ self .args : Tuple [object , ...] = args
258+ self .kwargs : Dict [str , object ] = kwargs
259+ self .state : int = _AsyncLazyValueState .NotStarted
260+ self .res : Optional [_T ] = None
261+ self ._futures : List [Future ] = []
262+ self ._awaiting_tasks = 0
263+
264+ async def _async_compute (self ) -> _T :
265+ futures = self ._futures
266+ try :
267+ coro_func = self .coro_func
268+ # lint-fixme: NoAssertsRule
269+ assert coro_func is not None
270+ self .res = res = await coro_func (* self .args , ** self .kwargs )
271+
272+ self .state = _AsyncLazyValueState .Done
273+
274+ # pyre-fixme[1001]: Awaitable assigned to `value` is never awaited.
275+ for value in futures :
276+ if not value .done ():
277+ value .set_result (self .res )
278+
279+ self .args = ()
280+ self .kwargs .clear ()
281+ del self ._futures [:]
282+ self .coro_func = None
283+
284+ return res
285+
286+ except (Exception , asyncio .CancelledError ) as e :
287+ # pyre-fixme[1001]: Awaitable assigned to `value` is never awaited.
288+ for value in futures :
289+ if not value .done ():
290+ value .set_exception (e )
291+ self ._futures = []
292+ self .state = _AsyncLazyValueState .NotStarted
293+ raise
294+
295+ def _get_future (self , loop : Optional [AbstractEventLoop ]) -> Future :
296+ if loop is None :
297+ loop = asyncio .get_event_loop ()
298+ f = asyncio .Future (loop = loop )
299+ self ._futures .append (f )
300+ self ._awaiting_tasks += 1
301+ return f
302+
303+ def __iter__ (self ) -> _AsyncLazyValue [_T ]:
304+ return self
305+
306+ def __next__ (self ) -> NoReturn :
307+ raise StopIteration (self .res )
308+
309+ def __await__ (self ) -> Generator [None , None , _T ]:
310+ if self .state == _AsyncLazyValueState .Done :
311+ # pyre-ignore[7]: Expected `Generator[None, None, Variable[_T](covariant)]`
312+ # but got `_AsyncLazyValue[Variable[_T](covariant)]`.
313+ return self
314+ elif self .state == _AsyncLazyValueState .Running :
315+ c = self ._get_future (None )
316+ return c .__await__ ()
317+ else :
318+ self .state = _AsyncLazyValueState .Running
319+ c = self ._async_compute ()
320+ return c .__await__ ()
321+
322+ def as_future (self , loop : AbstractEventLoop ) -> Future :
323+ if self .state == _AsyncLazyValueState .Done :
324+ f = asyncio .Future (loop = loop )
325+ f .set_result (self .res )
326+ return f
327+ elif self .state == _AsyncLazyValueState .Running :
328+ return self ._get_future (loop )
329+ else :
330+ if loop is None :
331+ loop = asyncio .get_event_loop ()
332+ t = loop .create_task (self ._async_compute ())
333+ self .state = _AsyncLazyValueState .Running
334+ # pyre-ignore[16]: Undefined attribute `asyncio.tasks.Task`
335+ # has no attribute `_source_traceback`.
336+ if t ._source_traceback :
337+ del t ._source_traceback [- 1 ]
338+ # pyre-fixme[7]: Expected `Future[Any]` but got `Task[_T]`.
339+ return t
340+
341+ _TAwaitableReturnType = TypeVar ("_TAwaitableReturnType" )
342+
343+ class async_cached_property (
344+ Generic [_TAwaitableReturnType , _TClass ],
345+ _BaseCachedProperty [_TClass , Awaitable [_TAwaitableReturnType ]],
346+ ):
347+ def __init__ (
348+ self ,
349+ f : Callable [[_TClass ], _TReturnType ],
350+ slot : Optional [Descriptor [_TReturnType ]] = None ,
351+ ) -> None :
352+ super ().__init__ (f , slot )
353+
354+ def __get__ (
355+ self , obj : Optional [_TClass ], cls : Type [_TClass ]
356+ ) -> (
357+ _BaseCachedProperty [_TClass , Awaitable [_TAwaitableReturnType ]]
358+ | Awaitable [_TAwaitableReturnType ]
359+ ):
360+ if obj is None :
361+ return self
362+
363+ slot = self .slot
364+ if slot is not None :
365+ try :
366+ res = slot .__get__ (obj , cls )
367+ except AttributeError :
368+ res = _AsyncLazyValue (self .fget , obj )
369+ slot .__set__ (obj , res )
370+ return res
371+
372+ lazy_value = _AsyncLazyValue (self .fget , obj )
373+ setattr (obj , self .__name__ , lazy_value )
374+ return lazy_value
375+
376+ class async_cached_classproperty (
377+ Generic [_TAwaitableReturnType , _TClass ],
378+ _BaseCachedProperty [Type [_TClass ], Awaitable [_TAwaitableReturnType ]],
379+ ):
380+ def __init__ (
381+ self ,
382+ f : Callable [[_TClass ], Awaitable [_TAwaitableReturnType ]],
383+ slot : Optional [Descriptor [Awaitable [_TAwaitableReturnType ]]] = None ,
384+ ) -> None :
385+ super ().__init__ (f , slot )
386+ self ._value : NoValueSet | Awaitable [_TAwaitableReturnType ] = NO_VALUE_SET
387+
388+ def __get__ (
389+ self , obj : Optional [_TClass ], cls : Type [_TClass ]
390+ ) -> Awaitable [_TAwaitableReturnType ]:
391+ lazy_value = self ._value
392+ if not isinstance (lazy_value , NoValueSet ):
393+ return lazy_value
394+ self ._value = lazy_value = _AsyncLazyValue (self .fget , cls )
395+ return lazy_value
396+
397+ class cached_classproperty (_BaseCachedProperty [Type [_TClass ], _TReturnType ]):
398+ def __init__ (
399+ self ,
400+ f : Callable [[_TClass ], _TReturnType ],
401+ slot : Optional [Descriptor [_TReturnType ]] = None ,
402+ ) -> None :
403+ super ().__init__ (f , slot )
404+ self ._value : NoValueSet | _TReturnType = NO_VALUE_SET
405+
406+ def __get__ (self , obj : Optional [_TClass ], cls : Type [_TClass ]) -> _TReturnType :
407+ result = self ._value
408+ if not isinstance (result , NoValueSet ):
409+ return result
410+ self ._value = result = self .fget (cls )
411+ return result
412+
413+ _TClass = TypeVar ("_TClass" )
414+ _TReturnType = TypeVar ("_TReturnType" )
415+
416+ if TYPE_CHECKING :
417+ from abc import ABC
418+
419+ @final
420+ class Descriptor (ABC , Generic [_TReturnType ]):
421+ __name__ : str
422+ __objclass__ : Type [object ]
423+
424+ def __get__ (
425+ self , inst : object , ctx : Optional [Type [object ]] = None
426+ ) -> _TReturnType : ...
427+
428+ def __set__ (self , inst : object , value : _TReturnType ) -> None :
429+ pass
430+
431+ def __delete__ (self , inst : object ) -> None :
432+ pass
433+
211434 class cached_property (_BaseCachedProperty [_TClass , _TReturnType ]):
212435 def __init__ (
213436 self ,
0 commit comments