"""Functools backports from standard lib."""

from __future__ import annotations

import sys
import types
from collections.abc import Callable
from typing import Any, Generic, TypeVar, overload

_T = TypeVar("_T")


class cached_property(Generic[_T]):  # pylint: disable=invalid-name
    """
    Backport of Python 3.12's cached_property.

    Includes https://github.com/python/cpython/pull/101890/files
    """

    def __init__(self, func: Callable[[Any], _T]) -> None:
        """Initialize."""
        self.func: Callable[[Any], _T] = func
        self.attrname: str | None = None
        self.__doc__ = func.__doc__

    def __set_name__(self, owner: type[Any], name: str) -> None:
        """Set name."""
        if self.attrname is None:
            self.attrname = name
        elif name != self.attrname:
            raise TypeError(
                "Cannot assign the same cached_property to two different names "
                f"({self.attrname!r} and {name!r})."
            )

    @overload
    def __get__(self, instance: None, owner: type[Any] | None = None) -> Any: ...

    @overload
    def __get__(self, instance: Any, owner: type[Any] | None = None) -> _T: ...

    def __get__(self, instance: Any | None, owner: type[Any] | None = None) -> _T | Any:
        """Get."""
        if instance is None:
            return self
        if self.attrname is None:
            raise TypeError(
                "Cannot use cached_property instance without "
                "calling __set_name__ on it."
            )
        try:
            cache = instance.__dict__
        # not all objects have __dict__ (e.g. class defines slots)
        except AttributeError:
            msg = (
                f"No '__dict__' attribute on {type(instance).__name__!r} "
                f"instance to cache {self.attrname!r} property."
            )
            raise TypeError(msg) from None
        val = self.func(instance)
        try:
            cache[self.attrname] = val
        except TypeError:
            msg = (
                f"The '__dict__' attribute on {type(instance).__name__!r} instance "
                f"does not support item assignment "
                f"for caching {self.attrname!r} property."
            )
            raise TypeError(msg) from None
        return val

    if sys.version_info >= (3, 9):
        __class_getitem__ = classmethod(types.GenericAlias)  # type: ignore
    else:

        def __class_getitem__(
            cls: type[cached_property],
            cls_item: Any,
        ) -> type[cached_property]:
            """Get item."""
            return cls
