Могу ли я обрабатывать импорт в абстрактном синтаксическом дереве? - PullRequest
0 голосов
/ 23 января 2019

Я хочу разобрать и проверить config.py на допустимые узлы. config.py может импортировать другие файлы конфигурации, которые также должны быть проверены.

Есть ли в модуле ast какая-либо функциональность для анализа ast.Import и ast.ImportFrom объектов на ast.Module объектов?

Вот пример кода, я проверяю файл конфигурации (path_to_config), но я также хочу проверить все файлы, которые он импортирует:

with open(path_to_config) as config_file:
    ast_tree = ast.parse(config_file.read())
    for script_object in ast_tree.body:
        if isinstance(script_object, ast.Import):
            # Imported file must be checked too
        elif isinstance(script_object, ast.ImportFrom):
            # Imported file must be checked too
        elif not _is_admissible_node(script_object):
            raise Exception("Config file '%s' contains unacceptable statements" % path_to_config)

1 Ответ

0 голосов
/ 29 января 2019

Это немного сложнее, чем вы думаете. 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() знает, как обращаться с пакетами.

...