#!/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)