Python: Register hooks to existing code to get control when a function is called

836 Views Asked by At

I'd like to get control (to execute some pre-emptive tasks) when a function is called in Python without modifying the source program, e.g., when calling test()

def test(i : int, s: str) -> int:
    pass

I'd like a function myobserver to be called, and have some way to inspect (maybe even modify?!) the parameters? Think of it sorta like a mini-debugger, e.g., to add logging to an existing program that can't/shouldn't be modified?

def myobserver(handle)
    name = get_name(handle)
    for n, arg in enumerate(get_arg_iterator(handle)):
        print("Argument {n} of function {name}: {arg}")

ETA: I am not looking for the traditional decorator, because adding a decorator requires changing the source code. (In this sense, decorators are nicer than adding a print, but still similar because they require changes to source.)

1

There are 1 best solutions below

5
anurag On

Your are looking for python decorators:

from functools import wraps
def debugger(func):
    @wraps(func)
    def with_logging(*args, **kwargs):
        print('"'+func.__name__+'({},{})"'.format(*args, **kwargs)+" was invoked")
        # -------------------
        # Your logic here
        # -------------------
        return func(*args, **kwargs)
    return with_logging

@debugger
def test(i : int, s: str) -> int:
    print('We are in test', i, s)

test(10, 'hello')

EDIT

Since the decorator method mentioned above interferes with the source code (have to apply the @ decorators), I propose the following:

# This is source code to observe, should not be _touched_!
class SourceCode:
    def __init__(self, label):
        self.label = label

    def test1(self, i, s):
        print('For object labeled {}, we are in {} with param {}, {}'.format(self.label, 'test1', i, s))

    def test2(self, k):
        print('For object labeled {}, we are in {} with param {}'.format(self.label, 'test2', k))

What I propose is perform some manual effort in writing the hooks, I am not sure if this is feasible (just occured to me, hence adding):

from functools import wraps

# the following is pretty much syntactic and generic
def hook(exist_func, debugger):
    @wraps(exist_func)
    def run(*args, **kwargs):
        return debugger(exist_func, *args, **kwargs)
    return run

# here goes your debugger
def myobserver(orig_func, *args, **kwargs):
    # -----------------------
    # Your logic goes here
    # -----------------------
    print('Inside my debugger')
    return orig_func(*args, **kwargs)


# ---------------------------------
obj = SourceCode('Test')

# making the wrapper ready to receive
no_iterference_hook1 = hook(obj.test1, myobserver)
no_iterference_hook2 = hook(obj.test2, myobserver)

# call your debugger on the function of your choice
no_iterference_hook1(10, 'hello')
no_iterference_hook2('Another')