Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Get call string for a Python function, as typed on the command line

I would like to store the exact string that was typed for a function call, from the function itself using introspection (I cannot/don't want to hack the command line interpreter -- so, for example getting history from readline or whatever is not what I am looking for).

Let's say, if user typed:

>>> myfunc('a', 'b', 1, mykwarg='hello')

I would like to get the call string (i.e. myfunc('a', 'b', 1, mykwarg='hello')) from the code inside myfunc.

I can craft something like this:

def myfunc(a,b,c,mykwarg=None):
    frame = inspect.currentframe()
    sig = inspect.signature(myfunc)
    args = []
    for param in sig.parameters.values():
        if param.name in frame.f_locals:
                args.append(f"{param.name}={str(frame.f_locals[param.name])}")
    cmd = f"{frame.f_code.co_name}({','.join(args)})"

    print(cmd)

I get:

>>> myfunc('a', 'b', 1, mykwarg='hello')
myfunc(a=a,b=b,c=1,mykwarg=hello)

Which is not exactly what user typed. Also, I hope there is something more robust and less 'hackish' to try...

Use case: I want to be able to associate a command call from my library with its result. I do not want to hard-code the command call storage for each function, I would prefer to use a decorator or something like that. This is probably much easier to do from the REPL, but I would like to not depend on it (like, if user calls the function from its own program, it should still be able to associate the command call with the result).

like image 796
mguijarr Avatar asked Aug 23 '26 14:08

mguijarr


1 Answers

Finally I answer my own question, hopefully it can help someone else one day.

I decided to try to go the dis way, ie. "disassembling" the Python code object of the outer frame calling my function, to see how it has been called to be able to reconstruct the command line:

import dis
import inspect
import io
import ast
import re

def get_function_call_string():
    in_func = False
    args=[]
    kwargs=[]
    func_name = inspect.stack()[1][3]
    frame = inspect.currentframe().f_back.f_back
    dis_output = io.StringIO()
    dis.dis(frame.f_code, file=dis_output)
    for line in dis_output.getvalue().split('\n'):
        instr = re.findall(r'^.*\s([A-Z_]+)\s*', line)[0]
        if instr.startswith("CALL_FUNCTION") and in_func:
           break
        elif instr.startswith('LOAD_'):
            name = re.findall(r'^.*\s\((.+)\)$', line)[0]
            if in_func:
                if name.startswith('('):
                    kwargs = ast.literal_eval(name)
                else:
                    args.append(name)
            elif name == func_name:
                in_func = True
    kwargs = [f"{a}={args.pop()}" for a in kwargs]
    return f"{func_name}({', '.join(args)}{', ' if kwargs else ''}{', '.join(kwargs)})"

Example:

>>> def f(a,b,arg="toto"):
        print(get_function_call_string())
>>> f(1,"test","example")
f(1, 'test', 'example')
>>> f(1, "test", arg="hello")
(1, 'test', arg='hello')

It is not a complete answer, since it cannot handle some argument types like dict or list... I maybe will continue on this or change my mind and do differently.

like image 137
mguijarr Avatar answered Aug 26 '26 05:08

mguijarr



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!