6.5. Decorator Class with Func

6.5.1. Rationale

  • MyDecorator is a decorator name

  • myfunction is a function name

6.5.2. Syntax

  • cls is a reference to class which is being decorated (MyClass in this case)

  • Wrapper is a closure class

  • Wrapper name is a convention, but you can name it anyhow

  • Wrapper can inherit from MyClass

  • Decorator must return reference to Wrapper

>>> class MyDecorator:
...     def __init__(self, func):
...         self._func = func
...
...     def __call__(self, *args, **kwargs):
...         return self._func(*args, **kwargs)
>>>
>>>
>>> @MyDecorator
... def myfunction():
...     ...
>>>
>>>
>>> myfunction()

6.5.3. Example

>>> class Run:
...     def __init__(self, func):
...         self._func = func
...
...     def __call__(self, *args, **kwargs):
...         return self._func(*args, **kwargs)
>>>
>>>
>>> @Run
... def hello(name):
...     return f'My name... {name}'
>>>
>>>
>>> hello('José Jiménez')
'My name... José Jiménez'

6.5.4. Use Case - Login Check

>>> class User:
...     def __init__(self):
...         self.is_authenticated = False
...
...     def login(self, username, password):
...         self.is_authenticated = True
>>>
>>>
>>> class LoginCheck:
...     def __init__(self, func):
...         self._func = func
...
...     def __call__(self, *args, **kwargs):
...         if user.is_authenticated:
...             return self._func(*args, **kwargs)
...         else:
...             print('Permission Denied')
>>>
>>>
>>> @LoginCheck
... def edit_profile():
...     print('Editing profile...')
>>>
>>>
>>> user = User()
>>>
>>> edit_profile()
Permission Denied
>>>
>>> user.login('admin', 'MyVoiceIsMyPassword')
>>> edit_profile()
Editing profile...

6.5.5. Use Case - Cache Args

>>> class Cache(dict):
...     def __init__(self, func):
...         self._func = func
...
...     def __call__(self, *args):
...         return self[args]
...
...     def __missing__(self, key):
...         self[key] = self._func(*key)
...         return self[key]
>>>
>>>
>>> @Cache
... def myfunction(a, b):
...     return a * b
>>>
>>>
>>> myfunction(2, 4)  # Computed
8
>>> myfunction('hi', 3)  # Computed
'hihihi'
>>> myfunction('ha', 3)  # Computed
'hahaha'
>>>
>>> myfunction('ha', 3)  # Fetched from cache
'hahaha'
>>> myfunction('hi', 3)  # Fetched from cache
'hihihi'
>>> myfunction(2, 4)  # Fetched from cache
8
>>> myfunction(4, 2)  # Computed
8
>>>
>>>
>>> myfunction  
{(2, 4): 8,
 ('hi', 3): 'hihihi',
 ('ha', 3): 'hahaha',
 (4, 2): 8}

6.5.6. Use Case - Cache Kwargs

>>> class Cache(dict):
...     _func: callable
...     _args: tuple
...     _kwargs: dict
...
...     def __init__(self, func):
...         self._func = func
...
...     def __call__(self, *args, **kwargs):
...         self._args = args
...         self._kwargs = kwargs
...         key = hash(args + tuple(kwargs.values()))
...         return self[key]
...
...     def __missing__(self, key):
...         self[key] = self._func(*self._args, **self._kwargs)
...         return self[key]
>>>
>>>
>>> @Cache
... def myfunction(a, b):
...     return a * b
>>>
>>>
>>> myfunction(1, 2)
2
>>> myfunction(2, 1)
2
>>> myfunction(6, 1)
6
>>> myfunction(6, 7)
42
>>> myfunction(9, 7)
63
>>> myfunction  
{-3550055125485641917: 2,
 6794810172467074373: 2,
 8062003079928221385: 6,
 1461316589696902609: 42,
 -4120545409808486724: 63}

6.5.7. Assignments

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

English:
    1. Create decorator class `MyDecorator`
    2. `MyDecorator` should have `__init__` which takes function as an argument
    3. `MyDecorator` should have `__call__` with parameters:
       `*args` and `**kwargs`
    4. `__call__` should call original function with original parameters,
       and return its value
    5. Run doctests - all must succeed

Polish:
    1. Stwórz dekorator klasę `MyDecorator`
    2. `MyDecorator` powinien mieć `__init__`, który przyjmuje funkcję
       jako argument
    3. `MyDecorator` powinien mieć `__call__` z parameterami:
       `*args` i `**kwargs`
    4.`__call__` powinien wywoływać oryginalną funkcję oryginalnymi
       parametrami i zwracać jej wartość
    5. Uruchom doctesty - wszystkie muszą się powieść

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

    >>> assert isclass(MyDecorator)
    >>> assert isinstance(MyDecorator(lambda: None), MyDecorator)

    >>> @MyDecorator
    ... def echo(text):
    ...     return text

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


Code 6.35. Solution
"""
* Assignment: Decorator Class Abspath
* Complexity: easy
* Lines of code: 10 lines
* Time: 13 min

English:
    1. Absolute path is when `path` starts with `current_directory`
    2. Create class decorator `Abspath`
    3. If `path` is relative, then `Abspath` will convert it to absolute
    4. If `path` is absolute, then `Abspath` will not modify it
    5. Run doctests - all must succeed

Polish:
    1. Ścieżka bezwzględna jest gdy `path` zaczyna się od `current_directory`
    2. Stwórz klasę dekorator `Abspath`
    3. Jeżeli `path` jest względne, to `Abspath` zamieni ją na bezwzględną
    4. Jeżeli `path` jest bezwzględna, to `Abspath` nie będzie jej modyfikował
    5. Uruchom doctesty - wszystkie muszą się powieść

Hints:
    * `path = Path(CURRENT_DIR, filename)`

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

    >>> @Abspath
    ... def display(path):
    ...     return str(path)

    >>> current_dir = str(Path().cwd())
    >>> display('iris.csv').startswith(current_dir)
    True
    >>> display('iris.csv').endswith('iris.csv')
    True
    >>> display('/home/python/iris.csv')
    '/home/python/iris.csv'
"""

from pathlib import Path


Code 6.36. Solution
"""
* Assignment: Decorator Class Type Check
* Complexity: medium
* Lines of code: 15 lines
* Time: 21 min

English:
    1. Refactor decorator `decorator` to decorator `TypeCheck`
    2. Decorator checks types of all arguments (`*args` oraz `**kwargs`)
    3. Decorator checks return type
    4. In case when received type is not expected throw an exception `TypeError` with:
        a. argument name
        b. actual type
        c. expected type
    5. Run doctests - all must succeed

Polish:
    1. Przerób dekorator `decorator` na klasę `TypeCheck`
    2. Dekorator sprawdza typy wszystkich argumentów (`*args` oraz `**kwargs`)
    3. Dekorator sprawdza typ zwracany
    4. W przypadku gdy otrzymany typ nie jest równy oczekiwanemu wyrzuć wyjątek `TypeError` z:
        a. nazwa argumentu
        b. aktualny typ
        c. oczekiwany typ
    5. Uruchom doctesty - wszystkie muszą się powieść

Hints:
    * `echo.__annotations__`

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

    >>> @TypeCheck
    ... 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
    ... 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
"""

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