Это немного сложнее, чем вы думаете. from foo import name
является допустимым способом импорта как объекта, определенного в модуле foo
, так и модуля foo.name
, поэтому вам, возможно, придется попробовать обе формы , чтобы увидеть, разрешают ли они файл. Python также допускает псевдонимы, в которые код может импортировать foo.bar
, но фактический модуль действительно определен как foo._bar_implementation
и сделан доступным как атрибут пакета foo
. Вы не можете обнаружить все эти случаи чисто, посмотрев на узлы Import
и ImportFrom
.
Если вы игнорируете эти случаи и смотрите только на имя from
, вам все равно придется преобразовать имя модуля в имя файла, а затем анализировать источник из файла для каждого импорта.
В Python 2 вы можете использовать imp.find_module
, чтобы получить объект открытого файла для модуля (*) . При синтаксическом анализе каждого модуля вы хотите сохранить полное имя модуля, потому что оно понадобится вам для того, чтобы в дальнейшем можно было определить относительный импорт пакетов. imp.find_module()
не может обработать импорт пакетов, поэтому я создал функцию-оболочку:
import imp
_package_paths = {}
def find_module(module):
# imp.find_module can't handle package paths, so we need to do this ourselves
# returns an open file object, the filename, and a flag indicating if this
# is a package directory with __init__.py file.
path = None
if '.' in module:
# resolve the package path first
parts = module.split('.')
module = parts.pop()
for i, part in enumerate(parts, 1):
name = '.'.join(parts[:i])
if name in _package_paths:
path = [_package_paths[name]]
else:
_, filename, (_, _, type_) = imp.find_module(part, path)
if type_ is not imp.PKG_DIRECTORY:
# no Python source code for this package, abort search
return None, None
_package_paths[name] = filename
path = [filename]
source, filename, (_, _, type_) = imp.find_module(module, path)
is_package = False
if type_ is imp.PKG_DIRECTORY:
# load __init__ file in package
source, filename, (_, _, type_) = imp.find_module('__init__', [filename])
is_package = True
if type_ is not imp.PY_SOURCE:
return None, None, False
return source, filename, is_package
Я бы также отслеживал, какие имена модулей вы уже импортировали, чтобы вы не обрабатывали их дважды; используйте имя из объекта spec
, чтобы убедиться, что вы отслеживаете его канонические имена.
Используйте стек для обработки всех модулей:
with open(path_to_config) as config_file:
# stack consists of (modulename, ast) tuples
stack = [('', ast.parse(config_file.read()))]
seen = {}
while stack:
modulename, ast_tree = stack.pop()
for script_object in ast_tree.body:
if isinstance(script_object, (ast.Import, ast.ImportFrom)):
names = [a.name for a in script_object.names]
from_names = []
if hasattr(script_object, 'level'): # ImportFrom
from_names = names
name = script_object.module
if script_object.level:
package = modulename.rsplit('.', script_object.level - 1)[0]
if script_object.module:
name = "{}.{}".format(name, script_object.module)
else:
name = package
names = [name]
for name in names:
if name in seen:
continue
seen.add(name)
source, filename, is_package = find_module(name)
if source is None:
continue
if is_package and from_names:
# importing from a package, assume the imported names
# are modules
names += ('{}.{}'.format(name, fn) for fn in from_names)
continue
with source:
module_ast = ast.parse(source.read(), filename)
queue.append((name, module_ast))
elif not _is_admissible_node(script_object):
raise Exception("Config file '%s' contains unacceptable statements" % path_to_config)
В случае импорта from foo import bar
, если foo
- пакет, foo/__init__.py
пропускается, и предполагается, что bar
будет модулем.
(*) imp.find_module()
устарело для кода Python 3. В Python 3 вы должны использовать importlib.util.find_spec()
для получения спецификации загрузчика модулей, а затем использовать атрибут ModuleSpec.origin
для получения имени файла. importlib.util.find_spec()
знает, как обращаться с пакетами.