Get source script details, similar to inspect.getm

2019-08-18 11:44发布

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?

2条回答
戒情不戒烟
2楼-- · 2019-08-18 12:40

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": []
    }
}
查看更多
祖国的老花朵
3楼-- · 2019-08-18 12:43

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.

查看更多
登录 后发表回答