简体   繁体   English

在python包中找到某些方法和功能的所有用法

[英]find all usages of certain methods and functions in a python package

Given a package of python that have certain modules, I want to find all the usages of the methods and functions defined in the package, I am thinking in something like pycharms find usages in which given a function or method it shows you all the lines in which this method/function was called. 给定一个具有某些模块的python软件包,我想找到该软件包中定义的方法和函数的所有用法,我在考虑pycharms之类的用法,在给定的函数或方法中它显示了所有内容。调用此方法/函数的方式。

Let stay my package have a lot of modules and I want to look for the the usages of the functions and methods defined in module_x . 让我的包中有很多模块,我想看看module_x定义的函数和方法的用法。 Using inspect and dir I can find all the callables defined in module_x 使用inspectdir我可以找到在module_x定义的所有可调用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')

Note: I this that methods inside classes will not be captured by this approach, but never mind. 注意:我这样说,类内的方法不会被这种方法捕获,但是没关系。 Lets say that I want to find all the usages of functions defined in module_x . 可以说我想查找module_x定义的函数的所有用法。

My question is: how can I scan the other modules in the package and see if they are using any of the defs in module_x , and if they are, return me the line numbers. 我的问题是:如何扫描软件包中的其他模块,查看它们是否正在使用module_x任何module_x ,如果是,请向我返回行号。

I tried to use ast , walking the tree and trying to find all the ast.Call . 我试图使用ast ,走到树上,并试图找到所有的ast.Call This actually retruns me all the calls, but I don't know how to check if this returns are defined in module_x . 实际上,这会重新运行所有调用,但是我不知道如何检查是否在module_x中定义了此返回。 Even more, I was thinking in something using regex but for example there could be to functions called test_func in two different modules. 甚至,我还在考虑使用正则表达式,但是例如可以在两个不同的模块中使用名为test_func函数。 Using this approach, how do I know which one I am calling? 使用这种方法,我怎么知道我要呼叫哪一个?

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

So, to end, my question is: how can I explore a file or a module and find all the usages and the line number of functions and methods defined in module_x . 因此,最后,我的问题是:如何浏览文件或模块并找到module_x定义的所有用法以及函数和方法的行数。 Thank you ;) 谢谢 ;)

You only need to care about names that were actually imported into the module you are currently inspecting. 您只需要关心实际导入到当前正在检查的模块中的名称。 Note that there are few complications here: 请注意,这里的并发症很少:

  • Imported names are available from other modules to import from the current module; 可以从其他模块导入名称,以从当前模块导入。 import foo in the module bar makes bar.foo available from the outside. 模块bar import foo使bar.foo可以从外部使用。 So from bar import foo is really the same thing as import foo . 因此, from bar import foo实际上与import foo相同。
  • Any object can be stored in a list, a tuple, become an attribute on another object, be stored in a dictionary, assigned to an alternative name, and can be referenced dynamically. 任何对象都可以存储在列表,元组中,成为另一个对象的属性,存储在字典中,分配给备用名称,并且可以动态引用。 Eg an imported attribute stored in a list, referenced by index: 例如,存储在列表中的导入属性,由索引引用:

     import foo spam = [foo.bar] spam[0]() 

    calls the foo.bar object. 调用foo.bar对象。 Tracking some of these uses through AST analysis can be done, but Python is a highly dynamic language and you'll soon run into limitations. 可以通过AST分析来跟踪其中的某些用途,但是Python是一种高度动态的语言,您很快就会遇到限制。 You can't know what spam[0] = random.choice([foo.bar, foo.baz]) will produce with any certainty, for example. 例如,您不知道spam[0] = random.choice([foo.bar, foo.baz])将产生什么。

  • With the use of the global and nonlocal statements, nested function scopes can alter the names in parent scopes. 通过使用globalnonlocal语句,嵌套函数作用域可以更改父作用域中的名称。 So a contrived function like: 因此,人为的功能如下:

     def bar(): global foo import foo 

    would import the module foo and add it to the global namespace, but only when bar() is called. 会导入模块foo并将其添加到全局名称空间,但仅在调用bar()时才如此。 Tracking this is difficult, as you need to track when bar() is actually called. 跟踪此操作很困难,因为您需要跟踪实际调用bar()时间。 This could even happen outside of the current module ( import weirdmodule; weirdmodule.bar() ). 这甚至可能发生在当前模块之外( import weirdmodule; weirdmodule.bar() )。

If you ignore those complications, and focus only on the use of the names used in import statements, then you need to track Import and ImportFrom nodes, and track scopes (so you know if a local name masks a global, or if an imported name was imported into a local scope). 如果您忽略了这些复杂性,仅关注于import语句中使用的名称,则需要跟踪ImportImportFrom节点,并跟踪作用域(这样您就可以知道本地名称是否掩盖了全局名称,或者是否知道了导入名称已导入本地范围)。 You then look for Name(..., Load) nodes that reference the imported names. 然后,您查找引用导入名称的Name(..., Load)节点。

I've covered tracking scopes before, see Getting all the nodes from Python AST that correspond to a particular variable with a given name . 我之前已经介绍了跟踪范围,请参阅从Python AST获取所有与给定名称的特定变量对应的节点 For this operation we can simplify this to a stack of dictionaries (encapsulated in a collections.ChainMap() instance ), and add imports: 对于此操作,我们可以将其简化为一堆字典(封装在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))

Now, given a module name foo.bar and the following source code file from a module in the foo package: 现在,给定模块名称foo.bar并从foo包中的模块获取以下源代码文件:

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)

you can parse the above and extract all foo.bar references with: 您可以解析以上内容,并使用以下命令提取所有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

Note that the modalias1 name in the sitamet scope is not seen as an actual reference to the imported module, as it is being used as a local name instead. 请注意, sitamet范围中的modalias1名称不会被视为对导入模块的实际引用,因为它已被用作本地名称。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM