How to inject a line of code into an existing function?

I have a function looking like following:

async def callback(ctx, first_arg, second_arg=None)
    ...

Where the parameter names, the count and everything can always be different. In the first paremeter is a method called respond(), and I want to make a decorator that returns the modified callback, where ctx.respond() is called.

I thought of backing up the original callback and then creating a “fake” callback where the ctx.respond() method is called and then the real callback, but it won’t work because after my decorator there is another decorator that will check on the functions parameters

The decorator

def auto_respond():
    def wraper(callback):
        # this is what I thought of
        b_callback = callback
        async def auto_callback(ctx, here comes the problem):
            await ctx.respond()
            return await b_callback(ctx, the problem is here too)
        return auto_callback
    return wraper

The problem is, that I cannot set the right parameters of the function, because I never now how what they are going to be.

Originally, I thought of maybe using *args and **kwargs to receive the params and directly pass them over, but then the check wouldn’t work


Let’s say I have this example

@the_other_decorator()
@auto_respond()
async def callback(ctx, user=None):
    ...

And in the other decorator, the paremeters will be checked with inspect.signature(callback).parameters

the_other_decorator

def the_other_decorator():
    def wraper(callback):
        params = inspect.signature(callback).parameters
        for name in params:
            param = params.get(name)
            if param.annotation != param.empty:
                ...
            elif param.default != inspect._empty:
                ...
        ...
    return wraper

So my next soloution was that if I somehow could “inject” a line of code to the callback function, get the first argument and use the respond method of it, I would “bypass” the param check

Note that the params checks are needed because I need to get some information about them in the decorator

So now there are two options left, the first one is to inject a line of code as I said or somehow “clone” the parameters of the callback function and set them to the “fake” callback function

By the way, sorry for the bad English, if I didn’t express myself right or some information are missing in this problem, please tell me so I can improve the question!

Answer

I think you want something like this (I removed the async stuff for simplicity, feel free to add them back):

import functools
import inspect
def auto_respond(func):
    # This will preserve the signature of func
    @functools.wraps(func)
    def wraper(*args, **kwargs):
        ctx = args[0]
        ctx.respond()
        return func(*args, **kwargs)
    return wraper

class Context:
    def respond(self):
        print("Response")

my_context = Context()  
@auto_respond
def my_func_1(ctx, arg1, arg2):
    print(arg1, arg2)
my_func_1(my_context, "a", "b")
print(inspect.signature(my_func_1).parameters)

@auto_respond
def my_func_2(ctx, arg1, arg2, arg3, arg4=None):
    print(arg1, arg2, arg3, arg4)
my_func_2(my_context, "a", "b", "c", "d")
print(inspect.signature(my_func_2).parameters)

>>>>>>
Response
a b
OrderedDict([('ctx', <Parameter "ctx">), ('arg1', <Parameter "arg1">), ('arg2', <Parameter "arg2">)])
Response
a b c d
OrderedDict([('ctx', <Parameter "ctx">), ('arg1', <Parameter "arg1">), ('arg2', <Parameter "arg2">), ('arg3', <Parameter "arg3">), ('arg4', <Parameter "arg4=None">)])

If you want to get the function and store it somewhere, you can also just do this:

def my_func_3(ctx, arg1, arg2):
    print(arg1, arg2)
my_auto_respond_func = auto_respond(my_func_3)

my_auto_respond_func(my_context, "a", "b")

To answer the second part of your question, you can chain another decorator on top of this one by using the same structure as auto_respond, and you’ll be able to check the parameters as these are passed through *args and **kwargs as if the first decorator did not exist thanks to @functools.wraps