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