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).
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With