I'm trying to get the source, callee list, defaults, keywords, args and varargs of the functions in a python script.
Currently, I'm importing the module and using the python inspect
module's getmembers
function and passing the isfunction
parameter like so:
members = inspect.getmembers(myModule, inspect.isfunction)
However, this method doesn't work if myModule
's imports aren't available to me (since myModule
has to be imported first).
I tried using the python ast
module to parse
and dump
the syntax tree, but getting the function source involved very hacky techniques and/or questionable and far from maintainable third party libraries. I believe I've scoured the documentation and stackoverflow pretty thoroughly and have failed to find a suitable solution. Am I missing something?
So I looked around some more and quickly Frankenstein'd a solution using this dude's answer to get each function's source. It isn't anywhere near perfect yet, but here it is if you're interested:
import ast
import re
import json
st = open('filename.py').read()
tree = ast.parse(st)
functions_info = {}
def parse(function):
global st
global functions_info
fn_info = {}
fn_info['Args'] = []
fn_info['Source'] = []
fn_info['Callees'] = []
print(function.name)
for arg in function.args.args:
fn_info['Args'].append(arg.arg)
lastBody = function.body[-1]
while isinstance (lastBody,(ast.For,ast.While,ast.If)):
lastBody = lastBody.Body[-1]
lastLine = lastBody.lineno
if isinstance(st,str):
st = st.split("\n")
for i , line in enumerate(st,1):
if i in range(function.lineno,lastLine+1):
# print(line)
fn_info['Source'].append(line)
for line in fn_info['Source']:
if not line.lstrip().startswith('#'):
fn_pattern = r'(\w+)\('
match = re.search(fn_pattern, line)
if match:
callee = match.group(1)
fn_info['Callees'].append(callee)
functions_info[function.name] = fn_info
for obj in tree.body:
if isinstance(obj, ast.ClassDef):
for func in obj.body:
if isinstance(func, (ast.FunctionDef)):
parse(func)
if isinstance(obj, ast.FunctionDef):
parse(obj)
print(json.dumps(functions_info, indent=4))
Output:
{
"displayWonder": {
"Source": [
" def displayWonder(self):",
" return \"Hello \" + str(self.displayGreeting())"
],
"Args": [
"self"
],
"Callees": []
},
"displayGreeting": {
"Source": [
" def displayGreeting(self):",
" string = \"Greetings \" + self.myName",
" return string"
],
"Args": [
"self"
],
"Callees": []
},
"myStatic": {
"Source": [
" @staticmethod",
" def myStatic():",
" return \"I am static\""
],
"Args": [],
"Callees": []
},
"displayHello": {
"Source": [
" def displayHello(self):",
" return \"Hello \" + self.myName"
],
"Args": [
"self"
],
"Callees": []
},
"__init__": {
"Source": [
" def __init__(self):",
" self.myName = 'Wonder?'"
],
"Args": [
"self"
],
"Callees": []
},
"main": {
"Source": [
"def main():",
" hello = Hello(\"Wonderful!!!\")",
" # name = unicode(raw_input(\"Enter name: \"), 'utf8')",
" # print(\"User specified:\", name)",
" print(hello.displayGreeting())"
],
"Args": [],
"Callees": []
}
}
A possible workaround is to monkeypatch the __import__
function with a custom function that never throws an ImportError and returns a dummy module instead:
import builtins
def force_import(module):
original_import = __import__
def fake_import(*args):
try:
return original_import(*args)
except ImportError:
return builtins
builtins.__import__ = fake_import
module = original_import(module)
builtins.__import__ = original_import
return module
This would allow you to import myModule
even if its dependencies cannot be imported. Then you can use inspect.getmembers
as you normally would:
myModule = force_import('myModule')
members = inspect.getmembers(myModule, inspect.isfunction)
A problem with this solution is that it only works around the failing imports. If myModule
tries to access any members of the imported modules, its import will fail:
# myModule.py
import this_module_doesnt_exist # works
print(this_module_doesnt_exist.variable) # fails
force_import('myModule')
# AttributeError: module 'builtins' has no attribute 'variable'
In order to work around this, you can create a dummy class that never throws an AttributeError:
class DummyValue:
def __call__(self, *args, **kwargs):
return self
__getitem__ = __setitem__ = __delitem__ = __call__
__len__ = __length_hint__ = __bool__ = __call__
__iter__ = __next__ = __call__
__getattribute__ = __call__
__enter__ = __leave__ = __call__
__str__ = __repr__ = __format__ = __bytes__ = __call__
# etc
(See the data model documentation for a list of dunder methods you may have to implement.)
Now if force_import
returns an instance of this class (change return builtins
to return DummyValue()
), importing myModule
will succeed.