Получите детали исходного скрипта, аналогично inspect.getmembers(), без импорта скрипта

Я пытаюсь получить источник, список вызываемых, значения по умолчанию, ключевые слова, аргументы и переменные функций в скрипте Python.

В настоящее время я импортирую модуль и использую питон inspect модуля getmembers функция и прохождение isfunction параметр вроде так:

members = inspect.getmembers(myModule, inspect.isfunction)

Однако этот метод не работает, если myModuleимпорт не доступен для меня (так как myModule должен быть импортирован первым).

Я пробовал использовать питон ast модуль для parse а также dump синтаксическое дерево, но получение источника функции включало очень хакерские методы и / или сомнительные и далекие от поддерживаемых сторонних библиотек. Я полагаю, что я тщательно изучил документацию и стекопоток и не смог найти подходящего решения. Я что-то пропустил?

2 ответа

Решение

Поэтому я еще немного огляделся и быстро нашел решение Франкенштейна, используя ответ этого чувака, чтобы получить источник каждой функции. Это еще далеко не идеально, но вот если вы заинтересованы:

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))

Выход:

{
    "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": []
    }
}

Возможный обходной путь состоит в том, чтобы обезьяна __import__ функция с пользовательской функцией, которая никогда не выдает ImportError и возвращает вместо этого фиктивный модуль:

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

Это позволит вам импортировать myModule даже если его зависимости не могут быть импортированы. Тогда вы можете использовать inspect.getmembers как обычно:

myModule = force_import('myModule')
members = inspect.getmembers(myModule, inspect.isfunction)

Проблема с этим решением заключается в том, что оно работает только при сбое импорта. Если myModule пытается получить доступ к любым членам импортированных модулей, его импорт завершится неудачно:

# 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'

Чтобы обойти это, вы можете создать фиктивный класс, который никогда не выбрасывает 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

(См. Документацию модели данных для списка более сложных методов, которые вам, возможно, придется реализовать.)

Сейчас если force_import возвращает экземпляр этого класса (изменить return builtins в return DummyValue()), импорт myModule преуспеет.

Другие вопросы по тегам