#!/usr/bin/env python3 # # 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_internal(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_internal(self, string, variables): def subst_callback(match): nonlocal variables nonlocal unmatched token = match.group(0) varname = match.group(1) value = _yaml.node_get(variables, str, varname) 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) else: # Return unmodified token unmatched += [varname] value = token return value unmatched = [] replacement = re.sub(VARIABLE_MATCH, subst_callback, string) return (replacement, unmatched) # 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 = self.subst_internal(value, variables) 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 = '%{' + 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)