11.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()

11.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ść'

11.13.2. Use Case - 0x01

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

11.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

11.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

11.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

11.13.6. Use Case - 0x05

File settings.py:

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

File utils.py:

>>> from http import HTTPStatus
>>> import httpx
>>>
>>>
>>> def _request(url, method='GET'):
...     url = BASE_URL + url
...     resp = httpx.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()  

11.13.7. Assignments

Code 11.30. 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 11.31. Solution
"""
* Assignment: Decorator Arguments Staff
* Complexity: easy
* Lines of code: 4 lines
* Time: 5 min

English:
    1. Create decorator `can_login`
    2. To answer if person is staff check field:
       `is_staff` in `users: list[dict]`
    3. Decorator will call decorated function, only if all users
       has field with specified value
    4. Field name and value are given as keyword arguments to decorator
    5. If user is not a staff:
       raise `PermissionError` with message `USERNAME is not a staff`,
       where USERNAME is user's username
    6. Run doctests - all must succeed

Polish:
    1. Stwórz dekorator `can_login`
    2. Aby odpowiedzieć czy osoba jest staffem sprawdź pole:
       `is_staff` in `users: list[dict]`
    3. Dekorator wywoła dekorowaną funkcję, tylko gdy każdy członek
       grupy ma pole o podanej wartości
    4. Nazwa pola i wartość są podawane jako argumenty nazwane do dekoratora
    5. Jeżeli użytkownik nie jest staffem:
       podnieś `PermissionError` z komunikatem `USERNAME is not a staff`,
       gdzie USERNAME to username użytkownika
    6. Uruchom doctesty - wszystkie muszą się powieść

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

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

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

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

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

    >>> group1 = [
    ...     {'is_staff': True, 'username': 'mwatney'},
    ...     {'is_staff': True, 'username': 'mlewis'},
    ...     {'is_staff': True, 'username': 'rmartinez'},
    ... ]
    ...
    >>> group2 = [
    ...     {'is_staff': False, 'username': 'avogel'},
    ...     {'is_staff': True,  'username': 'bjohanssen'},
    ...     {'is_staff': True,  'username': 'cbeck'},
    ... ]

    >>> @can_login(field='is_staff', value=True)
    ... def login(users):
    ...    users = ', '.join(user['username'] for user in users)
    ...    return f'Logging-in: {users}'

    >>> login(group1)
    'Logging-in: mwatney, mlewis, rmartinez'

    >>> login(group2)
    Traceback (most recent call last):
    PermissionError: avogel is not a staff

    >>> @can_login(field='is_staff', value='yes')
    ... def login(users):
    ...    users = ', '.join(user['username'] for user in users)
    ...    return f'Logging-in: {users}'

    >>> login(group1)
    Traceback (most recent call last):
    PermissionError: mwatney is not a staff

    >>> login(group2)
    Traceback (most recent call last):
    PermissionError: avogel is not a staff
"""

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


Code 11.32. 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