|
| 1 | +from __future__ import annotations |
| 2 | + |
| 3 | +import sys |
| 4 | +from typing import Any, Generic, TypeVar, overload |
| 5 | + |
| 6 | +__all__ = ["cached_property"] |
| 7 | + |
| 8 | +if sys.version_info >= (3, 12): |
| 9 | + from functools import cached_property |
| 10 | +else: |
| 11 | + # based on the code from Python 3.14: |
| 12 | + # https://github.com/python/cpython/blob/ |
| 13 | + # 5507eff19c757a908a2ff29dfe423e35595fda00/Lib/functools.py#L1089-L1138 |
| 14 | + # Copyright (C) 2006 Python Software Foundation. |
| 15 | + # vendored under the PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 because |
| 16 | + # prior to Python 3.12 cached_property used a threading.Lock, which makes |
| 17 | + # it very slow. |
| 18 | + _T_co = TypeVar("_T_co", covariant=True) |
| 19 | + _NOT_FOUND = object() |
| 20 | + |
| 21 | + class cached_property(Generic[_T_co]): |
| 22 | + def __init__(self, func: Callable[[Any, _T_co]]) -> None: |
| 23 | + self.func = func |
| 24 | + self.attrname = None |
| 25 | + self.__doc__ = func.__doc__ |
| 26 | + self.__module__ = func.__module__ |
| 27 | + |
| 28 | + def __set_name__(self, owner: type[any], name: str) -> None: |
| 29 | + if self.attrname is None: |
| 30 | + self.attrname = name |
| 31 | + elif name != self.attrname: |
| 32 | + raise TypeError( |
| 33 | + "Cannot assign the same cached_property to two different names " |
| 34 | + f"({self.attrname!r} and {name!r})." |
| 35 | + ) |
| 36 | + |
| 37 | + @overload |
| 38 | + def __get__(self, instance: None, owner: type[Any] | None = None) -> Self: ... |
| 39 | + |
| 40 | + @overload |
| 41 | + def __get__( |
| 42 | + self, instance: object, owner: type[Any] | None = None |
| 43 | + ) -> _T_co: ... |
| 44 | + |
| 45 | + def __get__( |
| 46 | + self, instance: object, owner: type[Any] | None = None |
| 47 | + ) -> _T_co | Self: |
| 48 | + if instance is None: |
| 49 | + return self |
| 50 | + if self.attrname is None: |
| 51 | + raise TypeError( |
| 52 | + "Cannot use cached_property instance without calling __set_name__ on it." |
| 53 | + ) |
| 54 | + try: |
| 55 | + cache = instance.__dict__ |
| 56 | + except ( |
| 57 | + AttributeError |
| 58 | + ): # not all objects have __dict__ (e.g. class defines slots) |
| 59 | + msg = ( |
| 60 | + f"No '__dict__' attribute on {type(instance).__name__!r} " |
| 61 | + f"instance to cache {self.attrname!r} property." |
| 62 | + ) |
| 63 | + raise TypeError(msg) from None |
| 64 | + val = cache.get(self.attrname, _NOT_FOUND) |
| 65 | + if val is _NOT_FOUND: |
| 66 | + val = self.func(instance) |
| 67 | + try: |
| 68 | + cache[self.attrname] = val |
| 69 | + except TypeError: |
| 70 | + msg = ( |
| 71 | + f"The '__dict__' attribute on {type(instance).__name__!r} instance " |
| 72 | + f"does not support item assignment for caching {self.attrname!r} property." |
| 73 | + ) |
| 74 | + raise TypeError(msg) from None |
| 75 | + return val |
0 commit comments