_includes.py 6.03 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11
import os
from . import _yaml
from ._exceptions import LoadError, LoadErrorReason


# Includes()
#
# This takes care of processing include directives "(@)".
#
# Args:
#    loader (Loader): The Loader object
12 13 14
#    copy_tree (bool): Whether to make a copy, of tree in
#                      provenance. Should be true if intended to be
#                      serialized.
15 16
class Includes:

17
    def __init__(self, loader, *, copy_tree=False):
18 19
        self._loader = loader
        self._loaded = {}
20
        self._copy_tree = copy_tree
21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37

    # process()
    #
    # Process recursively include directives in a YAML node.
    #
    # Args:
    #    node (dict): A YAML node
    #    included (set): Fail for recursion if trying to load any files in this set
    #    current_loader (Loader): Use alternative loader (for junction files)
    #    only_local (bool): Whether to ignore junction files
    def process(self, node, *,
                included=set(),
                current_loader=None,
                only_local=False):
        if current_loader is None:
            current_loader = self._loader

38 39 40 41 42 43 44 45
        includes = _yaml.node_get(node, None, '(@)', default_value=None)
        if isinstance(includes, str):
            includes = [includes]

        if not isinstance(includes, list) and includes is not None:
            provenance = _yaml.node_get_provenance(node, key='(@)')
            raise LoadError(LoadErrorReason.INVALID_DATA,
                            "{}: {} must either be list or str".format(provenance, includes))
46 47

        include_provenance = None
48
        if includes:
49
            include_provenance = _yaml.node_get_provenance(node, key='(@)')
50
            _yaml.node_del(node, '(@)')
51 52 53 54

            for include in reversed(includes):
                if only_local and ':' in include:
                    continue
55 56 57 58 59 60 61 62
                try:
                    include_node, file_path, sub_loader = self._include_file(include,
                                                                             current_loader)
                except LoadError as e:
                    if e.reason == LoadErrorReason.MISSING_FILE:
                        message = "{}: Include block references a file that could not be found: '{}'.".format(
                            include_provenance, include)
                        raise LoadError(LoadErrorReason.MISSING_FILE, message) from e
63 64 65 66
                    elif e.reason == LoadErrorReason.LOADING_DIRECTORY:
                        message = "{}: Include block references a directory instead of a file: '{}'.".format(
                            include_provenance, include)
                        raise LoadError(LoadErrorReason.LOADING_DIRECTORY, message) from e
67 68 69
                    else:
                        raise

70 71
                if file_path in included:
                    raise LoadError(LoadErrorReason.RECURSIVE_INCLUDE,
72
                                    "{}: trying to recursively include {}". format(include_provenance,
73 74 75 76
                                                                                   file_path))
                # Because the included node will be modified, we need
                # to copy it so that we do not modify the toplevel
                # node of the provenance.
77
                include_node = _yaml.node_copy(include_node)
78 79 80 81 82 83 84 85 86

                try:
                    included.add(file_path)
                    self.process(include_node, included=included,
                                 current_loader=sub_loader,
                                 only_local=only_local)
                finally:
                    included.remove(file_path)

87
                _yaml.composite_and_move(node, include_node)
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114

        for _, value in _yaml.node_items(node):
            self._process_value(value,
                                included=included,
                                current_loader=current_loader,
                                only_local=only_local)

    # _include_file()
    #
    # Load include YAML file from with a loader.
    #
    # Args:
    #    include (str): file path relative to loader's project directory.
    #                   Can be prefixed with junctio name.
    #    loader (Loader): Loader for the current project.
    def _include_file(self, include, loader):
        shortname = include
        if ':' in include:
            junction, include = include.split(':', 1)
            junction_loader = loader._get_loader(junction, fetch_subprojects=True)
            current_loader = junction_loader
        else:
            current_loader = loader
        project = current_loader.project
        directory = project.directory
        file_path = os.path.join(directory, include)
        key = (current_loader, file_path)
115
        if key not in self._loaded:
116
            self._loaded[key] = _yaml.load(file_path,
117
                                           shortname=shortname,
118 119
                                           project=project,
                                           copy_tree=self._copy_tree)
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134
        return self._loaded[key], file_path, current_loader

    # _process_value()
    #
    # Select processing for value that could be a list or a dictionary.
    #
    # Args:
    #    value: Value to process. Can be a list or a dictionary.
    #    included (set): Fail for recursion if trying to load any files in this set
    #    current_loader (Loader): Use alternative loader (for junction files)
    #    only_local (bool): Whether to ignore junction files
    def _process_value(self, value, *,
                       included=set(),
                       current_loader=None,
                       only_local=False):
135
        if _yaml.is_node(value):
136 137 138 139 140 141 142 143 144 145
            self.process(value,
                         included=included,
                         current_loader=current_loader,
                         only_local=only_local)
        elif isinstance(value, list):
            for v in value:
                self._process_value(v,
                                    included=included,
                                    current_loader=current_loader,
                                    only_local=only_local)