5.13. Decorator Arguments

  • Used for passing extra configuration to decorators

  • Use more one level of nesting

>>> def mydecorator(a=1, b=2):
...     def decorator(func):
...         def wrapper(*args, **kwargs):
...             return func(*args, **kwargs)
...         return wrapper
...     return decorator
>>>
>>>
>>> @mydecorator(a=0)
... def myfunction():
...     ...
>>>
>>>
>>> myfunction()

5.13.1. Example

>>> def translate(lang='en'):
...     TRANSLATION = {
...         'Hello': {'en': 'Hello', 'pl': 'Cześć', 'ru': 'Привет'},
...         'Goodbye': {'en': 'Goodbye', 'pl': 'Pa', 'ru': 'Пока'},
...     }
...     def decorator(func):
...         def wrapper(*args, **kwargs):
...             result = func(*args, **kwargs)
...             i18n = TRANSLATION.get(result, result)
...             return i18n.get(lang, result) if type(i18n) else i18n
...         return wrapper
...     return decorator
>>>
>>>
>>> @translate(lang='en')
... def say_hello():
...     return 'Hello'
>>>
>>> say_hello()
'Hello'
>>>
>>>
>>> @translate(lang='pl')
... def say_hello():
...     return 'Hello'
>>>
>>> say_hello()
'Cześć'

5.13.2. Use Case - 0x01

>>> 
... @setup(...)
... @teardown(...)
... def test():
...     ...

5.13.3. Use Case - 0x02

  • 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

5.13.4. Use Case - 0x03

  • Timeout (SIGALRM)

>>> from signal import signal, alarm, SIGALRM
>>> from time import sleep
>>>
>>>
>>> def timeout(seconds=1, 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)
... def countdown(n):
...     for i in reversed(range(n)):
...         print(i)
...         sleep(1)
...     print('countdown finished')
>>>
>>>
>>> countdown(10)  
9
8
7
Timeout

Note

Note to Windows users. Implementation of subprocess.Popen._wait()

>>> 
... def _wait(self, timeout):
...     """Internal implementation of wait() on Windows."""
...     if timeout is None:
...         timeout_millis = _winapi.INFINITE
...     else:
...         timeout_millis = int(timeout * 1000)
...     if self.returncode is None:
...         # API note: Returns immediately if timeout_millis == 0.
...         result = _winapi.WaitForSingleObject(self._handle,
...                                              timeout_millis)
...         if result == _winapi.WAIT_TIMEOUT:
...             raise TimeoutExpired(self.args, timeout)
...         self.returncode = _winapi.GetExitCodeProcess(self._handle)
...     return self.returncode

5.13.5. Use Case - 0x04

  • Timeout (Timer)

>>> from _thread import interrupt_main
>>> from threading import Timer
>>> from time import sleep
>>>
>>>
>>> def timeout(seconds=1.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.0)
...     print('countdown finished')
>>>
>>>
>>> countdown(10)  
9
8
7
Traceback (most recent call last):
TimeoutError: Timeout

5.13.6. Use Case - 0x05

File settings.py:

>>> BASE_URL = 'https://python3.info'

File utils.py:

>>> from http import HTTPStatus
>>> import requests
>>>
>>>
>>> def _request(url, method='GET'):
...     url = BASE_URL + url
...     resp = requests.request(url, method)
...     if resp.staus_code != HTTPStatus.OK:
...         raise ConnectionError
...     return resp
>>>
>>>
>>> def get(url):
...     def decorator(func):
...         def wrapper():
...             resp = _request(url)
...             return func(resp.json())
...         return wrapper
...     return decorator

File main.py:

>>> @get('/users/')
... def get_users(data: list[dict] = None) -> list['User']:
...     ...
>>>
>>>
>>> users = get_users()  

5.13.7. Assignments

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

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

Polish:
    1. Zdefiniuj dekorator `result`
    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(result), \
    'Create result() function'

    >>> assert isfunction(result(1, 2)), \
    'result() should take two positional arguments'

    >>> assert isfunction(result(a=1, b=2)), \
    'result() should take two keyword arguments: a and b'

    >>> assert isfunction(result(a=1, b=2)(lambda: ...)), \
    'result() should return decorator which can take a function as arg'

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

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

# type: Callable[[int,int], Callable]
def result():
    ...

Code 5.82. 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. 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. Nazwa pola 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
    >>> from inspect import isfunction, isclass

    >>> assert isfunction(check_astronauts), \
    'Create check_astronauts() function'

    >>> assert isfunction(check_astronauts('field', 'value')), \
    'check_astronauts() should take two positional arguments'

    >>> assert isfunction(check_astronauts(field='field', value='value')), \
    'check_astronauts() should take two keyword arguments: field and value'

    >>> assert isfunction(check_astronauts('field', 'value')(lambda: ...)), \
    'check_astronauts() should return decorator which can take a function'

    >>> CREW_PRIMARY = [
    ...    {'is_astronaut': True, 'name': 'Pan 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: Pan 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: Pan Twardowski is not an astronaut

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

# type: Callable[[str,str], Callable]
def check_astronauts(field, value):
    def decorator(func):
        def wrapper(crew):
            return func(crew)

        return wrapper

    return decorator


Code 5.83. Solution
"""
* Assignment: Decorator Arguments TypeCheck
* 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:
    * https://docs.python.org/3/howto/annotations.html
    * `inspect.get_annotations()`
    * `function.__code__.co_varnames`
    * `dict(zip(...))`
    * `dict.items()`
    * `dict1 | dict2` - merging dicts

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

    >>> assert isfunction(typecheck), \
    'Create typecheck() function'

    >>> assert isfunction(typecheck(True)), \
    'typecheck() should take one positional arguments'

    >>> assert isfunction(typecheck(check_return=True)), \
    'typecheck() should take one keyword arguments: check_return'

    >>> assert isfunction(typecheck(check_return=True)(lambda: ...)), \
    'typecheck() should return decorator which can take a function'

    >>> @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

    >>> @typecheck(check_return=True)
    ... def echo(a: str, b: int, c: float = 0.0) -> bool:
    ...     return str(a * b)
    >>>
    >>> echo('one', 1, 1.1)
    Traceback (most recent call last):
    TypeError: "return" is <class 'str'>, but <class 'bool'> was expected

    >>> @typecheck(check_return=False)
    ... def echo(a: str, b: int, c: float = 0.0) -> bool:
    ...     return str(a * b)
    >>>
    >>> echo('one', 1, 1.1)
    'one'
"""

# type: Callable[[Callable], Callable]
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}, '
                            f'but {expected} was expected')

    def wrapper(*args, **kwargs):
        arguments = kwargs | dict(zip(func.__annotations__.keys(), args))
        [validate(k, v) for k, v in arguments.items()]
        result = func(*args, **kwargs)
        validate('return', result)
        return result

    return wrapper