11.12. Decorator Class

  • MyDecorator is a decorator name

  • myfunction is a function name

  • func is a reference to function which is being decorated

Definition:

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

Usage:

>>> @MyDecorator
... def say_hello():
...     return 'hello'
>>>
>>>
>>> say_hello()
'hello'

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

11.12.2. Use Case - 0x01

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

11.12.3. Use Case - 0x02

  • 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}

11.12.4. Use Case - 0x03

  • 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}

11.12.5. Assignments

Code 11.27. 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), \
    'MyDecorator should be a decorator class'

    >>> assert MyDecorator(lambda: ...), \
    'MyDecorator should take function as an argument'

    >>> assert isinstance(MyDecorator(lambda: ...), MyDecorator), \
    'MyDecorator() should return an object which is an instance of MyDecorator'

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

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

# type: type
class MyDecorator:
    ...

Code 11.28. Solution
"""
* Assignment: Decorator Class Abspath
* Complexity: easy
* Lines of code: 6 lines
* Time: 5 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. Note: if you are using Windows operating system,
       then one doctest (with absolute path) can fail
    6. 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. Uwaga: jeżeli korzystasz z systemu operacyjnego Windows,
       to jeden z doctestów (ścieżki bezwzględnej) może nie przejść pomyślnie
    6. Uruchom doctesty - wszystkie muszą się powieść

Hints:
    * `path = Path(path).absolute()`

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

    >>> assert isclass(Abspath), \
    'Abspath should be a decorator class'

    >>> assert Abspath(lambda: ...), \
    'Abspath should take function as an argument'

    >>> assert isinstance(Abspath(lambda: ...), Abspath), \
    'Abspath() should return an object which is an instance of Abspath'

    >>> @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')  # Should pass regardless your OS
    '/home/python/iris.csv'

TODO: Windows Path().absolute()
TODO: Test if function was called
"""

from pathlib import Path


def abspath(func):
    def wrapper(path):
        path = Path(path).absolute()
        return func(path)

    return wrapper


# type: type
class Abspath:
    ...

Code 11.29. Solution
"""
* Assignment: Decorator Class TypeCheck
* Complexity: medium
* Lines of code: 8 lines
* Time: 8 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. When received type is not expected raise `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. Gdy otrzymany typ nie jest równy oczekiwanemu podnieś `TypeError` z:
        a. nazwa argumentu
        b. aktualny typ
        c. oczekiwany typ
    5. 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 isclass

    >>> assert isclass(TypeCheck), \
    'TypeCheck should be a decorator class'

    >>> assert TypeCheck(lambda: ...), \
    'TypeCheck should take function as an argument'

    >>> assert isinstance(TypeCheck(lambda: ...), TypeCheck), \
    'TypeCheck() should return an object which is an instance of TypeCheck'

    >>> @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 typecheck(func):
    annotations = func.__annotations__

    def merge(args, kwargs) -> dict:
        return kwargs | dict(zip(annotations, args))

    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):
        for argname, argvalue in merge(args, kwargs).items():
            check(argname, argvalue)
        result = func(*args, **kwargs)
        check('return', result)
        return result

    return wrapper


# Refactor typecheck into class
# type: type
class TypeCheck:
    ...