from typing import Callable
from typing import Any
import functools
import inspect


def call_only_once(func: Callable[..., Any]) -> Callable[..., Any]:
    """
    Decorate a method or property of a class, so that this method can only
    be called once for every instance.
    Calling it more than once will result in exception.
    """

    @functools.wraps(func)
    def wrapper(*args, **kwargs):  # type: ignore
        self = args[0]
        assert func.__name__ in dir(self), "call_only_once can only be used on method or property!"

        if not hasattr(self, "_CALL_ONLY_ONCE_CACHE"):
            cache = self._CALL_ONLY_ONCE_CACHE = set()
        else:
            cache = self._CALL_ONLY_ONCE_CACHE  # pylint: disable=W0212

        cls = type(self)
        # cannot use ismethod(), because decorated method becomes a function
        is_method = inspect.isfunction(getattr(cls, func.__name__))
        assert func not in cache, (
            f"{'Method' if is_method else 'Property'} {cls.__name__}.{func.__name__} "
            f"can only be called once per object!"
        )
        cache.add(func)

        return func(*args, **kwargs)

    return wrapper