# DecoratorUtilities

### Readthedocs Reference

Please read our documentation here: [Readthedocs Reference](https://decoratorutilities.readthedocs.io/en/dev/)

### Package Reference

Our package reference link is: https://pypi.org/project/decoratorutilities

## Menu

- [Intro](#intro)
- [Installation](#installation)
- [Decorators](#decorators)
    - [Check Type Decorator](#check_type_decorator)
    - [Overloading Decorator](#overloading_decorator)
    - [Mocking Decorator](#mocking-decorator)
    - [Memoized Decorator](#memoized-decorator)
    - [Timeit Decorator](#timeit-decorator)
    - [Singleton Decorator](#singleton-decorator)

## Intro
DecoratorUtilities is a python library that provides different utilities for functions and class methods such as
to check parameters and return type, allow overloading, to cache results to speed up them, instantiate a Singleton object
and mocking at runtime

## Installation

```bash
pip install decoratorutilities
```

## Decorators

### Check Type Decorator

Decorate your own functions with **@checktype** decorator to check parameters type and return type too
**Example:**

```python
from decoratorutilities import checktype
import pytest

@checktype
def my_functon(a: int, b: int) -> int:
    return 1

# Valid usage: Integer parameters and return type
my_functon(5, 6)  # return 1
assert my_functon(5, 6) == 1  # True

# Invalid usage: String parameters and return type
with pytest.raises(TypeError):
    my_functon("5", "6")  # Raises TypeError Exception
with pytest.raises(TypeError):
    my_functon("invalid", b="Invalid")  # Raises TypeError Exception
with pytest.raises(TypeError):
    my_functon(a="invalid", b="Invalid")  # Raises TypeError Exception
with pytest.raises(TypeError):
    assert my_functon(5, 6) == "1"  # Raises TypeError Exception
```

Decorate your own class methods with **@checktype** decorator to check parameters type and return type too
**Example:**

```python
from decoratorutilities import checktype
import pytest

class X(object):

    @checktype  # checktype decorator for classes methods
    def my_function(self, value: int):
        return value

# Valid usage: Integer parameters and return type
assert X().my_function(1) == 1  # True

# Invalid usage: String parameters and return type
with pytest.raises(TypeError):
    X().my_function('1')  # Raises TypeError Exception
with pytest.raises(TypeError):
    X().my_function(1) == "1"  # Raises TypeError Exception
```

### Overloading Decorator

Decorate your own function with **@overload** decorator to define multiple functions with same name but with different parameters  
**Example:**

```python
from decoratorutilities import overload

@overload
def my_functon(a:int):
    return 1

@overload
def my_functon(a:str):
    return 2

# Invoke first my_functon and return 1
my_functon(1)
# Invoke second my_functon and return 2
my_functon('1')
```

Decorate your own class method with **@overload** decorator to define multiple class methods with same name but with different parameters  
**Example:**

```python
from decoratorutilities import overload

class X(object):
    @overload
    def x(self, x: int):
        return int

    @overload
    def x(self, x: str):
        return str

assert X().x(1) == int  # True
assert X().x('1') == str  # True
```


### Mocking Decorator

Decorate your own function with **@mocking** decorator to mock that function adding args in a tuple, kwargs in a dict and return value  
**Example:**

```python
from decoratorutilities import mocking

# Define args tuple, kwargs dict and return value
@mocking([
   ((1, 2, 3), {"a": 1}, 1),
   ((4, 5, 6), {"b": 2}, 2)
])
def a():
   pass

# Valid usage
assert a(1, 2, 3, a=1) == 1  # return 1
assert a(4, 5, 6, b=2) == 2  # return 2

# Invalid usage
assert a(7, 8, 9, c=1) == 1  # Raises KeyError Exception
```

### Memoized Decorator

Decorate your own function or class method with **@memoized** decorator to speed up it by storing the results and returning the cached result when the same inputs occur again  
**Example:**

```python
from decoratorutilities import memoized
import datetime

def util_run_function_with_time(fn, args, kwargs):
    start_time = datetime.datetime.now()
    tmp = fn(*args, **kwargs)
    end_time = datetime.datetime.now()
    return (end_time - start_time), tmp

@memoized
def memoized_fibonacci(x):
    if x == 0:
        return 0

    if x == 1:
        return 1

    return memoized_fibonacci(x - 1) + memoized_fibonacci(x - 2)

def fibonacci(x):
    if x == 0:
        return 0

    if x == 1:
        return 1

    return fibonacci(x - 1) + fibonacci(x - 2)

fib_value = 20
memoized_execution_time, memoized_value = util_run_function_with_time(memoized_fibonacci, (fib_value, ), {})  # Return execution time and value for memoized function
unmemoized_execution_time, unmemoized_value = util_run_function_with_time(fibonacci, (fib_value, ), {})  # Return execution time and value for unmemoized function

print(f"memoized_execution_time: {memoized_execution_time} - unmemoized_execution_time: {unmemoized_execution_time}")

assert memoized_execution_time < unmemoized_execution_time  # OK
assert memoized_value == unmemoized_value  # OK
```   

### Timeit Decorator

Decorate your own function with **@timeit** decorator to monitoring execution time  
**Example:**

```python
from decoratorutilities import timeit
import time


@timeit
def hello():
    time.sleep(0.1)


if __name__ == "__main__":
    hello()  # print "Execution time: 100.75 ms"
```


### Debug Decorator

Decorate your own function with **@debug** decorator to print in console more Exception details  
**Example:**

```python
from decoratorutilities import debug
import pytest

@debug
def a():
    message = "Hello " + 5
    return message

with pytest.raises(TypeError):
    a() # Print in console: Found "<class 'TypeError'>" Exception in file "('../tests', 'test_debug.py')" on line "9"
        # Error message: "can only concatenate str (not "int") to str"
```

Decorate your own class methods with **@debug** decorator to print in console more Exception details  
**Example:**

```python
from decoratorutilities import debug
import pytest

class A(object):

    @debug
    def __init__(self):
        self.message = "Hello " + 5

with pytest.raises(TypeError):
    A()  # Raises TypeError Exception
```

### Singleton Decorator

Decorate your own classes with **@singleton()** decorator to ensure that only one instance of the singleton class ever exists.
Never invoke **@singleton()** decorator without brackets otherwise it will cause problems
Define your class method `__init__()` without parameters, pass them to the **@singleton()** decorator

**Example with args and kwargs**

```python
import pytest
from decoratorutilities import singleton

@singleton(10, message="Message to send", email=["simone.scalamandre95@gmail.com"])
class A(object):
    def __init__(self):
        self.x = 50  # Define constant class attribute

    def print_hello(self):  # Custom method
        return "Hello World"

# Valid usage
assert A[0] == 10  # True -> Get first arg
assert A.x == 50  # True -> Get decorated class attribute
assert A.print_hello() == "Hello World"  # True -> Get decorated class method "print_hello()"
assert A.message == "Message to send"  # True -> Get kwarg "message" passed to Singleton decorator
assert A.email == ["simone.scalamandre95@gmail.com"]  # True

with pytest.raises(TypeError):
    # Invalid usage, brackets are missing
    @singleton
    class B(object):
        def __init__(self):
            self.x = 10

    B()  # Raises TypeError Exception
```

We can define our singleton class with or without args and kwargs parameters but we can only instantiate one
**Example**

```python
import pytest
from decoratorutilities import singleton

@singleton()
class A(object):
    def __init__(self):
        pass

@singleton(x=10)
class B(object):
    def __init__(self):
        pass

with pytest.raises(TypeError) as e:
    A()  # Raises TypeError Exception

with pytest.raises(TypeError):
    B()  # Raises TypeError Exception
```