# # Copyright (C) 2016 Codethink Limited # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public # License as published by the Free Software Foundation; either # version 2 of the License, or (at your option) any later version. # # This library is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU # Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public # License along with this library. If not, see . # # Authors: # Tristan Van Berkom import re from ._exceptions import LoadError, LoadErrorReason from . import _yaml # Variables are allowed to have dashes here # _VARIABLE_MATCH = r'\%\{([a-zA-Z][a-zA-Z0-9_-]*)\}' # The Variables helper object will resolve the variable references in # the given dictionary, expecting that any dictionary values which contain # variable references can be resolved from the same dictionary. # # Each Element creates its own Variables instance to track the configured # variable settings for the element. # # Args: # node (dict): A node loaded and composited with yaml tools # # Raises: # LoadError, if unresolved variables occur. # class Variables(): def __init__(self, node): self.original = node self.variables = self._resolve(node) # subst(): # # Substitutes any variables in 'string' and returns the result. # # Args: # (string): The string to substitute # # Returns: # (string): The new string with any substitutions made # # Raises: # LoadError, if the string contains unresolved variable references. # def subst(self, string): substitute, unmatched, _ = self._subst(string, self.variables) unmatched = list(set(unmatched)) if unmatched: if len(unmatched) == 1: message = "Unresolved variable '{var}'".format(var=unmatched[0]) else: message = "Unresolved variables: " for unmatch in unmatched: if unmatched.index(unmatch) > 0: message += ', ' message += unmatch raise LoadError(LoadErrorReason.UNRESOLVED_VARIABLE, message) return substitute def _subst(self, string, variables): def subst_callback(match): nonlocal variables nonlocal unmatched nonlocal matched token = match.group(0) varname = match.group(1) value = _yaml.node_get(variables, str, varname, default_value=None) if value is not None: # We have to check if the inner string has variables # and return unmatches for those unmatched += re.findall(_VARIABLE_MATCH, value) matched += [varname] else: # Return unmodified token unmatched += [varname] value = token return value matched = [] unmatched = [] replacement = re.sub(_VARIABLE_MATCH, subst_callback, string) return (replacement, unmatched, matched) # Variable resolving code # # Here we substitute variables for values (resolve variables) repeatedly # in a dictionary, each time creating a new dictionary until there is no # more unresolved variables to resolve, or, until resolving further no # longer resolves anything, in which case we throw an exception. def _resolve(self, node): variables = node # Special case, if notparallel is specified in the variables for this # element, then override max-jobs to be 1. # Initialize it as a string as all variables are processed as strings. # if _yaml.node_get(variables, bool, 'notparallel', default_value=False): variables['max-jobs'] = str(1) # Resolve the dictionary once, reporting the new dictionary with things # substituted in it, and reporting unmatched tokens. # def resolve_one(variables): unmatched = [] resolved = {} for key, value in _yaml.node_items(variables): # Ensure stringness of the value before substitution value = _yaml.node_get(variables, str, key) resolved_var, item_unmatched, matched = self._subst(value, variables) if _wrap_variable(key) in resolved_var: referenced_through = find_recursive_variable(key, matched, variables) raise LoadError(LoadErrorReason.RECURSIVE_VARIABLE, "{}: ".format(_yaml.node_get_provenance(variables, key)) + ("Variable '{}' expands to contain a reference to itself. " + "Perhaps '{}' contains '{}").format(key, referenced_through, _wrap_variable(key))) resolved[key] = resolved_var unmatched += item_unmatched # Carry over provenance resolved[_yaml.PROVENANCE_KEY] = variables[_yaml.PROVENANCE_KEY] return (resolved, unmatched) # Resolve it until it's resolved or broken # resolved = variables unmatched = ['dummy'] last_unmatched = ['dummy'] while unmatched: resolved, unmatched = resolve_one(resolved) # Lists of strings can be compared like this if unmatched == last_unmatched: # We've got the same result twice without matching everything, # something is undeclared or cyclic, compose a summary. # summary = '' for unmatch in set(unmatched): for var, provenance in self._find_references(unmatch): line = " unresolved variable '{unmatched}' in declaration of '{variable}' at: {provenance}\n" summary += line.format(unmatched=unmatch, variable=var, provenance=provenance) raise LoadError(LoadErrorReason.UNRESOLVED_VARIABLE, "Failed to resolve one or more variable:\n{}".format(summary)) last_unmatched = unmatched return resolved # Helper function to fetch information about the node referring to a variable # def _find_references(self, varname): fullname = _wrap_variable(varname) for key, value in _yaml.node_items(self.original): if fullname in value: provenance = _yaml.node_get_provenance(self.original, key) yield (key, provenance) def find_recursive_variable(variable, matched_variables, all_vars): matched_values = (_yaml.node_get(all_vars, str, key) for key in matched_variables) for key, value in zip(matched_variables, matched_values): if _wrap_variable(variable) in value: return key else: return None def _wrap_variable(var): return "%{" + var + "}"