1""" 2These classes perform some python magic that we use to implement the nesting of exploration technique methods. 3This process is formalized as a "hooking" of a python method - each exploration technique's methods "hooks" a method of the same name on the simulation manager class. 4""" 5 6class HookSet: 7 """ 8 A HookSet is a static class that provides the capability to apply many hooks to an object. 9 """ 10 @staticmethod 11 def install_hooks(target, **hooks): 12 """ 13 Given the target `target`, apply the hooks given as keyword arguments to it. 14 If any targeted method has already been hooked, the hooks will not be overridden but will instead be pushed 15 into a list of pending hooks. The final behavior should be that all hooks call each other in a nested stack. 16 17 :param target: Any object. Its methods named as keys in `hooks` will be replaced by `HookedMethod` objects. 18 :param hooks: Any keywords will be interpreted as hooks to apply. Each method named will hooked with the 19 coresponding function value. 20 """ 21 for name, hook in hooks.items(): 22 func = getattr(target, name) 23 if not isinstance(func, HookedMethod): 24 func = HookedMethod(func) 25 setattr(target, name, func) 26 func.pending.append(hook) 27 28 @staticmethod 29 def remove_hooks(target, **hooks): 30 """ 31 Remove the given hooks from the given target. 32 33 :param target: The object from which to remove hooks. If all hooks are removed from a given method, the 34 HookedMethod object will be removed and replaced with the original function. 35 :param hooks: Any keywords will be interpreted as hooks to remove. You must provide the exact hook that was applied 36 so that it can it can be identified for removal among any other hooks. 37 """ 38 for name, hook in hooks.items(): 39 hooked = getattr(target, name) 40 if hook in hooked.pending: 41 try: 42 hooked.pending.remove(hook) 43 except ValueError as e: 44 raise ValueError("%s is not hooked by %s" % (target, hook)) from e 45 if not hooked.pending: 46 setattr(target, name, hooked.func) 47 48 49class HookedMethod: 50 """ 51 HookedMethod is a callable object which provides a stack of nested hooks. 52 53 :param func: The bottom-most function which provides the original functionality that is being hooked 54 55 :ivar func: Same as the eponymous parameter 56 :ivar pending: The stack of hooks that have yet to be called. When this object is called, it will pop the last 57 function in this list and call it. The function should call this object again in order to request 58 the functionality of the original method, at which point the pop-dispatch mechanism will run 59 recursively until the stack is exhausted, at which point the original function will be called. 60 When the call returns, the hook will be restored to the stack. 61 """ 62 63 def __init__(self, func): 64 self.func = func 65 self.pending = [] 66 67 def __repr__(self): 68 return "<HookedMethod(%s.%s, %d pending)>" % \ 69 (self.func.__self__.__class__.__name__, self.func.__name__, len(self.pending)) 70 71 def __call__(self, *args, **kwargs): 72 if self.pending: 73 current_hook = self.pending.pop() 74 try: 75 result = current_hook(self.func.__self__, *args, **kwargs) 76 finally: 77 self.pending.append(current_hook) 78 return result 79 else: 80 return self.func(*args, **kwargs) 81