Search code examples
pythonmockingmonkeypatching

python monkey patching an object's method within a function via a decorator


I'm trying to learn some advanced decorator usage. Specifically, I'm trying to to monkey patch a class's method via a decorator within a function.

This is a basic example to illustrate what I'm trying to do. I have a function something that does some stuff; and within that function there's an instance of a class. That instance I would like to monkey patch.

from functools import update_wrapper

class Foobar:
    def get_something(self):
        return "apple"

class FakeFoobar:
    def get_something(self):
        return "orange"

class my_decorator:
    def __init__(self, original_function):
        self._original_function = original_function
        update_wrapper(self, original_function)

    def __call__(self, *args, **kwargs):
        # some magic here?
        return self._original_function(*args, **kwargs)

@my_decorator
def something():
    f = Foobar()
    return f.get_something()

if __name__ == '__main__':
    print(something())

I'm trying either trying to do a 1 to 1 replacement with Foobar to FakeFoobar or, monkey patch Foobar's get_something method to FakeFoobar's get_something method.

When I run the code above, I get the following:

>>> something()
'apple'
>>>

I would like to find some way augment the Foobar's get_something method so that we get the following output:

>>> something()
'orange'
>>>

There's a mock module within the unittests library, however, it's not clear to be how I could leverage that for my use case. I'm fairly married to the idea of not passing an argument into the decorator or an extra argument into the something function as many of the examples of the mock library show.

I also notice that the moto library is accomplishing something similar to what I'm trying to do. I tried digging into the source code, but it seems fairly complex for what I'm trying to do.


Solution

  • How about updating the global variables dict of the function?

    from functools import update_wrapper
    
    class Foobar:
        def get_something(self):
            return "apple"
    
    class FakeFoobar:
        def get_something(self):
            return "orange"
    
    class my_decorator:
        def __init__(self, original_function):
            self._original_function = original_function
            update_wrapper(self, original_function)
    
        def __call__(self, *args, **kwargs):
            f = self._original_function
            restore_val = f.func_globals['Foobar']
            f.func_globals['Foobar'] = f.func_globals['FakeFoobar']
            # ^^^^^ This is your magic-line.
            try:
                return f(*args, **kwargs)
            except:
                raise
            finally:
                f.func_globals['Foobar'] = restore_val
    
    @my_decorator
    def something():
        f = Foobar()
        return f.get_something()
    
    if __name__ == '__main__':
        print(something())   #Prints orange
        print(Foobar().get_something()) #Prints apple