TOC

from functools import wraps
from inspect import signature
from time import time


def null(f):
    """
    Un decorateur qui annule l'appel d'une fonction
    """

    def wrapper(*args, **dargs):
        # __qualname__ returns the qualified name. So for a method
        # f in class C, it will return C.f
        print(f'Appel de {f.__qualname__} supprimé !')

    return wrapper


def timer(f):
    """
    un décorateur pour calculer le temps d'exécution d'une fonction.
    Implémentation avec une fonction (cloture)
    """

    def wrapper(*args, **kargs):
        start = time()
        res = f(*args, **kargs)
        print(f"{f.__qualname__} took {time() - start:.2f} seconds to complete")
        return res

    return wrapper


class Timer:
    """
    un décorateur pour calculer le temps d'exécution d'une fonction.
    Implémentation avec une classe
    """

    def __init__(self, f):
        self.f = f

    def __call__(self, *args):
        start = time()
        result = self.f(*args)
        print(f"{self.f.__qualname__} took {time() - start:.2f} seconds to complete")
        return result


def caller(func):
    """
    Un decorateur compte le nombre d'appels d'une fonction
    """

    @wraps(func)
    def wrapper(*args, **dwargs):
        wrapper.called = wrapper.called + 1
        print(f'calling function {func.__qualname__}, called {wrapper.called} times')
        return func(*args, **dwargs)

    wrapper.called = 0
    return wrapper


class ArgumentTypeError(Exception):
    """
    Exception raised when the type of the arguments of a function is not the one expected
    """
    pass


def validate_arg_type(_type):
    """
    un décorateur qui valide que le type de tous les arguments est de type _type. Si c'est la cas, appel la fonction
    décorée, sinon, affiche un message d'erreur.
    """

    def validate_type(f):
        def wrapper(*args, **kargs):
            for i in args:
                if type(i) is not _type:
                    raise ArgumentTypeError(f"{i} is not of type {_type}")
            for i in kargs.values():
                if type(i) is not _type:
                    raise ArgumentTypeError(f"{i} is not of type {_type}")
            return f(*args, **kargs)

        return wrapper

    return validate_type


@validate_arg_type(int)
def f(a, b):
    print(a, b)


f(b=1, a=2)


def memoize1(f):
    """
    Un décorateur pour cacher la valeur de retour d'une fonction en utilisant comme clef dans le cache
    les arguments passés (indice, on utilise repr((args, kwargs)))
    Lors d'un nouvel appel pour la même fonction et les mêmes argument, le décorateur va retourner le resultat caché.
    """
    cache = {}

    @wraps(f)
    def decorated(*args, **kwargs):
        # we take the repr to get a string to be sure to have an immutable
        key = repr((args, kwargs))
        if key not in cache:
            cache[key] = f(*args, **kwargs)
        else:
            print("cached result")
        return cache[key]

    return decorated


def memoize2(f):
    """
    Un décorateur pour cacher la valeur de retour d'une fonction en utilisant comme clef dans le cache
    les arguments passés. Lors d'un nouvel appel pour la même fonction et les mêmes argument,
    le décorateur va retourner le resultat caché.

    Le défaut de memoize1 est que le cache va considérer les appels df(1) et f(a=1) comme des appels différents pour
    la fonction définie par
    def f(a):
        pass

    Dans cette version, on va utiliser inspect.signature qui permet d'obtenir la signature d'une fonction,
    et inspect. Signature.bind qui permet de résoudre des arguments *args **kargs selon la signature de la fonction
    en retournant un objet inspect.BoundArguments. Finalement on peut extraire inspect.BoundArguments.args et
    inspect.BoundArguments.kargs qui sont les arguments correctement mappé à la signature de la fonction (donc l'ordre
    d'appel n'a plus d'importance).
    """
    cache = {}
    sig = signature(f)

    @wraps(f)
    def decorated(*args, **kwargs):
        bind = sig.bind(*args, **kwargs)
        key = repr((bind.args, bind.kwargs))
        if not key in cache:
            cache[key] = f(*args, **kwargs)
        else:
            print("cached result")
        return cache[key]

    return decorated


# example d'usage de null
@null
def unfinished_func(L):
    # do a super cool stuff on L not yet implemented
    MyNewClass.mega_filter(L)
    return L


unfinished_func(range(10))


class C:
    @null
    def f(self):
        pass


C().f()


# example d'usage de Timer
@Timer
def f_comp():
    return [x ** 2 for x in range(10000000)]


f_comp()


# example d'usage de timer de memoize2
@timer
@caller
@memoize2
def fib(n):
    a, b = 1, 1
    for i in range(n - 1):
        a, b = b, a + b
    return a


print(fib(10))
print(fib(4000))


@validate_arg_type(int)
def f(a, b):
    print(a, b)


f(b=1, a=2)
f(1, 2)
try:
    f(1, 'a')
except ArgumentTypeError as e:
    print(e.args)
#f(b='x', a='y')