Package: hooking
HardMediaGroup,CC BY-SA 3.0, via Wikimedia CommonsPyrustic HookingGeneric dual-paradigm hooking mechanismThis project is part of thePyrustic Open Ecosystem.Modules documentationTable of contentsOverviewExamplesFunctions and methods as targetsAnatomy of a hookTight couplingOverride a targetWrap a targetLoose couplingTagging mechanismBind hooks to tagsChain breakFreeze the hooking classExposed dataReset the hooking classSubclassing the hooking classMiscellaneousInstallationOverviewThis project is a minimalistPythonlibrary that implements an intuitive, flexible, and generic dual-paradigmhookingmechanism.In short,methodsandfunctions, calledtargets, aredecoratedand assigned user-definedhooks. So when a target is called, the assigned hooks will be automatically executedupstreamordownstreamaccording to the specification provided by the programmer.The programmer may wish to havetightorloosecouplingbetween targets and hooks, depending on the requirement. Hence, for a nice developer experience, this library provides two interfaces each representing aparadigm(tight or loose coupling) to cover the needs.From a hook, the programmer has access to the keyword arguments passed to the decorator, the arguments passed to the target, the target itself, and other useful information through aContextobject.Depending on whether the hook is executed upstream or downstream, the programmer can modify arguments to target, override the target itself with an arbitrarycallable, break the execution of the chain of hooks, modify the return of the target, et cetera.Why use this libraryThis library allows the programmer towrap,augment, oroverridea function or method with either a tight or loose coupling. It is therefore the perfect solution to create aplugin mechanismfor a project.It can also be used fordebugging,benchmarking,event-driven programming, implementing routing in a web development framework (Flaskuses decorators to implementrouting), et cetera.The interface of this library is designed to be intuitive to use not only forcraftinganAPIbut also forconsumingit.Back to topExamplesHere are some examples of using this library in the tight and loose coupling paradigm.Measure execution timeHere we will use the tight coupling paradigm to override the target function with a hook. From inside the hook, the target will be executed and we will measure the execution time.The following example can be copy-pasted into a file (e.g.test.py) and run as is:importtimefromhookingimportoverridedeftimeit(context,*args,**kwargs):# execute and measure the target run timestart_time=time.perf_counter()context.result=context.target(*args,**kwargs)total_time=time.perf_counter()-start_time# print elapsed timetext=context.config.get("text")# get 'text' from config dataprint(text.format(total=total_time))# decorate 'heavy_computation' with 'override' (tight coupling)# here, 'timeit' is the hook to execute when the target is called# all following keyword arguments are part of the configuration data@override(timeit,text="Done in{total:.3f}seconds !")defheavy_computation(a,b):time.sleep(2)# doing some heavy computation !returna*bif__name__=="__main__":# run 'heavy_computation'result=heavy_computation(6,9)print("Result:",result)$python-mtestDonein2.001seconds!
Result:54Back to topRouting by a fictional web frameworkThis example is divided into two parts:the server-side Python script;and the internals of the web framework.Server-side Python script# Script for the server-side (API consumer side)# web_script.pyfrommy_web_frameworkimportRouting,start# bind to 'home_view', three tags representing# three paths. This view will be executed when# the user will request the home page for example@Routing.tag("/")@Routing.tag("/home")@Routing.tag("/index")defhome_view():return"Welcome !"@Routing.tag("/about")defabout_view():return"About..."if__name__=="__main__":start()Web framework internals# Framework internals (API creator side)# my_web_framework.pyimportrandomfromhookingimportH# implementing custom routing mechanism by subclassing hooking.HRouting=H.subclass("Routing")defstart():# entry point of the web framework# Get user request, then serve the appropriate pagepath=get_user_request()serve_page(path)defget_user_request():# use randomness to simulate user page requestpaths=("/","/home","/index","/about")returnrandom.choice(paths)defserve_page(path):# get the list of functions and methods# tagged with the Routing.tag decoratorcache=Routing.targets.get(path,list())fortarget_infoincache:view=target_info.targethtml=view()render_html(html)defrender_html(html):print(html)Back to topFunctions and methods as targetsAs mentioned in theOverviewsection, functions and methods are the targets to which hooks are attached with a tight or loose coupling. A hook is a function that can be attached one or more times to one or more targets.Static methods, class methods, or decorated methods or functions should work fine with this library as long as one comes as close as possible to the native definition of the function or method. Example:fromhookingimportH,on_enter,on_leaveclassMyClass:# Good !@staticmethod@H.tag# innermostdefdo_something1(arg):pass# BAD !!!@H.tag# outermost@classmethoddefdo_something2(cls,arg):passdefmy_hook(context,*arg,**kwargs):pass# Good !@ExoticDecorator@on_enter(my_hook)# innermostdefmy_func():pass# BAD !!!@on_leave(my_hook)# outermost@ExoticDecoratordefmy_func():passBack to topAnatomy of a hookA hook is a callable that accepts an instance ofhooking.Contextand arguments passed to the target.Thehooking.Contextinstance exposes the following attributes:cls: the hook class;hid: the Hook ID (HID) as returned by the class methodsH.wrap,H.on_enter, andH.on_leave;tag: label string used to tag a function or method;config: dictionary representing keyword arguments passed to the decorator;spec: either thehooking.ENTERconstant or thehooking.LEAVEconstant;target: the decorated function or method;args: tuple representing the positional arguments passed to the target;kwargs: dictionary representing the keyword arguments passed to the target;result: depending on the context, this attribute may contain the value returned by the target;shared: ordered dictionary to store shared data between hooks (from upstream to downstream).The attributes listed above can be updated with theContext.updatemethod that accepts keyword arguments.fromhookingimportH# defining my_hookdefmy_hook(context,*args,**kwargs):ifcontext.tag!="target":raiseException("Wrong tag !")# reset argumentscontext.update(args=tuple(),kwargs=dict())@H.tag("target")# tagging my_func with "target"defmy_func(*args,**kwargs):pass# binding my_hook to the tag "target"H.on_enter("target",my_hook)Modify arguments to targetFrom an upstream hook, we can change the arguments passed to a target:# ...defupstream_hook(context,*args,**kwargs):# positional arguments are represented with a tuplecontext.args=(val1,val2)# keywords arguments are represented with a dictionarycontext.kwargs={"item1":val1,"item2":val2}# ...Back to topOverride the target from a hookThe library exposes thehooking.overrideto override a target function:fromhookingimportoverridedefmyhook(context,*args,**kwargs):context.result=new_target_function(*args,**kwargs)@override(myhook)deftarget():passbut one can still override the target from an arbitraryupstreamhook:fromhookingimporton_enterdefupstream_hook(context,*args,**kwargs):# override target with a new target that# that accepts same type signaturecontext.target=new_target_function@on_enter(upstream_hook)deftarget():passNote that you can setNonetocontext.targetto prevent the library for automatically running the target between the execution of upstream and downstream hooks.Back to topModify the return of a targetFrom a downstream hook, we can change the return of a target:# ...defdownstream_hook(context,*args,**kwargs):context.result=new_value# ...Back to topTight couplingIn this paradigm, hooks are directly bound to target. The library exposes the following decorators to decorate targets:DecoratorDescriptionSignaturehooking.overrideBind to a target a hook that will override it@override(hook, **config)hooking.wrapBind to a target two hooks that will be executed upstream and downstream@wrap(hook1, hook2, **config)hooking.on_enterBind to a target a hook that will be executed upstream, i.e, before the target@on_enter(hook, **config)hooking.on_leaveBind to a target a hook that will be executed downstream, i.e, after the target@on_leave(hook, **config)Override a targetThe library exposes thehooking.overridedecorator to bind to a target a hook that will override it:fromhookingimportoverridedefmyhook(context,*args,**kwargs):context.result=context.target(*args,**kwargs)# ormy_new_target=lambda*args,**kwargs:print("New Target Here !")context.result=my_new_target(*args,**kwargs)# override target with myhook@override(hook3,foo=42,bar="Alex")# foo and bar are config datadeftarget():passNote that with thehooking.overridedecorator, the programmer must execute the target or its replacement inside the hook and set the result tocontext.result.Back to topWrap a targetThehooking.wrapdecorator allows the programmer to wrap the target between two hooks that will be executed upstream and downstream. Two additional decoratorshooking.on_enterandhooking.on_leaveallow the programmer to bind either a upstream or a downstream hook to a target.fromhookingimportwrap,on_enter,on_leavedefhook1(context,*args,**kwargs):passdefhook2(context,*args,**kwargs):pass# bind an upstream hook and a downstream hook to my_func1@wrap(hook1,hook2,foo=42,bar="Alex")# foo and bar are config datadefmy_func1():pass# bind an upstream hook to my_func2@on_enter(hook1,foo=42,bar="Alex")defmy_func2():pass# bind a downstream hook to my_func3@on_leave(hook2,foo=42,bar="Alex")defmy_func3():passBack to topLoose couplingIn this paradigm, hooks aren't directly bound to target but to tags which are linked to targets. The library exposes thehooking.Hclass to support the loose coupling paradigm. In short, thehooking.H.tagdecorator is used to tag targets, then class methodshooking.H.on_enter,hooking.H.on_leave, andhooking.H.wrapare used to bind hooks to tags.Tagging mechanismThehooking.H.tagclass method allows you to tag a function or a method:fromhookingimportH@H.tagdefmy_func(*args,**kwargs):passclassMyClass:@H.tagdefmy_method(self,*args,**kwargs):passThehooking.H.tagdecorator accepts alabelstring as argument. By default, when this argument isn't provided, the library uses thequalified nameof the method or function as thelabel.Here we provide thelabelargument:fromhookingimportH@H.tag("my_func")defmy_func(*args,**kwargs):passclassMyClass:@H.tag("MyClass.my_method")defmy_method(self,*args,**kwargs):passBack to topBind hooks to tagsThese are thehooking.Hclass methods to bind hooks to tags:Class methodDescriptionSignaturehooking.H.on_enterBind to a tag a hook that will be executed downstream, i.e, before the targeton_enter(tag, hook)hooking.H.on_leaveBind to a tag a hook that will be executed downstream, i.e, after the targeton_leave(tag, hook)hooking.H.wrapBind to a tag two hooks that will be executed upstream and downstreamwrap(tag, hook1, hook2)fromhookingimportH@H.tag("target")defmy_func(*args,**kwargs):passdefmy_hook1(context,*args,**kwargs):passdefmy_hook2(context,*args,**kwargs):pass# bind my_hook1 to "target" and run it upstreamhook_id=H.on_enter("target",my_hook1)# bind my_hook2 to "target" and run it downstreamhook_id=H.on_leave("target",my_hook2)# bind my_hook1 and my_hook2 to "target"hook_id1,hook_id2=H.wrap("target",my_hook1,my_hook2)Unbind hooksWhenever a hook is bound to a tag, the Hook ID (HID) which could be used later tounbindthe hook, is returned:fromhookingimportHdefhook(context,*args,**kwargs):pass# bindhid=H.on_enter("tag",hook)# unbindH.unbind(hid)Multiplehooks can be unbound in a single statement:fromhookingimportHdefhook1(context,*args,**kwargs):passdefhook2(context,*args,**kwargs):pass# bindhid1=H.on_enter("tag",hook1)hid2=H.on_leave("tag",hook2)# unbind multiple hooks manuallyH.unbind(hid1,hid2)Clear hooks bound to a specific tagTheclearclass method ofhooking.Hunbinds all hooks bound to a specific tag:fromhookingimportHdefhook1(context,*args,**kwargs):passdefhook2(context,*args,**kwargs):pass@H.tagdeftarget():pass# bindhid1=H.on_enter("target",hook1)hid2=H.on_enter("target",hook2)# unbind hook1 and hook2 from "target"H.clear("target")You can clearmultipletags in a single statement:fromhookingimportHdefhook1(context,*args,**kwargs):passdefhook2(context,*args,**kwargs):pass@H.tagdeftarget1():pass@H.tagdeftarget2():pass# bindhid1=H.on_enter("target1",hook1)hid2=H.on_enter("target2",hook2)# unbind hook1 and hook2 from "target1" and "target2"H.clear("target1","target2")Back to topChain breakThis library exposes anExceptionsubclass to allow the programmer to break the execution of a chain of hooks:fromhookingimportH,ChainBreak@H.tag("target")defmy_func(*args,**kwargs):passdefhook1(context,*args,**kwargs):passdefhook2(context,*args,**kwargs):raiseChainBreakdefhook3(context,*args,**kwargs):pass# bind hook1, hook2 and hook3 to 'target'forhookin(hook1,hook2,hook3):H.on_enter("target",hook)# call the targetmy_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 ignoredBack to topFreeze the hooking classWe could freeze the hooking class and thus prevent the execution of all hooks:fromhookingimportH@H.tagdefmy_func(*args,**kwargs):passH.freeze()# from now, no hook will be executed anymoreUnfreeze the hooking classTounfreezethe hooking class, use theH.unfreezeclass method:fromhookingimportHH.unfreeze()# from now, hooks will be executed when neededBack to topExposed dataThe library exposes data through a class method, class variables, and data transfer objects (namedtuples).Get the list of upstream and downstream hooksUpstream and downstream hooks bound to a specific tag can be retrieved with theget_hooksclass method.fromhookingimportH# returns a 2-tuple of upstream hooks and# downstream hooksupstream_hooks,downstream_hooks=H.get_hooks("tag")# iterate through upstream_hooks which is# a list of instances of hooking.HookInfoforhook_infoinupstream_hooks:print(hook_info)Read-only class variablesThehooking.Hclass exposes the following class variables:Class variableDescriptiontargetsOrdered dictionary. Keys are tags and values are lists of instances ofhooking.TargetInfo. Example: {"tag1": [TargetInfo(), TargetInfo(), ...], ...}hooksOrdered dictionary. Keys are tags and values are lists of instances ofhooking.HookInfo. Example: {"tag1": [HookInfo(), HookInfo(), ...], ...}tagsThe set of registered tagsfrozenBoolean to tell whether the hooking mechanism is frozen or notNote:it is not recommended to modify the contents of these class variables directly. Use the appropriate class methods for this purpose.Bothhooking.TargetInfoandhoooking.HookInfoare namedtuples that will be explored in the next subsection.Data transfer objectHere are the fields from thehooking.TargetInfonamedtuple:cls: the hooking class;tag: the string label that represents the tag;target: the target method or function;config: dictionary containing the configuration data passed to the decorator.Here are the fields from thehooking.HookInfonamedtuple:cls: the hooking class;hid: the hook identifier;hook: the callable representing the hook;tag: the string label that represents the tag;spec: eitherhooking.ENTERorhooking.LEAVE.Back to topReset the hooking classYou may need to reset the hooking class, i.e., reinitialize the contents of the following class variables:hooking.H.hooks,hooking.H.tags, andhooking.H.frozen. In this case, you just have to call thehooking.H.resetclass method.Note:targets won't be removed.Back to topSubclassing the hooking classThis library is flexible enough to allow the programmer to create their own subclass ofhooking.Hlike this:fromhookingimportHMyCustomHookingClass=H.subclass("MyCustomHookingClass")Subclassinghooking.Hallows the programmer to apply theseparation of concerns. For example, a web framework creator might create aRoutingsubclass to implement a routing mechanism, and also create anExtensionsubclass to implement a plugin mechanism. Each subclass would have its own set of tags, hooks, and targets.Note:class variables are automatically reset when subclassinghooking.H.Back to topMiscellaneousMultithreadingWhenever threads are introduced into a program, the state shared between threads becomes vulnerable to corruption. To avoid this situation, this library usesthreading.Lockas a synchronization tool.Back to topInstallationHookingiscross-platformand should work onPython 3.5ornewer.First time$pipinstallhookingUpgrade$pipinstallhooking--upgrade--upgrade-strategyeagerShow package information$pipshowhookingBack to top