Generic hooking mechanism for Python
Project description
HardMediaGroup, CC BY-SA 3.0, via Wikimedia Commons
Pyrustic Hooking
Generic hooking mechanism for Python
This project is part of the Pyrustic Open Ecosystem.
Table of contents
- Overview
- Tagging mechanism
- Bind hooks
- Anatomy of a hook
- Chain break
- Freeze tags
- Exposed variables
- Clear data
- Miscellaneous
- Installation
Overview
This library, written in Python, implements an intuitive and minimalist hooking mechanism. It exposes a decorator 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 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:
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 of the method or function as the label
.
Here we provide the label
argument:
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.
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:
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:
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 byH.bind
;tag
: the label string used to tag a function or method;spec
: one of theBEFORE
orAFTER
constants;target
: the function or method tagged with theH.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 toAFTER
, this attribute contains the value returned by the target.
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:
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:
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:
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:
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 ofHookInfo
;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 as a synchronization tool.
Installation
Hooking is cross-platform and should work on Python 3.5 or newer.
For the first time
$ pip install hooking
Upgrade
$ pip install hooking --upgrade --upgrade-strategy eager
Show information
$ pip show hooking
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.