Как найти вызовы метода, используя интроспекцию Python? - PullRequest
2 голосов
/ 27 марта 2019

Я только что нашел несколько тестовых методов в проекте, у которых не было требуемого префикса "test_", чтобы гарантировать, что они действительно выполняются. Должно быть возможно избежать этого с небольшой задержкой:

  1. Найти все TestCase вызовы утверждений в базе кода.
  2. Найдите метод с именем, начинающимся с «test_» в иерархии вызовов.
  3. Если такого метода нет, выведите сообщение об ошибке.

Мне интересно, как сделать первые два, которые в основном сводятся к одной проблеме: как мне найти все вызовы определенного метода в моей базе кода?

Гриппинг или другие текстовые поиски не подойдут, потому что мне нужно проанализировать результаты и найти родительские методы и т. Д., Пока я не доберусь до метода тестирования или не останется больше вызывающих. Мне нужно получить ссылку на метод, чтобы избежать сопоставления методов, имена которых совпадают с теми, которые я ищу.

Ответы [ 2 ]

1 голос
/ 27 марта 2019

Здесь возможны 2 подхода.

  1. Статический подход:

    Вы можете проанализировать кодовую базу, используя модуль ast, чтобы идентифицировать все вызовы функций и последовательно сохранять источник и цель вызова. Вам нужно будет идентифицировать все классы и определения функций, чтобы отслеживать текущий контекст каждого вызова. Ограничение здесь заключается в том, что если вы вызываете методы экземпляра, то не существует простого способа определить, к какому классу на самом деле принадлежит метод. То же самое, если вы используете переменные, которые ссылаются на модули

    Вот подкласс Visitor, который может читать исходные файлы Python и создавать dict {caller: callee}:

    class CallMapper(ast.NodeVisitor):
        def __init__(self):
            self.ctx = []
            self.funcs = []
            self.calls = collections.defaultdict(set)
        def process(self, filename):
            self.ctx = [('M', os.path.basename(filename)[:-3])]
            tree = ast.parse(open(filename).read(), filename)
            self.visit(tree)
            self.ctx.pop()
        def visit_ClassDef(self, node):
            print('ClassDef', node.name, node.lineno, self.ctx)
            self.ctx.append(('C', node.name))
            self.generic_visit(node)
            self.ctx.pop()
        def visit_FunctionDef(self, node):
            print('FunctionDef', node.name, node.lineno, self.ctx)
            self.ctx.append(('F', node.name))
            self.funcs.append('.'.join([elt[1] for elt in self.ctx]))
            self.generic_visit(node)
            self.ctx.pop()
        def visit_Call(self, node):
            print('Call', vars(node.func), node.lineno, self.ctx)
            try:
                id = node.func.id
            except AttributeError:
                id = '*.' + node.func.attr
            self.calls['.'.join([elt[1] for elt in self.ctx])].add(id)
            self.generic_visit(node)
    
  2. Динамический подход:

    Если вы действительно хотите определить, какой метод вызывается, когда более одного человека могут иметь одно и то же имя, вам придется использовать динамический подход. Вы должны декорировать отдельные функции или все методы из класса, чтобы подсчитать, сколько раз они были вызваны, и, необязательно, откуда они были вызваны. Затем вы начинаете тестирование и изучаете, что на самом деле произошло.

    Вот функция, которая будет декорировать все методы из класса, так что номер всех вызовов будет сохранен в словаре:

    def tracemethods(cls, track):
        def tracker(func, track):
            def inner(*args, **kwargs):
                if func.__qualname__ in track:
                    track[func.__qualname__] += 1
                else:
                    track[func.__qualname__] = 1
                return func(*args, *kwargs)
            inner.__doc__ = func.__doc__
            inner.__signature__ = inspect.signature(func)
            return inner
        for name, func in inspect.getmembers(cls, inspect.isfunction):
            setattr(cls, name, tracker(func, track))
    

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

0 голосов
/ 27 марта 2019

Ну, вот и начало.Вы будете использовать несколько стандартных библиотек:

import dis
import inspect

Предположим, вы заинтересованы в следующем исходном коде: myfolder/myfile.py Затем сделайте следующее:

import myfolder.myfile

def some_func():
    ''

loads = {'LOAD_GLOBAL', 'LOAD_ATTR'}
name_to_member = dict(inspect.getmembers(myfolder.myfile))
for name, member in name_to_member.items():
    if type(member) == type(some_func):
        print(name)
        for ins in dis.get_instructions(member):
            if ins.opname in loads:
                print(name, ins.opname, ins.argval)

Другие интересные вещи:запустите dis.dis(member) или распечатайте dis.code_info(member).

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

...