Wrap, augment, and override functions and methods
Tags: python, productivity, tutorial, showdev
Published: 2022-11-09 12:44:54


![Hooks](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/8z82sz3mbwhph9yovxvz.jpg)

<figcaption><a href="https://commons.wikimedia.org/wiki/File:%D0%9A%D0%B0%D0%BD%D0%B0%D1%82%D0%BD%D1%8B%D0%B9_%D1%81%D1%82%D1%80%D0%BE%D0%BF%D1%8B.jpg">HardMediaGroup</a>, <a href="https://creativecommons.org/licenses/by-sa/3.0">CC BY-SA 3.0</a>, via Wikimedia Commons</figcaption>


---

  
## On decoration
[Python](https://www.python.org/) has a concept called [Decorator](https://peps.python.org/pep-0318/) which is a function that takes another function and extends the behavior. In the following script, the `timeit` decorator is used to measure the execution time of the `heavy_computation` function:

```python
import time
from functools import wraps

def timeit(text):
    def deco(target):
        @wraps(target)
        def wrapper(*args, **kwargs):
            # execute and measure the target run time
            start_time = time.perf_counter()
            result = target(*args, **kwargs)
            total_time = time.perf_counter() - start_time
            # print elapsed time
            print(text.format(total=total_time))
            return result
        return wrapper
    return deco

@timeit(text="Done in {total:.3f} seconds !")
def heavy_computation(a, b):
    time.sleep(2)  # doing some heavy computation !
    return a*b

if __name__ == "__main__":
    result = heavy_computation(6, 9)
    print("Result:", result)

```
Output:
```bash
$ python -m test
Done in 2.001 seconds !
Result: 54

```

Besides benchmarking, there are many other cool things that can be done with the Python decorator. For example, the [Flask](https://hackersandslackers.com/flask-routes/) and [Bottle](http://bottlepy.org/docs/dev/) web frameworks implement routing with decorators.

---

## Hooking
While decorators are cool, it's worth mentioning that using a decorator is much more intuitive than writing its code. The code is entirely different depending on whether the decorator takes arguments or not.

The following code performs the same task as the previous one, except it is more clear and intuitive:

```python
import time
from hooking import on_enter

def timeit(context, *args, **kwargs):
    # execute and measure the target run time
    start_time = time.perf_counter()
    context.result = context.target(*args, **kwargs)
    total_time = time.perf_counter() - start_time
    # print elapsed time
    text = context.config.get("text")  # get 'text' from config data
    print(text.format(total=total_time))
    context.target = None

@on_enter(timeit, text="Done in {total:.3f} seconds !")
def heavy_computation(a, b):
    time.sleep(2)  # doing some heavy computation !
    return a*b

if __name__ == "__main__":
    result = heavy_computation(6, 9)
    print("Result:", result)

```

Output:
```bash
$ python -m test
Done in 2.001 seconds !
Result: 54

```

The Hooking library used in the code above uses Python decorators to wrap, augment, and override functions and methods. It is a generic [hooking](https://en.wikipedia.org/wiki/Hooking) mechanism which is perfect for creating a **plug-in** mechanism for a project, performing **benchmarking** and **debugging**, implementing **routing** in a web framework, et cetera.

---

## Dual paradigm
Also, it is a dual paradigm hooking mechanism since it supports **tight** and **loose** [coupling](https://en.wikipedia.org/wiki/Coupling_(computer_programming)). The previous code uses the tight coupling paradigm, that's why the `timeit` hook is directly tied to the target function.

In loose coupling paradigm, targets functions and methods are tagged using a decorator, and hooks are bound to these tags. So when a target is called, the bound hooks are executed upstream or downstream. This paradigm is served by a class designed for pragmatic access via [class methods](https://stackoverflow.com/a/38276). This class can be easily subclassed to group tags by theme for example.

Here is an example of the loose coupling paradigm:

```python
import time
from hooking import H

@H.tag
def heavy_computation(a, b):
    print("heavy computation...")
    time.sleep(2)  # doing some heavy computation !
    return a*b

def upstream_hook(context, *args, **kwargs):
    print("upstream hook...")

def downstream_hook(context, *args, **kwargs):
    print("downstream hook...")

# bind upstream_hook and downstream_hook to the "heavy_computation" tag
H.wrap("heavy_computation", upstream_hook, downstream_hook)

if __name__ == "__main__":
    result = heavy_computation(6, 9)
    print("Result:", result)

```

Output:
```bash
$ python -m test
upstream hook...
heavy computation...
downstream hook...
Result: 54

```

---

## Conclusion
This library is available on [PyPI](https://github.com/pyrustic/hooking#installation) and you can play with the [examples](https://github.com/pyrustic/hooking#examples) which are on the project's [README](https://github.com/pyrustic/hooking#readme). I would like to know what you [think](http://sl4.org/crocker.html) of this project. Your questions, suggestions and criticisms are welcome !


Link to the project: <https://github.com/pyrustic/hooking>