6.6. Decorator Arguments

6.6.1. Rationale

Decorator:
@mydecorator(a, b)
def myfunction(*args, **kwargs):
    pass
Is equivalent to:
myfunction = mydecorator(a, b)(myfunction)

6.6.2. Syntax

Definition:

def mydecorator(a=1, b=2):
    def decorator(func):
        def wrapper(*args, **kwargs):
            return func(*args, **kwargs)
        return wrapper
    return mydecorator

Decoration:

@mydecorator(a=0)
def myfunction():
    ...

Usage:

myfunction()

6.6.3. Example

def run(lang='en'):
    def decorator(func):
        def wrapper(*args, **kwargs):
            return func(*args, **kwargs)
        return wrapper
    return decorator


@run(lang='en')
def hello(name):
    return f'My name... {name}'


hello('José Jiménez')
# 'My name... José Jiménez'

6.6.4. Use Cases

Deprecated:

import warnings


def deprecated(removed_in_version=None):
    def decorator(func):
        def wrapper(*args, **kwargs):
            name = func.__name__
            file = func.__code__.co_filename
            line = func.__code__.co_firstlineno + 1
            message = f"Call to deprecated function {name} in {file} at line {line}"
            message += f'\nIt will be removed in {removed_in_version}'
            warnings.warn(message, DeprecationWarning)
            return func(*args, **kwargs)
        return wrapper
    return decorator


@deprecated(removed_in_version=2.0)
def myfunction():
    pass


myfunction()
# /home/python/myscript.py:11: DeprecationWarning: Call to deprecated function myfunction in /home/python/myscript.py at line 19
# It will be removed in 2.0

Timeout using signal(SIGALRM):

from signal import signal, alarm, SIGALRM
from time import sleep


def timeout(seconds=2.0, error_message='Timeout'):
    def on_timeout(signum, frame):
        raise TimeoutError

    def decorator(func):
        def wrapper(*args, **kwargs):
            signal(SIGALRM, on_timeout)
            alarm(int(seconds))
            try:
                return func(*args, **kwargs)
            except TimeoutError:
                print(error_message)
            finally:
                alarm(0)
        return wrapper
    return decorator


@timeout(seconds=3.0)
def countdown(n):
    for i in reversed(range(n)):
        print(i)
        sleep(1)
    print('countdown finished')

countdown(5)
# 4
# 3
# 2
# Sorry, timeout

Timeout using threading.Timer:

from _thread import interrupt_main
from threading import Timer
from time import sleep


def timeout(seconds=2.0, error_message='Timeout'):
    def decorator(func):
        def wrapper(*args, **kwargs):
            timer = Timer(seconds, interrupt_main)
            timer.start()
            try:
                result = func(*args, **kwargs)
            except KeyboardInterrupt:
                raise TimeoutError(error_message)
            finally:
                timer.cancel()
            return result
        return wrapper
    return decorator


@timeout(seconds=3.0)
def countdown(n):
    for i in reversed(range(n)):
        print(i)
        sleep(1)
    print('countdown finished')

countdown(5)
# 4
# 3
# 2
# Traceback (most recent call last):
# TimeoutError: Timeout

6.6.5. Assignments

Code 6.14. Solution
"""
* Assignment: Decorator Arguments Syntax
* Complexity: easy
* Lines of code: 5 lines
* Time: 5 min

English:
    1. Define decorator `mydecorator`
    2. Decorator should take `a` and `b` as arguments
    2. Define `wrapper` with `*args` and `**kwargs` parameters
    3. Wrapper should call original function with it's original parameters,
       and return its value
    4. Decorator should return `wrapper` function
    5. Run doctests - all must succeed

Polish:
    1. Zdefiniuj dekorator `mydecorator`
    2. Dekorator powinien przyjmować `a` i `b` jako argumenty
    2. Zdefiniuj `wrapper` z parametrami `*args` i `**kwargs`
    3. Wrapper powinien wywoływać oryginalną funkcję z jej oryginalnymi
       parametrami i zwracać jej wartość
    4. Decorator powinien zwracać funckję `wrapper`
    5. Uruchom doctesty - wszystkie muszą się powieść

Tests:
    >>> import sys; sys.tracebacklimit = 0
    >>> from inspect import isfunction

    >>> assert isfunction(mydecorator)
    >>> assert isfunction(mydecorator(a=1, b=2))
    >>> assert isfunction(mydecorator(a=1, b=2)(lambda: None))

    >>> @mydecorator(a=1, b=2)
    ... def echo(text):
    ...     return text

    >>> echo('hello')
    'hello'
"""


