Is there any tool/library through which the list of methods/functions called within another methods/functions can be listed?
For example: If that tool or library runs for below method
def calculate(a: int, b: int, operator: Operator):
if operator == Operator.add:
add(a, b)
elif operator == Operator.subtract
subtract(a, b)
then it should return
1. add
2. subtract
This question is almost same as this one but it's for Java.
This is basically same as what PyCharm does for Find Usage.
Thanks!
2 Answers 2
This seems to do the work:
import dis
def list_func_calls(fn):
funcs = []
bytecode = dis.Bytecode(fn)
instrs = list(reversed([instr for instr in bytecode]))
for (ix, instr) in enumerate(instrs):
if instr.opname=="CALL_FUNCTION":
load_func_instr = instrs[ix + instr.arg + 1]
funcs.append(load_func_instr.argval)
return ["%d. %s" % (ix, funcname) for (ix, funcname) in enumerate(reversed(funcs), 1)]
Example:
>>> list_func_calls(calculate)
['1. add', '2. subtract']
What's happening here is:
- we make a Bytecode object of the function
- we reverse the list of instructions, since the function name will follow the function call
we step through the list, and for each CALL_FUNCTION instruction,
we use the instructions
argparameter to tell us how many arguments we're gettingwe look one past that to find the instruction that loads the function we're calling
we add that function's name (
instr.argval) to a list which we then reverse, enumerate, and return in the requested format
Note that since Python 3.6, there are three CALL_FUNCTION instructions, so you'll have to check the documentation to extend this example to be fully functional with current python
10 Comments
"." not in entry.argvalUpdate: added compatibility for Python2.7
Tested and confirmed working with Python2.7, Python3.5 and Python3.6
Credit for pointing out dis goes to Patrick Haugh1
Implementation (parsing of the dis output) is my own:
Setup:
import dis
import sys
from contextlib import contextmanager
# setup test environment
def a(_,__):
pass
def b(_,__,___):
pass
def c(_):
pass
def g():
pass
d = 4
def test(flag):
e = c
if flag:
a(a(b,c), [l for l in g(1, x=2)])
else:
b(a, int(flag), c(e))
d = d + 1
def calculate(a, b, operator):
if operator == Operator.add:
add(a, b)
elif operator == Operator.subtract:
subtract(a, b)
class Operator(object):
add = "add"
subtract = "subtract"
Python 2/3 compatibility:
class AttrDict(dict):
def __init__(self, *args, **kwargs):
super(AttrDict, self).__init__(*args, **kwargs)
self.__dict__ = self
@contextmanager # https://stackoverflow.com/a/12111817/2422125
def captureStdOut(output):
stdout = sys.stdout
sys.stdout = output
try:
yield
finally:
sys.stdout = stdout
""" for Python <3.4 """
def get_instructions(func):
import StringIO
out = StringIO.StringIO()
with captureStdOut(out):
dis.dis(func)
return [AttrDict({
'opname': i[16:36].strip(),
'arg': int(i[37:42].strip() or 0),
'argval': i[44:-1].strip()
}) for i in out.getvalue().split("\n")]
if sys.version_info < (3, 4):
dis.get_instructions = get_instructions
import __builtin__ as builtin
else:
import builtins as builtin
Code:
def get_function_calls(func, built_ins=False):
# the used instructions
ins = list(dis.get_instructions(func))[::-1]
# dict for function names (so they are unique)
names = {}
# go through call stack
for i, inst in list(enumerate(ins))[::-1]:
# find last CALL_FUNCTION
if inst.opname[:13] == "CALL_FUNCTION":
# function takes ins[i].arg number of arguments
ep = i + inst.arg + (2 if inst.opname[13:16] == "_KW" else 1)
# parse argument list (Python2)
if inst.arg == 257:
k = i+1
while k < len(ins) and ins[k].opname != "BUILD_LIST":
k += 1
ep = k-1
# LOAD that loaded this function
entry = ins[ep]
# ignore list comprehensions / ...
name = str(entry.argval)
if "." not in name and entry.opname == "LOAD_GLOBAL" and (built_ins or not hasattr(builtin, name)):
# save name of this function
names[name] = True
# reduce this CALL_FUNCTION and all its paramters to one entry
ins = ins[:i] + [entry] + ins[ep + 1:]
return sorted(list(names.keys()))
Output:
> print(get_function_calls(test))
> ['a', 'b', 'c', 'g']
> print(get_function_calls(test, built_ins=True))
> ['a', 'b', 'c', 'g', 'int']
> print(get_function_calls(calculate))
> ['add', 'subtract']
1As Patrick Haugh's comment about dis is over 2h old I consider this one free for taking...
10 Comments
CALL_FUNCTION_KW (and CALL_FUNCTION_EX). Worth noting may also be that it will also catch things like class instantiation and builtins like dict() and tuple(), int()... technically not wrong but probably not what the user might think of.CALL_FUNCTION_EX will get funny ... eta sometime today ... I thinkbuiltin here before hasattr(builtin, name) so you get NameError when you set built_ins=False. If you set built_ins=True it won't include builtins with keywoard arguments, e.g. enumerate([1,2], start=1).
disto disassemble the function. Look for theCALL_FUNCTIONbytecodes. You'll have to work backwards from them to get the function they're calling (which won't always be there).