【问题标题】:How to find calls to a method using Python introspection?如何使用 Python 内省查找对方法的调用?
【发布时间】:2019-03-27 01:53:16
【问题描述】:

我刚刚在一个项目中发现了一些测试方法,它们没有所需的“test_”前缀来确保它们实际运行。应该可以通过一些 linting 来避免这种情况:

  1. 在代码库中查找所有TestCase 断言调用。
  2. 在调用层次结构中查找名称以“test_”开头的方法。
  3. 如果没有这样的方法,打印错误信息。

我想知道如何做前两个,这基本上归结为一个问题:我如何在我的代码库中找到对特定方法的所有调用?

Grepping 或其他文本搜索不会做,因为我需要反省结果并找到父方法等,直到我到达测试方法或没有更多的调用者。我需要获得该方法的引用,以避免匹配恰好与我正在寻找的方法同名的方法。

【问题讨论】:

  • 对于所有与 python 相关的问题,请始终使用通用 [python] 标签。自行决定使用特定于版本的标签。

标签: python python-3.x introspection


【解决方案1】:

这里有两种可能的方法。

  1. 静态方法:

    您可以使用 ast 模块解析代码库以识别所有函数调用并一致地存储调用的来源和目标。您必须识别所有类和函数定义以跟踪每个调用的当前上下文。这里的限制是,如果您调用实例方法,则没有简单的方法可以识别该方法实际属于哪个类。如果您使用引用模块的变量,则相同

    这是一个访问者子类,可以读取 Python 源文件并构建一个 dict {caller: 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)
    
  2. 动态方法:

    如果您真的想确定调用了什么方法,当多个方法可以共享相同的名称时,您将不得不使用动态方法。您将装饰一个类中的单个函数或所有方法,以计算它们被调用的次数,以及它们被调用的位置。然后您将开始测试并检查实际发生的情况。

    这是一个函数,它将修饰一个类中的所有方法,以便所有调用的数字都存储在一个字典中:

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

    您可以调整该代码以浏览解释器堆栈以识别每个调用的调用者,但这并不容易,因为您获得调用者函数的非限定名称,并且必须使用文件名和行号来唯一识别调用者。

【讨论】:

  • 我正在尝试进行静态分析,所以我需要确定是否在没有运行代码的情况下调用了断言方法。但第二种方法看起来确实很有趣。
【解决方案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)

这将让您访问文件中定义的每个函数, 并访问每个可执行语句以查看它是否可能是您关心的方法调用。 然后由您决定使用潜在的测试方法做正确的事。

【讨论】:

  • 这似乎给了我Instruction 包含字符串的对象, 但我真的不想依赖字符串匹配方法。这些库是否能够为我提供被调用方法的reference
  • 嗯,这确实是python源码拥有的唯一一种方法描述。当然,在调用getmembers 之前将所有导入设置为与运行时相同会带来不便,我会同意的。但是一旦到位,您就可以完全按照解释器在运行时执行的操作,在 globals() 中查找 LOAD_GLOBAL 字符串或在 locals() 中查找 LOAD_ATTR。看看例如dir(globals()['__builtins__'])。持有这样的参考,您可以查询__file____name__ 等。有关详细信息,请参阅 Allison Kaptur 令人惊叹的纯 Python 文章和 Byterun 的实现。
猜你喜欢
  • 1970-01-01
  • 2013-04-26
  • 1970-01-01
  • 2012-02-05
  • 1970-01-01
  • 2014-06-04
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多