"""
Funkcja dekoratora wykonująca dowolną zadaną weryfikację argumentów
innej funkcji lub metody. Dwa przykłady to sprawdzenie zakresu i typu.
Funkcja valuetest wykonuje bardziej ogólne testy wartości argumentów.
Argumenty w dekoratorze są określone za pomocą słów kluczowych.
We właściwym wywołaniu argumenty mogą być przekazywane za pomocą
pozycji lub słów kluczowych, a domyślne argumenty mogą być pomijane.
Przykłady użycia pokazane są w samotestującym kodzie niżej.
Uwagi: dekorator nie obsługuje w pełni zagnieżdżania, ponieważ argumenty
funkcji pośredniczącej są inne. Nie można weryfikować dodatkowych
argumentów przekazanych do argumentu *args dekorowanej funkcji.
W szczególnych przypadkach może to być bardziej skomplikowany sposób
niż użycie asercji.
"""
trace = False


def rangetest(**argchecks):
    return argtest(argchecks, lambda arg, vals: arg < vals[0] or arg > vals[1])

def typetest(**argchecks):
    return argtest(argchecks, lambda arg, type: not isinstance(arg, type))

def valuetest(**argchecks):
    return argtest(argchecks, lambda arg, tester: not tester(arg))


def argtest(argchecks, failif):             # Weryfikacja argumentów + kryteria w failif
    def onDecorator(func):                  # onCall zachowuje func, argchecks, failif
        if not __debug__:                   # Brak działań w przypadku użycia "python -O main.py argumenty..."
            return func
        else:
            code = func.__code__
            expected = code.co_varnames[:code.co_argcount]

            def onError(argname, criteria):
                 errmsg = f'{func.__name__} argument "{argname}" nie spełnia kryterium {criteria}'
                 raise TypeError(errmsg)

            def onCall(*pargs, **kargs):
                positionals = expected[:len(pargs)]
                for (argname, criteria) in argchecks.items():      # Dla wszystkich sprawdzanych argumentów:
                    if argname in kargs:                           # przekazanych za pomocą nazwy
                        if failif(kargs[argname], criteria):
                            onError(argname, criteria)

                    elif argname in positionals:                   # przekazanych za pomocą pozycji
                        position = positionals.index(argname)
                        if failif(pargs[position], criteria):
                            onError(argname, criteria)

                    else:                                          # Argumenty domyślne
                        if trace:
                            print('Argument "%s" ma domyślną wartość' % argname)
                return func(*pargs, **kargs)   # OK: wywołanie oryginalnej funkcji
            return onCall
    return onDecorator

