【问题标题】:How to find out if (the source code of) a function contains a call to a method from a specific module?如何找出函数(的源代码)是否包含对特定模块的方法的调用?
【发布时间】:2018-12-16 00:51:03
【问题描述】:

假设我有一堆函数abcde,我想知道它们是否调用了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 仅被导入,但从未调用其方法之一。)

我无法修改 abcde。所以我认为可以使用ast 并沿着我从@​​987654339@ 获得的函数代码进行操作。 但我对任何其他建议持开放态度,这只是一个想法。

据我所知ast

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

【问题讨论】:

  • 我想会有一条使用 mockassert_called 来模拟在 ast 类型中找到的函数的路径。会考虑的

标签: python abstract-syntax-tree static-analysis inspect


【解决方案1】:

这是一项正在进行的工作,但也许它会激发一个更好的想法。我正在使用 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

【讨论】:

    【解决方案2】:

    您可以用模拟对象替换random 模块,提供自定义属性访问,从而拦截函数调用。每当其中一个函数尝试导入(从)random 时,它实际上会访问模拟对象。 mock对象也可以设计成上下文管理器,在测试后交还原来的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
    

    【讨论】:

      【解决方案3】:

      您可以简单地将模拟 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 模块,而是用于模拟custom attribute access 的模拟模块,因此可以拦截函数调用。

      如果您不想通过引发异常来中断函数,您仍然可以使用相同的方法,通过在模拟模块中导入原始的 random 模块(适当地修改 sys.path)然后回退到原始功能。

      【讨论】:

        猜你喜欢
        • 2019-06-03
        • 2013-03-11
        • 2014-12-29
        • 1970-01-01
        • 2018-03-25
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多