#%% V Pythonu je téměř vše objekt (a co objekt není, to se jako objekt tváří) - i funkce:


def some_fn(arg=0):
    """This is a documentation."""
    print(f'arg = {arg}')


var = some_fn
print(f'var = {var}')
print(f'var = {var.__name__}')
print(f'var = {var.__doc__}')


#%% Samozřejmě jí lze předat i jako argument:
def call_some_fn(fn_to_call):
    fn_to_call(42)


call_some_fn(some_fn)


#%% A protože se předpokládá, že se za běhu funkce nebude měnit (= je immutable), lze ji použít jako klíč do slovníku:
func_metadata = {
    some_fn: 42,
    call_some_fn: 0,
}

print(f'func_metadata[some_fn] = {func_metadata[some_fn]}')


#%% Základ dekorátoru:
def basic_decorator(func):
    print(f'Going to decorate {func}')
    return func


@basic_decorator
def my_fn_1():
    print('I am my_fn_1')


def my_fn_2():
    print('I am my_fn_2')


my_fn_2 = basic_decorator(my_fn_2)


#%% Test:

my_fn_1()
my_fn_2()


#%% Nahrazení vlastní funkcí:
def replacing_decorator(func):
    def replacement():
        print('I am the replacement function!')

    return replacement


@replacing_decorator
def my_fn_1():
    print('I am my_fn_1')


my_fn_1()


#%% Obalení funkce:
def wrapper_decorator(func):
    def wrapper():
        print(f'Doing something before calling the {func}')
        func()
        print(f'Doing something after calling the {func}')

    return wrapper


@wrapper_decorator
def my_fn_1():
    print('I am my_fn_1')


my_fn_1()


#%% Aplikace více deokorátorů
def wrapper_1(func):
    def wrapper():
        print(f'wrapper_1 for {func}: preprocessing')
        func()
        print(f'wrapper_1 for {func}: postprocessing')
    return wrapper


def wrapper_2(func):
    def wrapper():
        print(f'wrapper_2 for {func}: preprocessing')
        func()
        print(f'wrapper_2 for {func}: postprocessing')
    return wrapper


def wrapper_3(func):
    def wrapper():
        print(f'wrapper_3 for {func}: preprocessing')
        func()
        print(f'wrapper_3 for {func}: postprocessing')
    return wrapper


@wrapper_1
@wrapper_2
@wrapper_3
def fn():
    print('Executing fn()')


fn()


#%% Předání libovolných parametrů a návratové hodnoty:
def wrapper_decorator(func):
    def wrapper(*args, **kwargs):
        print(f'Doing something before calling the {func}')
        return_value = func(*args, **kwargs)
        print(f'Doing something after calling the {func}')
        return return_value

    return wrapper


@wrapper_decorator
def square(x):
    return x ** 2


squared_answer = square(42)
print(f'The Answer, Squared: 42 ** 2 = {squared_answer}')


#%% Pozor na to, když nahrazujeme funkci:
from functools import wraps


def null_wrapper(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)

    return wrapper


@null_wrapper
def square(x):
    return x ** 2


print(f'Function: {square}')


#%% TODO: Napište vlastní dekorátor, který zaloguje, že je funkce volána a bude počítat počet volání.

pass


#%% Předávání parametrů
from functools import wraps


def return_value_replacer(replacement_value):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            func(*args, **kwargs)
            return replacement_value
        return wrapper
    return decorator


@return_value_replacer(42)
def get_random_number():
    # https://xkcd.com/221/
    return 4


print(f'Random number = {get_random_number()}')


#%% Volání jak s parametry, tak bez parametrů
@return_value_replacer
def get_random_number():
    # https://xkcd.com/221/
    return 4


print(f'Random number = {get_random_number()}')


#%% Volání jak s parametry, tak bez parametrů
def return_value_replacer(replacement_value):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            func(*args, **kwargs)
            return replacement_value
        return wrapper

    if callable(replacement_value):
        fn = replacement_value
        replacement_value = None
        return decorator(fn)
    else:
        return decorator


@return_value_replacer
def get_random_number():
    # https://xkcd.com/221/
    return 4


print(f'Random number = {get_random_number()}')


#%% Volání jak s parametry, tak bez parametrů - keyword only arguments:
def return_value_replacer(fn=None, *, replacement_value=None):
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            func(*args, **kwargs)
            return replacement_value
        return wrapper

    if fn is None:
        return decorator
    else:
        return decorator(fn)


@return_value_replacer
def get_random_number_1():
    return 4


@return_value_replacer(replacement_value=42)
def get_random_number_2():
    return 4


print(f'Random number 1 = {get_random_number_1()}')
print(f'Random number 2 = {get_random_number_2()}')


#%% TODO: Napište dekorátor, který umožní volat dekorátor jak s parametry, tak bez parametrů.

pass


#%% Context Manager lze použít i jako dekorátor:
from contextlib import ContextDecorator


class DecoratorAndContextManager(ContextDecorator):
    def __enter__(self):
        print('__enter__')

    def __exit__(self, exc_type, exc_val, exc_tb):
        print('__exit__')


@DecoratorAndContextManager()
def my_fn_1():
    print('my_fn_1()')


my_fn_1()

with DecoratorAndContextManager():
    print('inside with')


#%% Dekorátor může být i třída s funkcí __call__:

class ReturnValueReplacer:
    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        self.func(*args, **kwargs)
        return None


@ReturnValueReplacer
def get_random_number():
    return 4


print(f'Random number = {get_random_number()}')


#%% Dekorátor implementovaný třídou, u kterého jsou potřeba parametry, je nejlepší obalit do funkce:
class ReturnValueReplaceDecorator:
    def __init__(self, func, replacement_value=None):
        self.func = func
        self.replacement_value = replacement_value

    def __call__(self, *args, **kwargs):
        print(f'{args}, {kwargs}')
        self.func(*args, **kwargs)
        return self.replacement_value


def replace_return_value(replacement_value=None):
    def wrapper(func):
        return ReturnValueReplaceDecorator(func, replacement_value)
    return wrapper


@replace_return_value(42)
def get_random_number():
    return 4


print(f'Random number = {get_random_number()}')


#%% Dekorovat lze i třídu, platí stejná pravidla, akorát argumentem dekorátoru není funkce, ale třída:
def class_decorator(cls):
    def method(self):
        print(f'This method came from the decorator and was called on {self}')

    cls.method = method
    return cls


@class_decorator
class MyClass:
    pass


inst = MyClass()
inst.method()


#%% property
from django.utils.functional import cached_property


class CartItem:
    def __init__(self, quantity, unit_price):
        self.quantity = quantity
        self.unit_price = unit_price

    @cached_property
    def total_quantity(self):
        print('Computing...')
        return self.quantity * self.unit_price


item = CartItem(10, 500)
print(item.total_quantity)
print(item.total_quantity)
print(item.total_quantity)
print(item.total_quantity)
