SimpleDeco 0.0.3
Decorators without nested functions
Requires Python: >=3.9

# SimpleDeco
### Decorators without nested functions

SimpleDeco is a way to create decorators with arguments and not to think about higher-order functions.

Instead, of nested functions, with SimpleDeco you split the decorator definitions into one or more plain functions.

## Basic example

Let's create a `count_time(iterations)` decorator, which runs the given function the given number of iterations and counts the average time.

```python
from time import time
from src.simpledeco import SimpleDeco, Wrapped


@SimpleDeco
def count_time(wrapped: Wrapped, iterations: int):
    t1 = time()

    for _ in range(iterations):
        # run the wrapped func with given arguments
        wrapped.func(*wrapped.args, **wrapped.kwargs)

    t2 = time()
    print('time:', (t2 - t1) / iterations)


@count_time(1000)
def f(x, y):
    return sum(range(x, y))


# Counts the sum of numbers from 1 to 50000 for 1000 times
# and prints the average time
f(1, 50000)
```

The similar code **without SimpleDeco**:

```python
from time import time


def count_time(iterations: int):
    def decorator(func):
        def wrapper(*args, **kwargs):
            t1 = time()
            for _ in range(iterations):
                func(*args, **kwargs)
            t2 = time()
            print('time:', (t2 - t1) / iterations)

        return wrapper
    return decorator

@count_time(1000)
def f(x, y):
    return sum(range(x, y))


f(1, 50000)
```

Pretty more complex.

## Wrapped object

SimpleDeco objects (like `count_time` above) are callable objects, which return decorators.

When using a SimpleDeco object with arguments as a decorator, the `Wrapped` instance and given arguments
are passed.

`Wrapped` object has simple attributes:

- `wrapped.func` - the given function
- `wrapped.args` - a tuple of all positional arguments passed to a function
- `wrapped.kwargs` - a dict of all keyword arguments passed to a function
- `wrapped.arguments` - an object with all passed arguments

Thus, you can use `wrapped.func(*wrapped.args, **wrapped.kwargs)` to call the original function with original arguments.

If you need to use specific arguments, use `wrapped.arguments` attributes. For example, if you need to decorate only functions with `x` and `y` arguments:

```python
@SimpleDeco
def count_time(wrapped: Wrapped, iterations: int):
    t1 = time()
    
    for _ in range(iterations):
        wrapped.func(wrapped.arguments.x, wrapped.arguments.y)
    
    t2 = time()
    print('x:', wrapped.arguments.x, 'y:', wrapped.arguments.y)
    print('time:', (t2 - t1) / iterations)
```

## Hooks

Sometimes you need to do something after wrapping a function or before decorating it.
There are special SimpleDeco methods for that.

- `simpledeco.after_wrapping` decorates a function that takes wrapped SimpleDeco and wrapper as arguments
- `simpledeco.before_decorating` decorates a function that takes wrapped SimpleDeco and decorator as arguments

For example:

```python
from time import time
from src.simpledeco import SimpleDeco


@SimpleDeco
def count_time(wrapped, iterations):
    t1 = time()

    for _ in range(iterations):
        # run the wrapped func with given arguments
        wrapped.func(*wrapped.args, **wrapped.kwargs)

    t2 = time()
    print('time:', (t2 - t1) / iterations)


@count_time.after_wrapping
def after_wrapping(count_time_wrapped, wrapper):
    print('Running function for', count_time_wrapped.arguments.iterations, 'times')
    print('With arguments (1, 50000)')
    wrapper(1, 50000)


@count_time.before_decorating
def before_decorating(count_time_wrapped, decorator):
    print('Generated decorator with argument:', count_time_wrapped.arguments.iterations)
    # 'decorator' is the generated decorator


@count_time(1000)
def f(x, y):
    return sum(range(x, y))
```
The output:
```
Generated decorator with argument: 1000
Running function for 1000 times
With arguments (1, 50000)
time: 0.0022199389934539795
```

Compare with similar code **without SimpleDeco**:

```python
from time import time


def count_time(iterations: int):
    def decorator(func):
        def wrapper(*args, **kwargs):
            t1 = time()
            for _ in range(iterations):
                func(*args, **kwargs)
            t2 = time()
            print('time:', (t2 - t1) / iterations)
        print('Running function for', iterations, 'times')
        print('With arguments (1, 50000)')
        wrapper(1, 50000)
        return wrapper
    print('Generated decorator with argument:', iterations)
    return decorator


@count_time(1000)
def f(x, y):
    return sum(range(x, y))
```

# License

This project is licensed under the terms of the MIT license.