web-dev-qa-db-fra.com

Comment savoir si (le code source de) une fonction contient un appel à une méthode depuis un module spécifique?

Disons que j'ai un tas de fonctions a, b, c, d et e et je veux savoir si elles appellent une méthode du module 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)

Je veux écrire une fonction uses_module afin que je puisse m'attendre à ce que ces assertions passent:

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) est False car random est uniquement importé mais jamais l'une de ses méthodes n'est appelée.)

Je ne peux pas modifier a, b, c, d et e. J'ai donc pensé qu'il serait peut-être possible d'utiliser ast pour cela et de suivre le code de la fonction que je reçois de inspect.getsource. Mais je suis ouvert à toute autre proposition, ce n'était qu'une idée de la façon dont cela pourrait fonctionner.

C'est pour autant que je suis venu avec ast:

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

Vous pouvez remplacer le module random par un objet fictif, en fournissant un accès aux attributs personnalisé et en interceptant ainsi les appels de fonction. Chaque fois que l'une des fonctions essaie d'importer (depuis) ​​random, elle accédera réellement à l'objet fictif. L'objet fictif peut également être conçu en tant que gestionnaire de contexte, en rendant le module random d'origine après le test.

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

Nom du module variable

Une manière plus flexible, en spécifiant le nom du module, est obtenue par:

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
1
a_guest

Ceci est un travail en cours, mais peut-être que cela suscitera une meilleure idée. J'utilise les types de nœuds du AST pour tenter d'affirmer qu'un module est importé et qu'une fonction qu'il fournit est utilisée. 

J'ai ajouté ce qui peut être nécessaire pour déterminer qu'il en est ainsi dans un checker defaultdict pouvant être évalué pour certaines conditions, mais je n'utilise pas toutes les paires clé-valeur pour établir une assertion pour vos cas d'utilisation.

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
2
Wes Doyle

Vous pouvez simplement placer un modèle random.py dans votre répertoire local (test) contenant le code suivant:

# >= 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()

Ensuite, vous testez simplement vos fonctions comme suit:

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

Cela fonctionne car au lieu d’importer le module random intégré, il s’applique au module fictif qui émule attribut d’accès personnalisé et peut donc intercepter les appels de fonction.

Si vous ne souhaitez pas interrompre les fonctions en générant une exception, vous pouvez toujours utiliser la même approche en important le module random d'origine dans le module fictif (en modifiant sys.path de manière appropriée), puis en vous reportant aux fonctions d'origine.

1
a_guest