5 Lazy conditional string evaluation module for Moe configuration variables.
8 * Each variable has ordered list of operations (definitions), each defining operation either
9 assigns (SET) or appends (APPEND) value of an expression to the variable. Each operation may be guarded by condition(s).
11 NOTE: If no 'SET' applies, a variable is still undefined even if some 'APPEND' applies. This might change.
13 * Each condition is a formula (tree consisting of 'AND', 'OR', 'NOT' and '==', '!=' between two expressions.
15 * Expression is a list of strings and variables to be expanded.
17 NOTE: All expanded data should be (or is converted to) unicode
20 TODO: Cleanup of unused undefined variables.
21 TODO: Better variable name checking (no name '.'-structural prefix of another)
22 TODO: Implemet "subtree" listing.
23 TODO: Test fixing, conditions and unicode
26 import types, itertools, re, bisect
28 from moe import MoeError
30 "Allowed depth of recursion - includes ALL recursive calls, so should quite high."
33 "Maximum attained depth of recursion - for debug/testing"
36 "Variable name regexp, dots (separators) must be separated from edges and each other."
37 re_VARNAME = re.compile(r'\A([A-Za-z0-9_-]+\.)*[A-Za-z0-9_-]+\Z')
39 def check_depth(depth):
40 "Helper to check for recursion depth."
42 if depth > c_maxdepth:
43 raise CyclicConfigError('Too deep recursion in config evaluation (cyclic substitution?)')
44 if depth > debug_maxdepth:
45 debug_maxdepth = depth
48 class ConfigError(MoeError):
51 class UndefinedError(ConfigError):
54 class VariableNameError(ConfigError):
57 class VariableFixedError(ConfigError):
60 class CyclicConfigError(ConfigError):
64 class ConfigTree(object):
66 Configuration tree containing all the variables.
68 The variables in `self.variables` are referenced directly by the full name.
74 def lookup(self, key, create = True):
76 Lookup and return a variable.
77 If not found and `create` set, check the name and transparently create a new one.
79 if key not in self.variables:
80 if not re_VARNAME.match(key):
81 raise VariableNameError('Invalid variable identifier %r in config', key)
83 raise UndefinedError('Config variable %r undefined.', key)
84 self.variables[key] = ConfigVar(key)
85 return self.variables[key]
87 def dump(self, prefix=''):
89 Pretty printing of the tree.
90 Returns an iterator of lines (strings).
92 return itertools.chain(*[
93 self.variables[k].dump(prefix) for k in sorted(self.variables.keys())
97 class ConfigElem(object):
99 Base class for cahed config elements - variables and conditions
102 def __init__(self, name):
103 # Full name with separators, definition for conditions
105 # Vars and conditions depending on value of this one
106 self.dependants = set([])
107 # Cached value (may be None in case of evaluation error)
109 self.cached_val = None
111 def invalidate(self, depth=0):
113 Invalidate cached data and invalidate all dependants.
114 Does nothing if not cached.
118 log.debug('invalidating %s', self)
120 for d in self.dependants:
121 d.invalidate(depth + 1)
123 def value(self, depth=0):
124 "Caching helper calling self.evaluate(), returns a value or throws an exception."
127 self.cached_val = self.evaluate(depth=depth+1)
129 if self.cached_val == None:
130 raise UndefinedError("Unable to evaluate %r."%(self.name,))
131 return self.cached_val
137 class ConfigCondition(ConfigElem):
139 Condition using equality and logic operators.
140 Clause is a tuple-tree in the following recursive form:
141 ('AND', c1, c1), ('OR', c1, c2), ('NOT', c1),
142 ('==', e1, e2), ('!=', e1, e2) where e1, e2 are `ConfigExpression`s.
145 def __init__(self, formula, text=None, parent=None):
147 Condition defined by `text` (informative), `formula` as in class definition,
148 `parent` is the parent condition (if any).
151 text = self.formula_string(formula)
152 super(ConfigCondition, self).__init__(text)
153 self.formula = formula
155 # Setup dependencies on used variables (not on the parent condition)
156 for v in self.variables():
157 v.dependants.add(self)
159 self.parent.dependants.add(self)
161 def variables(self, cl=None):
162 "Return an iterator of variables used in formula `cl`"
165 if cl[0] in ['==','!=']:
166 return itertools.chain(cl[1].variables(), cl[2].variables())
167 if cl[0] in ['AND','OR']:
168 return itertools.chain(self.variables(cl[1]), self.variables(cl[2]))
169 return self.variables(cl[1]) # only 'NOT' left
171 def remove_dependencies(self):
172 "Remove self as a dependant from all used variables"
173 for v in self.variables():
174 v.dependants.discard(self)
176 self.parent.dependants.discard(self)
178 def evaluate(self, cl=None, depth=0):
179 """Evaluate formula `cl` (or the entire condition).
180 Partial evaluation for AND and OR. Tests the parent condition first."""
184 if self.parent and not self.parent.value():
186 if cl[0] in ['==','!=']:
187 v = cl[1].evaluate(depth=depth+1) == cl[2].evaluate(depth=depth+1)
188 if cl[0] == '!=': v = not v
190 v1 = self.evaluate(cl=cl[1], depth=depth+1)
193 if cl[0] == 'OR' and v1: return True
194 if cl[0] == 'AND' and not v1: return False
195 return self.evaluate(cl=cl[2], depth=depth+1)
197 def formula_string(self, formula):
198 "Create a string representation of a formula."
199 if formula[0] == 'AND':
200 return itertools.chain(['('], self.formula_string(formula[1]), [' and '], self.formula_string(formula[2]),[')'])
201 elif formula[0] == 'OR':
202 return itertools.chain(['('], self.formula_string(formula[1]), [' or '], self.formula_string(formula[2]),[')'])
203 elif formula[0] == 'NOT':
204 return itertools.chain(['(not '], self.formula_string(formula[1]),[')'])
205 elif formula[0] in ['==', '!=']:
206 return itertools.chain(formula[1], formula[0], formula[2])
207 return iter(['<invalid formula>'])
209 def str(self, parents=False):
210 "Retur the defining expression, if `parents` set, then prefixed with parent conditions."
211 if parents and self.parent:
212 return self.parent.str(parents=True) + u' && ' + self.name
216 return self.str(parents=False)
219 class Operation(object):
220 "Helper class for operation data. Must not be present in more variables or present multiple times."
222 def __init__(self, operation, condition, expression, level=0, source='?'):
223 # operation is currently 'SET' and 'APPEND'
224 self.operation = operation
225 self.condition = condition
226 self.expression = expression
231 return "%s <%d, %s> [%s] %r" % ( {'SET':'=', 'APPEND':'+'}[self.operation], self.level, self.source,
232 (self.condition and self.condition.str(parents=True)) or '', unicode(self.expression))
235 class ConfigVar(ConfigElem):
237 def __init__(self, name):
238 super(ConfigVar, self).__init__(name)
239 # Ordered list of `Operations` (ascending by `level`)
241 # Fixed to value (may be None)
243 self.fixed_val = None
246 "Return a set of variables used in the expressions"
247 return set(sum([ list(op.expression.variables()) for op in self.operations ], []))
251 Fixes the value of the variable. Exception is raised should the variable
252 evaluate to a different value while fixed.
257 self.fixed_val = self.value()
260 "Set the variable to be modifiable again."
263 def value(self, depth=0):
264 "Handle the case when fixed, raise exc. on different evaluation"
265 val = super(ConfigVar,self).value(depth)
266 if self.fixed and self.fixed_val != val:
267 raise VariableFixedError("value of var %s was fixed to %r but evaluated to %r", self.name, self.fixed_val, val)
270 def add_operation(self, operation):
272 Inserts an operation. The operations are sorted by `level` (ascending), new operation goes last among
273 these with the same level.
274 Adds the variable as a dependant of the conditions and variables used in the expressions.
276 # Invalidate cached value
279 pos = bisect.bisect_right([o.level for o in self.operations], operation.level)
280 self.operations.insert(pos, operation)
281 # Create dependencies
282 for v in operation.expression.variables():
283 v.dependants.add(self)
284 if operation.condition:
285 operation.condition.dependants.add(self)
287 def remove_operation(self, operation):
289 Remove the Operation.
290 Also removes the variable as dependant from all conditions and variables used in this
291 operation that are no longer used.
293 # Invalidate cached value
295 # Remove the operation
296 self.operations.remove(operation)
297 # Remove dependencies on variables unused in other operations
298 vs = self.variables()
299 for v in operation.expression.variables():
301 v.dependants.remove(self)
302 # Remove the dependency on the conditions (if not used in another operation)
303 if operation.condition and operation.condition not in [op.condition for op in self.operations]:
304 operation.condition.dependants.remove(self)
306 def evaluate(self, depth=0):
308 Find the last 'SET' operation that applies and return the result of concatenating with all
309 subsequent applicable 'APPEND' operations. The result is the same as performing the operations
311 NOTE: undefined if some 'APPEND' apply but no 'SET' applies.
314 log.debug('evaluating var %r', self.name)
315 # List of strings to be concatenated
317 # Scan for last applicable expression - try each starting from the end, concatenate extensions
318 for i in range(len(self.operations)-1, -1, -1):
319 op = self.operations[i]
320 # Check the guarding condition
321 if (not op.condition) or op.condition.value(depth+1):
322 val.insert(0, op.expression.evaluate(depth=depth+1))
323 if op.operation == 'SET':
327 def dump(self, prefix=''):
329 Pretty printing of the variable. Includes all operations.
330 Returns iterator of lines (unicode strings).
332 # Try to evaluate the variable, but avoid undefined exceptions
335 v = self.value(depth=0)
338 yield prefix+u'%s = %r' % (self.name, v)
339 for op in self.operations:
340 #yield prefix+u' %s [%s] %s' % (op.operation, op.condition and op.condition.str(parents=True), op.expression)
341 yield prefix + u' ' + unicode(op)
344 class ConfigExpression(object):
346 String expression with some unexpanded config variables. Used in variable operations and conditions.
347 Expression is given as a list of unicode strings and ConfigVar variables to be expanded.
350 def __init__(self, exprlist, original = u'<unknown>'):
351 self.exprlist = exprlist
352 # Original defining string
353 self.original = original
354 # Replace strings with unicode
355 for i in range(len(self.exprlist)):
357 if isinstance(e, types.StringTypes):
358 if not isinstance(e, unicode):
359 self.exprlist[i] = unicode(e, 'ascii')
362 "Return an iterator of variables user in the expression"
363 return itertools.ifilter(lambda e: isinstance(e, ConfigVar), self.exprlist)
368 def evaluate(self, depth):
370 "Return unicode result of expansion of the variables."
372 for e in self.exprlist:
373 if isinstance(e, ConfigVar):
374 s.append(e.value(depth+1))
375 elif isinstance(e, unicode):
378 raise ConfigError('Invalid type %s in expression \'%s\'.'%(type(e), self))