Code 6.15. Solution
"""
* Assignment: Decorator Arguments Astronauts
* Complexity: easy
* Lines of code: 4 lines
* Time: 5 min

English:
    1. Create decorator `check_astronauts`
    2. To answer if person is an astronaut check field:
       `is_astronaut` in `crew: list[dict]`
    3. Decorator will call decorated function, only if all crew members has
       field with specified value
    4. Both field name and value are given as keyword arguments to decorator
    5. If any member is not an astronaut raise `PermissionError` and print
       his first name and last name
    6. Run doctests - all must succeed

Polish:
    1. Stwórz dekorator `check_astronauts`
    2. Aby odpowiedzieć czy osoba jest astronautą sprawdź pole:
       `is_astronaut` in `crew: list[dict]`
    3. Dekorator wywoła dekorowaną funkcję tylko wtedy, gdy każdy członek
       załogi ma pole o podanej wartości
    4. Zarówno nazwa pola jak i wartość są podawane jako argumenty nazwane do dekoratora
    5. Jeżeli, jakikolwiek członek nie jest astronautą, podnieś wyjątek
       `PermissionError` i wypisz jego imię i nazwisko
    6. Uruchom doctesty - wszystkie muszą się powieść

Tests:
    >>> import sys; sys.tracebacklimit = 0

    >>> CREW_PRIMARY = [
    ...    {'is_astronaut': True, 'name': 'Jan Twardowski'},
    ...    {'is_astronaut': True, 'name': 'Mark Watney'},
    ...    {'is_astronaut': True, 'name': 'Melissa Lewis'}]

    >>> CREW_BACKUP = [
    ...    {'is_astronaut': True, 'name': 'Melissa Lewis'},
    ...    {'is_astronaut': True, 'name': 'Mark Watney'},
    ...    {'is_astronaut': False, 'name': 'Alex Vogel'}]

    >>> @check_astronauts(field='is_astronaut', value=True)
    ... def launch(crew):
    ...    crew = ', '.join(astro['name'] for astro in crew)
    ...    return f'Launching: {crew}'

    >>> launch(CREW_PRIMARY)
    'Launching: Jan Twardowski, Mark Watney, Melissa Lewis'

    >>> launch(CREW_BACKUP)
    Traceback (most recent call last):
    PermissionError: Alex Vogel is not an astronaut

    >>> @check_astronauts(field='name', value='Melissa Lewis')
    ... def launch(crew):
    ...    crew = ', '.join(astro['name'] for astro in crew)
    ...    return f'Launching: {crew}'

    >>> launch(CREW_PRIMARY)
    Traceback (most recent call last):
    PermissionError: Jan Twardowski is not an astronaut

    >>> launch(CREW_BACKUP)
    Traceback (most recent call last):
    PermissionError: Mark Watney is not an astronaut
"""

def check_astronauts(field, value):
    def decorator(func):
        def wrapper(crew):
            return func(crew)
        return wrapper
    return decorator


Code 6.16. Solution
"""
* Assignment: Decorator Arguments Type Check
* Complexity: easy
* Lines of code: 3 lines
* Time: 5 min

English:
    1. Create decorator function `typecheck`
    2. Decorator checks return type only if `check_return` is `True`
    3. Run doctests - all must succeed

Polish:
    1. Stwórz dekorator funkcję `typecheck`
    2. Dekorator sprawdza typ zwracany tylko gdy `check_return` jest `True`
    3. Uruchom doctesty - wszystkie muszą się powieść

Hints:
    * `echo.__annotations__`

Tests:
    >>> import sys; sys.tracebacklimit = 0

    >>> @typecheck(check_return=True)
    ... def echo(a: str, b: int, c: float = 0.0) -> bool:
    ...     return bool(a * b)

    >>> echo('one', 1)
    True
    >>> echo('one', 1, 1.1)
    True
    >>> echo('one', b=1)
    True
    >>> echo('one', 1, c=1.1)
    True
    >>> echo('one', b=1, c=1.1)
    True
    >>> echo(a='one', b=1, c=1.1)
    True
    >>> echo(c=1.1, b=1, a='one')
    True
    >>> echo(b=1, c=1.1, a='one')
    True
    >>> echo('one', c=1.1, b=1)
    True
    >>> echo(1, 1)
    Traceback (most recent call last):
    TypeError: "a" is <class 'int'>, but <class 'str'> was expected
    >>> echo('one', 'two')
    Traceback (most recent call last):
    TypeError: "b" is <class 'str'>, but <class 'int'> was expected
    >>> echo('one', 1, 'two')
    Traceback (most recent call last):
    TypeError: "c" is <class 'str'>, but <class 'float'> was expected
    >>> echo(b='one', a='two')
    Traceback (most recent call last):
    TypeError: "b" is <class 'str'>, but <class 'int'> was expected
    >>> echo('one', c=1.1, b=1.1)
    Traceback (most recent call last):
    TypeError: "b" is <class 'float'>, but <class 'int'> was expected
"""

def decorator(func):
    def validate(argname, argval):
        argtype = type(argval)
        expected = func.__annotations__[argname]
        if argtype is not expected:
            raise TypeError(f'"{argname}" is {argtype}, but {expected} was expected')

    def merge(*args, **kwargs):
        args = dict(zip(func.__annotations__.keys(), args))
        return kwargs | args          # Python 3.9
        # return {**args, **kwargs)}  # Python 3.7, 3.8

    def wrapper(*args, **kwargs):
        for argname, argval in merge(*args, **kwargs).items():
            validate(argname, argval)

        result = func(*args, **kwargs)
        validate('return', result)
        return result
    return wrapper