Get last function's call arguments from traceback?
-
22-07-2019 - |
Question
Can I get the parameters of the last function called in traceback? How?
I want to make a catcher for standard errors to make readable code, yet provide detailed information to user.
In the following example I want GET_PARAMS to return me a tuple of parameters supplied to os.chown. Examining the inspect
module advised by Alex Martelli, I couldn't find that.
def catch_errors(fn):
def decorator(*args, **kwargs):
try:
return fn(*args, **kwargs)
except (IOError, OSError):
msg = sys.exc_info()[2].tb_frame.f_locals['error_message']
quit(msg.format(SEQUENCE_OF_PARAMETERS_OF_THE_LAST_FUNCTION_CALLED)\
+ '\nError #{0[0]}: {0[1]}'.format(sys.exc_info()[1].args), 1)
return decorator
@catch_errors
def do_your_job():
error_message = 'Can\'t change folder ownership \'{0}\' (uid:{1}, gid:{2})'
os.chown('/root', 1000, 1000) # note that params aren't named vars.
if __name == '__main__' and os.getenv('USERNAME') != 'root':
do_your_job()
(Thanks to Jim Robert for the decorator)
Solution
The problem with using a decorator for what you're trying to achieve is that the frame the exception handler gets is do_your_job()
s, not os.listdir()
s, os.makedirs()
s or os.chown()
s. So the information you'll be printing out is the arguments to do_your_job()
. In order to get the behavior I think you intend, you would have to decorate all the library functions you're calling.
OTHER TIPS
For such inspection tasks, always think first of module inspect
in the standard library. Here, inspect.getargvalues gives you the argument values given a frame, and inspect.getinnerframes gives you the frames of interest from a traceback object.
Here is an example of such function and some problems that you can't get around:
import sys
def get_params(tb):
while tb.tb_next:
tb = tb.tb_next
frame = tb.tb_frame
code = frame.f_code
argcount = code.co_argcount
if code.co_flags & 4: # *args
argcount += 1
if code.co_flags & 8: # **kwargs
argcount += 1
names = code.co_varnames[:argcount]
params = {}
for name in names:
params[name] = frame.f_locals.get(name, '<deleted>')
return params
def f(a, b=2, c=3, *d, **e):
del c
c = 4
e['g'] = 6
assert False
try:
f(1, f=5)
except:
print get_params(sys.exc_info()[2])
The output is:
{'a': 1, 'c': 4, 'b': 2, 'e': {'g': 6, 'f': 5}, 'd': ()}
I didn't used inspect.getinnerframes()
to show another way to get needed frame. Although it simplifies a bit, it also do some extra work that is not needed for you while being relatively slow (inspect.getinnerframes()
reads source file for every module in traceback; this is not important for one debugging call, but could be an issue in other cases).