4

Basically I want to do something like this: How can I hook a function in a python module?

but I want to call the old function after my own code.

like

import whatever

oldfunc = whatever.this_is_a_function

def this_is_a_function(parameter):
    #my own code here
    # and call original function back
    oldfunc(parameter)

whatever.this_is_a_function = this_is_a_function

Is this possible?

I tried copy.copy, copy.deepcopy original function but it didn't work.

Community
  • 1
  • 1
Konrad
  • 6,385
  • 12
  • 53
  • 96
  • 1
    It depends on what `whatever` is. What is it in this case? Is it your own module? – Jared Goguen Mar 02 '16 at 21:24
  • no it's not my own module, it is a module registered from C++ code by application I don't have source code for(I know I can hook function in C++ but I'm interested if it's possible straight from python) – Konrad Mar 02 '16 at 21:25
  • The code you've posted looks fine, aside from perhaps `return oldfunc(parameter)` instead of just `oldfunc(parameter)`. Are you having a problem with it? – user2357112 Mar 02 '16 at 21:34

4 Answers4

11

Something like this? It avoids using globals, which is generally a good thing.

import whatever
import functools

def prefix_function(function, prefunction):
    @functools.wraps(function)
    def run(*args, **kwargs):
        prefunction(*args, **kwargs)
        return function(*args, **kwargs)
    return run

def this_is_a_function(parameter):
    pass # Your own code here that will be run before

whatever.this_is_a_function = prefix_function(
    whatever.this_is_a_function, this_is_a_function)

prefix_function is a function that takes two functions: function and prefunction. It returns a function that takes any parameters, and calls prefunction followed by function with the same parameters. The prefix_function function works for any callable, so you only need to program the prefixing code once for any other hooking you might need to do.

@functools.wraps makes it so that the docstring and name of the returned wrapper function is the same.

If you need this_is_a_function to call the old whatever.this_is_a_function with arguments different than what was passed to it, you could do something like this:

import whatever
import functools

def wrap_function(oldfunction, newfunction):
    @functools.wraps(function)
    def run(*args, **kwargs):
        return newfunction(oldfunction, *args, **kwargs)
    return run

def this_is_a_function(oldfunc, parameter):
    # Do some processing or something to customize the parameters to pass
    newparams = parameter * 2  # Example of a change to newparams
    return oldfunc(newparams)

whatever.this_is_a_function = wrap_function(
        whatever.this_is_a_function, this_is_a_function)

There is a problem that if whatever is a pure C module, it's typically impossible (or very difficult) to change its internals in the first place.

Alyssa Haroldsen
  • 3,652
  • 1
  • 20
  • 35
3

So, here's an example of monkey-patching the time function from the time module.

import time

old_time = time.time

def time():
    print('It is today... but more specifically the time is:')
    return old_time()

time.time = time

print time.time()
# Output:
# It is today... but more specifically the time is:
# 1456954003.2

However, if you are trying to do this to C code, you will most likely get an error like cannot overwrite attribute. In that case, you probably want to subclass the C module.

You may want to take a look at this question.

Community
  • 1
  • 1
Jared Goguen
  • 8,772
  • 2
  • 18
  • 36
3

This is the perfect time to tout my super-simplistic Hooker

def hook(hookfunc, oldfunc):
    def foo(*args, **kwargs):
        hookfunc(*args, **kwargs)
        return oldfunc(*args, **kwargs)
    return foo

Incredibly simple. It will return a function that first runs the desired hook function (with the same parameters, mind you) and will then run the original function that you are hooking and return that original value. This also works to overwrite a class method. Say we have static method in a class.

class Foo:
    @staticmethod
    def bar(data):
        for datum in data:
            print(datum, end="") # assuming python3 for this
        print()

But we want to print the length of the data before we print out its elements

def myNewFunction(data):
    print("The length is {}.".format(len(data)))

And now we simple hook the function

Foo.bar(["a", "b", "c"])
# => a b c
Foo.bar = hook(Foo.bar, myNewFunction)
Foo.bar(["x", "y", "z"])
# => The length is 3.
# => x y z 
Goodies
  • 4,439
  • 3
  • 31
  • 57
  • 1
    Why use `staticmethod` instead of just plain functions? – Alyssa Haroldsen Mar 02 '16 at 21:51
  • For the Hooker class, an instance variable is entirely unnecessary. I added an init function so it CAN be used, i.e. `h = Hooker()`, but it isn't necessary. If I don't use `@staticmethod`, then an instance would have had to have been declared. – Goodies Mar 02 '16 at 21:52
  • No, no. Why does `Hooker` exist at all? Why is `hook` a static method of a `Hooker` class instead of just a module-level function? – user2357112 Mar 02 '16 at 21:53
  • @Goodies I suppose I meant "why encapsulate in a class at all"? Considering this is a utility function, I feel this would make most sense as a plain function in its own module, much like `functools.wraps`. – Alyssa Haroldsen Mar 02 '16 at 21:53
  • Oh, I see. Well, personally, I just have this sitting in my libs. From any file I can import Hooker. The file is called Hooker.py and there are no classes in it. yes, it is unnecessary. – Goodies Mar 02 '16 at 21:56
  • Shouldn't it be the other way around? `Foo.bar = hook(myNewFunction, Foo.bar)`. foo returns `oldfunc` – Agey Jun 01 '17 at 09:37
  • Just to make sure, the final two outputs are reversed yes? – dia Dec 04 '22 at 04:29
1

Actually, you can replace the target function's func_code. The example below

# a normal function
def old_func():
    print "i am old"

# a class method
class A(object):
    def old_method(self):
        print "i am old_method"

# a closure function
def make_closure(freevar1, freevar2):
    def wrapper():
        print "i am old_clofunc, freevars:", freevar1, freevar2
    return wrapper
old_clofunc = make_closure('fv1', 'fv2')

# ===============================================

# the new function
def new_func(*args):
    print "i am new, args:", args
# the new closure function
def make_closure2(freevar1, freevar2):
    def wrapper():
        print "i am new_clofunc, freevars:", freevar1, freevar2
    return wrapper
new_clofunc = make_closure2('fv1', 'fv2')

# ===============================================

# hook normal function
old_func.func_code = new_func.func_code
# hook class method
A.old_method.im_func.func_code = new_func.func_code
# hook closure function
# Note: the closure function's `co_freevars` count should be equal
old_clofunc.func_code = new_clofunc.func_code

# ===============================================

# call the old
old_func()
A().old_method()
old_clofunc()

output:

i am new, args: ()
i am new, args: (<__main__.A object at 0x0000000004A5AC50>,)
i am new_clofunc, freevars: fv1 fv2
Baloo
  • 11
  • 3