查找python包中某些方法和函数的所有用法

x4shl7ld  于 2022-11-19  发布在  Python
关注(0)|答案(1)|浏览(139)

给定一个包含特定模块的python包,我想找到包中定义的方法和函数的所有用法,我想用pycharms find usages这样的东西,在给定的函数或方法中,它会显示调用此方法/函数的所有行。
假设我的包有很多模块,我想查找module_x中定义的函数和方法的用法。

import inspect

callables = [method_name for method_name in dir(module)
             if callable(getattr(module, method_name))]

module_inspected = inspect.getmodule(module)
module_file = module_inspected.__file__

module_x_callables = []

for name, member in inspect.getmembers(module):
    # to see if the definitions are defined/imported in the member_file that we are looking    
    if name in callables: 
        module_x_callables.append(member)
        member_file = inspect.getmodule(member).__file__
        # print('{}: {},{}'.format(name, member, callable(member)))
        print('{}'.format(name))
        print('{}'.format(member))
        #        print('parent: {}'.format(inspect.getmodule(member)))
        print('member_file: {}'.format(member_file))
        if member_file == module_file:
            source, line_no = inspect.findsource(member)
            print(line_no)
        print('\n')

注意:这种方法不会捕获类中的方法,但没关系,假设我想找到module_x中定义的函数的所有用法。
我的问题是:我如何扫描包中的其他模块,看看它们是否使用了module_x中的任何def,如果使用了,请返回行号。
我试着使用ast,遍历树并试图找到所有的ast.Call。这实际上返回了我所有的调用,但我不知道如何检查是否返回了所有在module_x中定义的结果。甚至,我想使用regex,但例如,在两个不同的模块中可能有两个名为test_func的函数。使用这种方法,我怎么知道我在打哪个电话?

string_code = open(file,'r').read()
tree = ast.parse(string_code)
for node in ast.walk(tree):
    #print(node)
    if isinstance(node, ast.Call):
        print('call')
        print(ast.dump(node))
        print(inspect.getmodule(node))
        print(func.value)
        print(func.attr)
        print('\n')

所以,总而言之,我的问题是:我怎样才能浏览一个文件或一个模块,并找到module_x中定义的函数和方法的所有用法和行号。谢谢;)

ffscu2ro

ffscu2ro1#

您只需要关心实际导入到您当前正在检查的模块中的名称。请注意,这里有一些复杂的情况:

  • 导入的名称可以从其他模块导入到当前模块; bar模块中的import foo使得bar.foo从外部可用。所以from bar import foo实际上和import foo是一样的。
  • 任何对象都可以存储在列表、元组中,成为另一个对象的属性,存储在字典中,分配给一个替代名称,并且可以动态引用。例如,存储在列表中的导入属性,通过索引引用:
import foo
spam = [foo.bar]
spam[0]()

调用foo.bar对象。可以通过AST分析跟踪其中的一些使用,但Python是一种高度动态的语言,很快就会遇到限制。例如,你无法确定spam[0] = random.choice([foo.bar, foo.baz])将生成什么。

  • 通过使用globalnonlocal语句,嵌套函数作用域可以改变父作用域中的名称。
def bar():
    global foo
    import foo

将导入模块foo并将其添加到全局名称空间,但仅在调用bar()时。跟踪这一点很困难,因为您需要跟踪实际调用bar()的时间。这甚至可能发生在当前模块(import weirdmodule; weirdmodule.bar())之外。
如果忽略这些复杂性,只关注import语句中使用的名称,那么需要跟踪ImportImportFrom节点,并跟踪作用域(这样就可以知道本地名称是否屏蔽了全局名称,或者导入的名称是否导入到了本地作用域),然后查找引用导入名称的Name(..., Load)节点。
我之前已经讨论过跟踪作用域,请参阅从Python AST中获取对应于给定名称的特定变量的所有节点。对于这个操作,我们可以将其简化为一个字典堆栈(封装在一个collections.ChainMap()示例中),并添加导入:

import ast
from collections import ChainMap
from types import MappingProxyType as readonlydict

class ModuleUseCollector(ast.NodeVisitor):
    def __init__(self, modulename, package=''):
        self.modulename = modulename
        # used to resolve from ... import ... references
        self.package = package
        self.modulepackage, _, self.modulestem = modulename.rpartition('.')
        # track scope namespaces, with a mapping of imported names (bound name to original)
        # If a name references None it is used for a different purpose in that scope
        # and so masks a name in the global namespace.
        self.scopes = ChainMap()
        self.used_at = []  # list of (name, alias, line) entries

    def visit_FunctionDef(self, node):
        self.scopes = self.scopes.new_child()
        self.generic_visit(node)
        self.scopes = self.scopes.parents

    def visit_Lambda(self, node):
        # lambdas are just functions, albeit with no statements
        self.visit_Function(node)

    def visit_ClassDef(self, node):
        # class scope is a special local scope that is re-purposed to form
        # the class attributes. By using a read-only dict proxy here this code
        # we can expect an exception when a class body contains an import 
        # statement or uses names that'd mask an imported name.
        self.scopes = self.scopes.new_child(readonlydict({}))
        self.generic_visit(node)
        self.scopes = self.scopes.parents

    def visit_Import(self, node):
        self.scopes.update({
            a.asname or a.name: a.name
            for a in node.names
            if a.name == self.modulename
        })

    def visit_ImportFrom(self, node):
        # resolve relative imports; from . import <name>, from ..<name> import <name>
        source = node.module  # can be None
        if node.level:
            package = self.package
            if node.level > 1:
                # go up levels as needed
                package = '.'.join(self.package.split('.')[:-(node.level - 1)])
            source = f'{package}.{source}' if source else package
        if self.modulename == source:
            # names imported from our target module
            self.scopes.update({
                a.asname or a.name: f'{self.modulename}.{a.name}'
                for a in node.names
            })
        elif self.modulepackage and self.modulepackage == source:
            # from package import module import, where package.module is what we want
            self.scopes.update({
                a.asname or a.name: self.modulename
                for a in node.names
                if a.name == self.modulestem
            })

    def visit_Name(self, node):
        if not isinstance(node.ctx, ast.Load):
            # store or del operation, means the name is masked in the current scope
            try:
                self.scopes[node.id] = None
            except TypeError:
                # class scope, which we made read-only. These names can't mask
                # anything so just ignore these.
                pass
            return
        # find scope this name was defined in, starting at the current scope
        imported_name = self.scopes.get(node.id)
        if imported_name is None:
            return
        self.used_at.append((imported_name, node.id, node.lineno))

现在,给定一个模块名称foo.barfoo包中的一个模块的以下源代码文件:

from .bar import name1 as namealias1
from foo import bar as modalias1

def loremipsum(dolor):
    return namealias1(dolor)

def sitamet():
    from foo.bar import consectetur

    modalias1 = 'something else'
    consectetur(modalias1)

class Adipiscing:
    def elit_nam(self):
        return modalias1.name2(self)

您可以解析上面的代码,并使用以下代码提取所有foo.bar引用:

>>> collector = ModuleUseCollector('foo.bar', 'foo')
>>> collector.visit(ast.parse(source))
>>> for name, alias, line in collector.used_at:
...     print(f'{name} ({alias}) used on line {line}')
...
foo.bar.name1 (namealias1) used on line 5
foo.bar.consectetur (consectetur) used on line 11
foo.bar (modalias1) used on line 15

请注意,sitamet作用域中的modalias1名称不会被视为对导入模块的实际引用,因为它被用作本地名称。

相关问题