7.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()
7.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ść'
7.13.2. Use Case - 0x01
>>>
... @setup(...)
... @teardown(...)
... def test():
... ...
7.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
7.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
7.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
7.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()
7.13.7. Assignments
"""
* 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():
...
"""
* 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
"""
* 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'
"""
from inspect import get_annotations
# type: Callable[[Callable], Callable]
def typecheck(func):
annotations = get_annotations(func)
def merge(args, kwargs) -> dict:
args = dict(zip(annotations, args))
return args | kwargs
def check(argname, argvalue):
argtype = type(argvalue)
expected = annotations[argname]
if argtype is not expected:
raise TypeError(f'"{argname}" is {argtype}, '
f'but {expected} was expected')
def wrapper(*args, **kwargs):
arguments = merge(args, kwargs).items()
for argname, argvalue in arguments:
check(argname, argvalue)
result = func(*args, **kwargs)
check('return', result)
return result
return wrapper