найти все применения определенных методов и функций в пакете Python - PullRequest
0 голосов
/ 24 апреля 2019

Учитывая пакет python, который имеет определенные модули, я хочу найти все применения методов и функций, определенных в пакете, я думаю, что-то вроде pycharms найти использования , в котором дана функция или метод, который показывает вам все строки, в которых был вызван этот метод / функция.

Пусть в моем пакете много модулей, и я хочу посмотреть на использование функций и методов, определенных в module_x. Используя inspect и dir, я могу найти все вызовы, определенные в 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, и, если они есть, вернуть мне номера строк.

Я пытался использовать ast, прогуливаясь по дереву и пытаясь найти все ast.Call. Это фактически повторяет мне все вызовы, но я не знаю, как проверить, определены ли эти возвраты в module_x. Более того, я думал о чем-то, используя регулярные выражения, но, например, могут быть функции с именем 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. Спасибо;)

1 Ответ

1 голос
/ 24 апреля 2019

Вам нужно заботиться только об именах, которые были фактически импортированы в модуль, который вы сейчас проверяете.Обратите внимание, что здесь есть несколько сложностей:

  • Импортированные имена доступны из других модулей для импорта из текущего модуля;import foo в модуле bar делает доступным 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]) произведет с какой-либо определенностью.

  • При использовании операторов global и nonlocal области вложенных функций могут изменятьимена в родительских областях.Таким образом, надуманная функция, такая как:

    def bar():
        global foo
        import foo
    

    , импортирует модуль foo и добавляет его в глобальное пространство имен, но только при вызове bar().Отслеживать это сложно, так как вам нужно отслеживать, когда на самом деле вызывается bar().Это может произойти даже за пределами текущего модуля (import weirdmodule; weirdmodule.bar()).

Если вы проигнорируете эти сложности и сосредоточитесь только на использовании имен, используемых в операторах import, тогдавам нужно отслеживать узлы Import и ImportFrom и области видимости (чтобы вы знали, маскирует ли локальное имя глобальное или импортированное имя было импортировано в локальную область).Затем вы ищете 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, must 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и следующий файл исходного кода из модуля в пакете 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)

, вы можете проанализировать вышеизложенное и извлечь все ссылки 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

Обратите внимание, чтоимя modalias1 в области действия sitamet не рассматривается как фактическая ссылка на импортированный модуль, поскольку вместо этого оно используется как локальное имя.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...