Как узнать, содержит ли (исходный код) функцию вызов метода из определенного модуля? - PullRequest
0 голосов
/ 16 декабря 2018

Допустим, у меня есть набор функций a, b, c, d и e, и я хочу выяснить, вызывают ли они какой-либо метод из модуля random:

def a():
    pass

def b():
    import random

def c():
    import random
    random.randint(0, 1)

def d():
    import random as ra
    ra.randint(0, 1)

def e():
    from random import randint as ra
    ra(0, 1)

Я хочу написать функцию uses_module, поэтому могу ожидать, что эти утверждения пройдут:

assert uses_module(a) == False
assert uses_module(b) == False
assert uses_module(c) == True
assert uses_module(d) == True
assert uses_module(e) == True

(uses_module(b) равно False, поскольку random импортируется тольконо ни один из его методов не вызывается.)

Я не могу изменить a, b, c, d и e.Поэтому я подумал, что для этого можно использовать ast и пройтись по коду функции, который я получаю из inspect.getsource. Но я открыт для любых других предложений, это была только идея, как это могло бы работать.

Насколько я пришел с ast:

def uses_module(function):
    import ast
    import inspect
    nodes = ast.walk(ast.parse(inspect.getsource(function)))
    for node in nodes:
        print(node.__dict__)

Ответы [ 3 ]

0 голосов
/ 16 декабря 2018

Вы можете просто поместить макет random.py в свой локальный (тестовый) каталог, содержащий следующий код:

# >= Python 3.7.
def __getattr__(name):
    def mock(*args, **kwargs):
        raise RuntimeError(f'{name}: {args}, {kwargs}')  # For example.
    return mock


# <= Python 3.6.
class Wrapper:
    def __getattr__(self, name):
        def mock(*args, **kwargs):
            raise RuntimeError('{}: {}, {}'.format(name, args, kwargs))  # For example.
        return mock

import sys
sys.modules[__name__] = Wrapper()

Затем вы просто протестируете свои функции следующим образом:

def uses_module(func):
    try:
        func()
    except RuntimeError as err:
        print(err)
        return True
    return False

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

Если вы этого не сделаетеЕсли вы хотите прервать функции, вызвав исключение, вы все равно можете использовать тот же подход, импортировав оригинальный модуль random в модуль mock (соответственно изменив sys.path), а затем прибегнув к исходным функциям.

0 голосов
/ 16 декабря 2018

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

import sys


class Mock:
    import random
    random = random

    def __enter__(self):
        sys.modules['random'] = self
        self.method_called = False
        return self

    def __exit__(self, *args):
        sys.modules['random'] = self.random

    def __getattr__(self, name):
        def mock(*args, **kwargs):
            self.method_called = True
            return getattr(self.random, name)
        return mock


def uses_module(func):
    with Mock() as m:
        func()
        return m.method_called

имя модуля переменной

более гибкий способ,указание имени модуля, достигается:

import importlib
import sys


class Mock:
    def __init__(self, name):
        self.name = name
        self.module = importlib.import_module(name)

    def __enter__(self):
        sys.modules[self.name] = self
        self.method_called = False
        return self

    def __exit__(self, *args):
        sys.modules[self.name] = self.module

    def __getattr__(self, name):
        def mock(*args, **kwargs):
            self.method_called = True
            return getattr(self.module, name)
        return mock


def uses_module(func):
    with Mock('random') as m:
        func()
        return m.method_called
0 голосов
/ 16 декабря 2018

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

Я добавил, что может быть необходимым, чтобы определить, что это так, к checker defaultdict, который может быть оценен для некоторого набора условий, но я не использую все пары ключ-значение для установленияутверждение для ваших случаев использования.

def uses_module(function):
    """
    (WIP) assert that a function uses a module
    """
    import ast
    import inspect
    nodes = ast.walk(ast.parse(inspect.getsource(function)))
    checker = defaultdict(set)
    for node in nodes:
        if type(node) in [ast.alias, ast.Import, ast.Name, ast.Attribute]:
            nd = node.__dict__
            if type(node) == ast.alias:
                checker['alias'].add(nd.get('name'))
            if nd.get('name') and nd.get('asname'):
                checker['name'].add(nd.get('name'))
                checker['asname'].add(nd.get('asname'))
            if nd.get('ctx') and nd.get('attr'):
                checker['attr'].add(nd.get('attr'))
            if nd.get('id'):
                checker['id'].add(hex(id(nd.get('ctx'))))
            if nd.get('value') and nd.get('ctx'):
                checker['value'].add(hex(id(nd.get('ctx'))))

    # print(dict(checker)) for debug

    # This check passes your use cases, but probably needs to be expanded
    if checker.get('alias') and checker.get('id'):
        return True
    return False
...