Вам нужно заботиться только об именах, которые были фактически импортированы в модуль, который вы сейчас проверяете.Обратите внимание, что здесь есть несколько сложностей:
- Импортированные имена доступны из других модулей для импорта из текущего модуля;
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
не рассматривается как фактическая ссылка на импортированный модуль, поскольку вместо этого оно используется как локальное имя.