| #!/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 <http://www.gnu.org/licenses/>. |
| # |
| # Authors: |
| # Tristan Van Berkom <tristan.vanberkom@codethink.co.uk> |
| |
| 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) |