7.5. Decorator Class with Func

7.5.1. Rationale

  • MyDecorator is a decorator name

  • myfunction is a function name

Syntax:
@MyDecorator
def myfunction(*args, **kwargs):
    ...
Is equivalent to:
myfunction = MyDecorator(myfunction)

7.5.2. Syntax

  • cls is a pointer 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 pointer to Wrapper

Definition:

class MyDecorator:
    def __init__(self, func):
        self._func = func

    def __call__(self, *args, **kwargs):
        return self._func(*args, **kwargs)

Decoration:

@MyDecorator
def myfunction():
    ...

Usage:

myfunction()

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

7.5.4. Use Cases

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

Dict Cache:

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)           # 8         # Computed
myfunction('hi', 3)        # 'hihihi'  # Computed
myfunction('ha', 3)        # 'hahaha'  # Computed

myfunction('ha', 3)        # 'hahaha'  # Fetched from cache
myfunction('hi', 3)        # 'hihihi'  # Fetched from cache
myfunction(2, 4)           # 8         # Fetched from cache
myfunction(4, 2)           # 8         # Computed


myfunction
# {
#   (2, 4): 8,
#   ('hi ', 3): 'hihihi',
#   ('ha', 3): 'hahaha',
#   (4, 2): 8,
# }
from pickle import dumps


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(dumps(args) + dumps(kwargs))
        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
# {-5031589639694290544: 2,
#  -7391056524300571861: 2,
#  -2712444627064717062: 6,
#  7201789803359913928: 42,
#  8409437572158207229: 63}

7.5.5. Assignments

Code 7.40. 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 7.41. 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)

    >>> display('iris.csv').startswith(str(CURRENT_DIR))
    True
    >>> display('iris.csv').endswith('iris.csv')
    True
    >>> display('/home/python/iris.csv')
    '/home/python/iris.csv'
"""

from pathlib import Path


CURRENT_DIR = Path().cwd()


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

English:
    1. Create decorator class `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. Stwórz dekorator 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
"""

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