Python 3.x 在python包中查找某些方法和函数的所有用法

Python 3.x 在python包中查找某些方法和函数的所有用法,python-3.x,reference,Python 3.x,Reference,给定一个包含特定模块的python包,我想找到包中定义的方法和函数的所有用法,我想用类似pycharms的东西,其中给定一个函数或方法,它会显示调用该方法/函数的所有行 我的包有很多模块,我想了解module\ux中定义的函数和方法的用法。 使用inspect和dir我可以找到module_x 导入检查 callables=[目录(模块)中方法名称的方法名称] 如果可调用(getattr(模块,方法\名称))] 已检查的模块=检查。获取模块(模块) 模块\文件=模块\已检查。\文件__ 模块_x

给定一个包含特定模块的python包,我想找到包中定义的方法和函数的所有用法,我想用类似pycharms的东西,其中给定一个函数或方法,它会显示调用该方法/函数的所有行

我的包有很多模块,我想了解
module\ux
中定义的函数和方法的用法。 使用
inspect
dir
我可以找到
module_x

导入检查
callables=[目录(模块)中方法名称的方法名称]
如果可调用(getattr(模块,方法\名称))]
已检查的模块=检查。获取模块(模块)
模块\文件=模块\已检查。\文件__
模块_x_可调用项=[]
对于inspect.getmembers(模块)中的名称和成员:
#查看定义是否在我们正在查找的成员_文件中定义/导入
如果名称在可调用项中:
模块_x_可调用项.append(成员)
成员\文件=inspect.getmodule(成员)。\文件__
#打印({}:{},{})。格式(名称、成员、可调用(成员)))
打印(“{}”。格式(名称))
打印(“{}”。格式(成员))
#打印('parent:{}'。格式(inspect.getmodule(成员)))
打印('member_file:{}'。格式(member_file))
如果成员文件==模块文件:
source,line_no=inspect.findsource(成员)
打印(行号)
打印(“\n”)
注意:我认为类内的方法不会被这种方法捕获,但没关系。假设我想找到
module\u x
中定义的函数的所有用法

我的问题是:如何扫描软件包中的其他模块,查看它们是否使用了
模块x
中的任何DEF,如果使用了,请返回行号

我试图使用
ast
,在树上走着,试图找到所有
ast.Call
。这实际上会重新运行所有调用,但我不知道如何检查此返回是否在
module\u x
中定义。更重要的是,我在考虑使用正则表达式,但例如,在两个不同的模块中可能有名为
test\u func
的函数。使用这种方法,我如何知道我正在呼叫哪一个

string\u code=open(文件,'r').read()
tree=ast.parse(字符串\代码)
对于ast.walk(树)中的节点:
#打印(节点)
如果isinstance(节点,ast.Call):
打印('调用')
打印(ast.dump(节点))
打印(检查.getmodule(节点))
打印(功能值)
打印(功能属性)
打印(“\n”)
因此,最后,我的问题是:如何浏览一个文件或模块,找到
module\u x
中定义的函数和方法的所有用法和行号。
谢谢;)

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

  • 导入的名称可从其他模块导入,以从当前模块导入<在模块
    bar
    中的code>import foo使
    bar.foo
    从外部可用。因此,bar import foo的
    实际上与
    import foo的
    是一样的
  • 任何对象都可以存储在列表、元组中,成为另一个对象的属性,存储在字典中,分配给替代名称,并且可以动态引用。例如,存储在列表中的导入属性,由索引引用:

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

  • 通过使用
    global
    nonlocal
    语句,嵌套函数作用域可以更改父作用域中的名称。因此,一个人为的函数如下:

    def bar():
        global foo
        import foo
    
    将导入模块
    foo
    并将其添加到全局命名空间,但仅当调用
    bar()
    时。跟踪这一点很困难,因为您需要跟踪实际调用
    bar()
    的时间。这甚至可能发生在当前模块之外(
    import-weirdmodule;weirdmodule.bar()

如果忽略这些复杂情况,只关注
import
语句中使用的名称,则需要跟踪
import
ImportFrom
节点,并跟踪作用域(以便知道本地名称是否屏蔽了全局名称,或者导入的名称是否已导入本地作用域)。然后查找引用导入名称的
Name(…,Load)
节点

我以前讨论过跟踪范围,请参阅。对于此操作,我们可以将其简化为一堆字典(封装在中),并添加导入:

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.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
名称不被视为对导入模块的实际引用,因为它被用作本地名称。

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

  • 导入的名称可从其他模块导入,以从当前模块导入<在模块
    bar
    中的code>import foo
    使
    bar.foo
    从外部可用。因此,bar import foo的
    实际上与
    import foo的
    是一样的
  • 任何对象都可以存储在列表、元组中,成为另一个对象的属性,存储在字典中,分配给替代名称,并且可以动态引用。例如,存储在列表中的导入属性,由索引引用:

    import foo
    spam = [foo.bar]
    spam[0]()
    
    调用
    foo.bar
    对象。