Metadata-Version: 2.1
Name: hooking
Version: 0.0.3
Summary: Generic hooking mechanism for Python
Home-page: https://github.com/pyrustic/hooking
Author: Pyrustic Evangelist
Author-email: rusticalex@yahoo.com
Maintainer: Pyrustic Evangelist
Maintainer-email: rusticalex@yahoo.com
License: MIT
Keywords: minimalist,generic,hooking,mechanism,hooks,library,handlers,pyrustic,tag,tagging,events,bind,decorator,decoration
Platform: UNKNOWN
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Requires-Python: >=3.5
Description-Content-Type: text/markdown
License-File: LICENSE

[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![PyPI package version](https://img.shields.io/pypi/v/hooking)](https://pypi.org/project/hooking)
[![Downloads](https://pepy.tech/badge/hooking)](https://pepy.tech/project/hooking)

<!-- Cover -->
<div align="center">
    <img src="https://raw.githubusercontent.com/pyrustic/misc/master/assets/hooking/hooks.jpg" alt="Hooks" width="960">
    <p align="center">
        <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
    </p>
</div>


<!-- Intro Text -->
# Pyrustic Hooking
<b> Generic hooking mechanism for Python </b>


This project is part of the [Pyrustic Open Ecosystem](https://pyrustic.github.io).
> [Modules documentation](https://github.com/pyrustic/hooking/tree/master/docs/modules#readme)


## Table of contents
- [Overview](#overview)
- [Tagging mechanism](#tagging-mechanism)
- [Bind hooks](#bind-hooks)
- [Anatomy of a hook](#anatomy-of-a-hook)
- [Chain break](#chain-break)
- [Freeze tags](#freeze-tags)
- [Exposed variables](#exposed-variables)
- [Clear data](#clear-data)
- [Miscellaneous](#miscellaneous)
- [Installation](#installation)

# Overview
This library, written in **Python**, implements an intuitive and minimalist [hooking](https://en.wikipedia.org/wiki/Hooking) mechanism. It exposes a [decorator](https://peps.python.org/pep-0318/) to tag **methods** and **functions** (targets), so when called, user-defined hooks will be executed upstream or downstream according to the spec (either `BEFORE` or `AFTER`) provided by the user. 

Arguments to targets are passed to hooks which can modify them or replace the targets themselves with an arbitrary [callable](https://en.wikipedia.org/wiki/Callable_object) or `None`. 

Thanks to the tagging mechanism, hooks are not directly tied to targets but to tags (either user-defined or derived from functions or methods themselves). Thus, hooks are loosely coupled to targets and dynamically bound to tags.

# Tagging mechanism
The `H.tag` class method allows you to tag a function or a method:
```python
from hooking import H

@H.tag
def my_func(*args, **kwargs):
    pass

class MyClass:
    @H.tag
    def my_method(self, *args, **kwargs):
        pass
```
`H.tag` accepts a `label` string as argument. By default, when this argument isn't provided, the library uses the [qualified name](https://peps.python.org/pep-3155/) of the method or function as the `label`.

Here we provide the `label` argument:
```python
from hooking import H

@H.tag("my_func")
def my_func(*args, **kwargs):
    pass

class MyClass:
    @H.tag("MyClass.my_method")
    def my_method(self, *args, **kwargs):
        pass
```

# Bind hooks
Hooks are not directly bound to functions or methods but to tags. The `H.bind` class method  allows the user to bind a hook to a tag and specify with the `spec` parameter whether the hook should be run upstream or downstream.

```python
from hooking import H, BEFORE, AFTER

@H.tag("target")
def my_func(*args, **kwargs):
    pass

def my_hook1(context):
    pass

def my_hook2(context):
    pass

# bind my_hook1 to "target" and run it upstream
H.bind("target", my_hook1) # by default, spec == BEFORE

# bind my_hook1 to "target" and run it downstream
hook_id = H.bind("target", my_hook2, spec=AFTER)
```
The `H.bind` class method returns a Hook ID (HID) which could be used later to **unbind** the hook:

```python
from hooking import H

def hook(context):
    pass

# bind
hid = H.bind("tag", hook)

# unbind
H.unbind(hid)
```

**Multiple** hooks can be unbound in a single statement:

```python
from hooking import H

def hook1(context):
    pass

def hook2(context):
    pass

# bind
hid1 = H.bind("tag", hook1)
hid2 = H.bind("tag", hook2)

# unbind multiple hooks manually
H.unbind(hid1, hid2)

# unbind all hooks automatically
H.unbind()
```

# Anatomy of a hook
A hook is a callable that accepts an instance of `hooking.Context` that exposes the following attributes:
- `hid`: the Hook ID (HID) as returned by `H.bind`;
- `tag`: the label string used to tag a function or method;
- `spec`: one of the `BEFORE` or `AFTER` constants;
- `target`: the function or method tagged with the `H.tag` decorator;
- `args`: tuple representing the arguments passed to the target;
- `kwargs`: dictionary representing the keyword arguments passed to the target;
- `result`: when spec is set to `AFTER`, this attribute contains the value returned by the target.

```python
from hooking import H, BEFORE, AFTER

@H.tag("target")
def my_func(*args, **kwargs):
    pass

def my_hook(context):
    if context.tag != "target":
        raise Exception("Wrong tag !")

H.bind("target", my_hook)
```

# Chain break
This library exposes an exception subclass to allow the programmer to break the execution of a chain of hooks:
```python
from hooking import H, ChainBreak

@H.tag("target")
def my_func(*args, **kwargs):
    pass

def hook1(context):
    pass

def hook2(context):
    raise ChainBreak

def hook3(context):
    pass

# bind hook1, hook2 and hook3 to 'target'
for hook in (hook1, hook2, hook3):
    H.bind("target", hook)

# call the target
my_func()

# since the target was called,
# the chain of hooks (hook1, hook2, hook3)
# must be executed.

# hook2 having used ChainBreak,
# the chain of execution will be broken
# and hook3 will be ignored

```

# Freeze tags
We could freeze a tag and thus prevent the execution of hooks bound to this tag:
```python
from hooking import H, BEFORE, AFTER

@H.tag
def my_func(*args, **kwargs):
    pass

H.freeze("my_func")

# from now on hooks bound to `my_func` will no longer be executed
```
The `H.freeze` class method can freeze multiple tags at once, or the entire hooking mechanism:
```python
from hooking import H, BEFORE, AFTER

# freeze all tags manually
H.freeze("tag1", "tag2", "tag3", "tagx")

# freeze the entire hooking mechanism
H.freeze()

# from now, no hook will be executed anymore
```

To **unfreeze** specific tags or the entire hooking mechanism, use the `H.unfreeze` class method:
```python
from hooking import H, BEFORE, AFTER

# unfreeze all tags manually
H.unfreeze("tag1", "tag2", "tag3", "tagx")

# unfreeze the entire hooking mechanism
H.unfreeze()

# from now, hooks will be executed when needed
```

# Exposed variables
The `H` class exposes the following class variables:
- `hooks`: dict, keys are HIDs (Hook IDs), values are instances of `HookInfo`; 
- `tags`: dict to hold relationship between tags and HIDs. Keys are tags, and values are sets; 
- `frozen`: boolean to tell whether the hooking mechanism is frozen or not;
- `frozen_tags`: set containing frozen tags.

# Clear data
The `H.clear` class method resets the following class variables: `H.hooks`, `H.tags`, `H.frozen`, `H.frozen_tags`.

# Miscellaneous
Whenever threads are introduced into a program, the state shared between threads becomes vulnerable to corruption. To avoid this situation, this library uses [threading.Lock](https://docs.python.org/3/library/threading.html#lock-objects) as a synchronization tool.

# Installation
**Hooking** is **cross-platform** and should work on **Python 3.5** or [newer](https://www.python.org/downloads/).

## For the first time

```bash
$ pip install hooking
```

## Upgrade
```bash
$ pip install hooking --upgrade --upgrade-strategy eager

```

## Show information
```bash
$ pip show hooking
```


<br>
<br>
<br>

[Back to top](#readme)

