1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
|
#
# 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(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
# We failed to find a recursive variable
return None
def _wrap_variable(var):
return "%{" + var + "}"
|