#python #python-3.x #introspection
#python #python-3.x #самоанализ
Вопрос:
Я только что нашел несколько методов тестирования в проекте, у которых не было требуемого префикса «test_», чтобы убедиться, что они действительно выполняются. Должно быть возможно избежать этого с небольшим количеством линтинга:
- Найдите все
TestCase
вызовы утверждений в базе кода. - Найдите метод с именем, начинающимся с «test_» в иерархии вызовов.
- Если такого метода нет, выведите сообщение об ошибке.
Мне интересно, как выполнить первые два, которые в основном сводятся к одной проблеме: как мне найти все вызовы определенного метода в моей кодовой базе?
Grepping или другие текстовые запросы не подойдут, потому что мне нужно проанализировать результаты и найти родительские методы и т.д., Пока я либо не перейду к тестовому методу, либо больше не будет вызывающих. Мне нужно получить ссылку на метод, чтобы избежать совпадения методов, которые имеют то же имя, что и те, которые я ищу.
Комментарии:
1. Пожалуйста, всегда используйте общий тег [python] для всех вопросов, связанных с python. Используйте тег, зависящий от версии, по своему усмотрению.
Ответ №1:
Здесь возможны 2 подхода.
-
Статический подход:
Вы могли бы проанализировать базу кода с помощью модуля ast, чтобы идентифицировать все вызовы функций и последовательно сохранять источник и цель вызова. Вам пришлось бы идентифицировать все классы и определение функции, чтобы отслеживать текущий контекст каждого вызова. Ограничение здесь в том, что если вы вызываете методы экземпляра, нет простого способа определить, к какому классу на самом деле принадлежит метод. То же самое, если вы используете переменные, которые ссылаются на модули
Вот подкласс Visitor, который может читать исходные файлы Python и создавать dict {вызывающий: 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)
-
Динамический подход:
Если вы действительно хотите определить, какой метод вызывается, когда несколько методов могут иметь одно и то же имя, вам придется использовать динамический подход. Вы бы оформили отдельные функции или все методы из класса, чтобы подсчитать, сколько раз они вызывались, и, возможно, откуда они были вызваны. Затем вы должны запустить тесты и изучить, что на самом деле произошло.
Вот функция, которая оформит все методы из класса так, что количество всех вызовов будет сохранено в словаре:
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))
Вы могли бы настроить этот код, чтобы просматривать стек интерпретатора для идентификации вызывающей функции для каждого вызова, но это не очень просто, потому что вы получаете неквалифицированное имя вызывающей функции и должны будете использовать имя файла и номер строки, чтобы однозначно идентифицировать вызывающую функцию.
Комментарии:
1. Я пытаюсь выполнить статический анализ, поэтому мне нужно выяснить, вызывается ли метод утверждения без запуска кода. Но второй подход действительно выглядит интересным.
Ответ №2:
Ну, вот и начало. Вы будете использовать пару стандартных библиотек:
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)
.
Это позволит вам посетить каждую функцию, определенную в файле, и посетить каждый исполняемый оператор, чтобы увидеть, может ли это быть вызовом метода, который вас интересует. Тогда вам решать, как правильно поступить с потенциальными методами тестирования.
Комментарии:
1. Кажется, это дает мне
Instruction
объекты, содержащие строки, но я действительно не хочу полагаться на методы сопоставления строк. Способны ли эти библиотеки предоставить мне ссылку на вызываемый метод?2. Ну, на самом деле это единственное описание метода, имеющееся в исходном коде python. Конечно, есть неудобство настраивать весь импорт так же, как во время выполнения перед вызовом
getmembers
, я согласен с вами в этом. Но как только это будет сделано, вы сможете делать именно то, что делает интерпретатор во время выполнения, просматривая строку LOAD_GLOBAL в globals() или LOAD_ATTR в locals(). Взгляните, например,dir(globals()['__builtins__'])
. Имея такую ссылку, вы можете запрашивать__file__
,__name__
и подобные. Подробные сведения см. в потрясающей статье Эллисон Каптур о чистом python и реализации Byterun.3. Byterun находится в aosabook.org/en/500L /…