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 (OPT): Cleanup of unused undefined variables.
21 TODO (OPT): Better variable name checking (no name '.'-structural prefix of another)
22 TODO (OPT): Implemet "subtree" listing.
25 import types, itertools, re, bisect
28 from moe import MoeError
31 "Allowed depth of recursion - includes ALL recursive calls, so should quite high."
34 "Maximum attained depth of recursion - for debug/testing"
37 "Variable name regexp, dots (separators) must be separated from edges and each other."
38 re_VARNAME = re.compile(r'\A([A-Za-z0-9_-]+\.)*[A-Za-z0-9_-]+\Z')
41 def check_depth(depth):
42 "Helper to check for recursion depth."
44 if depth > c_maxdepth:
45 raise CyclicConfigError('Too deep recursion in config evaluation (cyclic substitution?)')
46 if depth > debug_maxdepth:
47 debug_maxdepth = depth
50 class ConfigError(MoeError):
53 class UndefinedError(ConfigError):
56 class VariableNameError(ConfigError):
59 class VariableFixedError(ConfigError):
62 class CyclicConfigError(ConfigError):
66 class ConfigTree(object):
68 Configuration tree containing all the variables.
70 The variables in `self.variables` are referenced directly by the full name.
76 def lookup(self, key, create = True):
78 Lookup and return a variable.
79 If not found and `create` set, check the name and transparently create a new one.
81 if key not in self.variables:
82 if not re_VARNAME.match(key):
83 raise VariableNameError('Invalid variable identifier %r in config', key)
85 raise UndefinedError('Config variable %r undefined.', key)
86 self.variables[key] = ConfigVar(key)
87 return self.variables[key]
89 def __getitem__(self, key):
91 Return the value of an existing variable.
93 return self.lookup(key, create=False).value()
95 def dump(self, prefix=''):
97 Pretty printing of the tree.
98 Returns an iterator of lines (strings).
100 return itertools.chain(*[
101 self.variables[k].dump(prefix) for k in sorted(self.variables.keys())
104 def parse(self, s, source=None, level=0):
105 """Parse `s` (stream/string) into the tree, see `moe.confparser.ConfigParser` for details."""
106 import moe.confparser
107 p = moe.confparser.ConfigParser(text, self, source=source, level=level)
110 def parse_file(self, filename, desc=None, level=0):
111 """Parse an utf-8 file into the tree, see `moe.confparser.ConfigParser` for details.
112 Names the source "`filename` <`desc`>". """
113 f = open(filename, 'rt')
115 filename += " <" + desc + ">"
116 self.parse(f, source=filename, level=level)
119 class ConfigElem(object):
121 Base class for cahed config elements - variables and conditions
124 def __init__(self, name):
125 # Full name with separators, definition for conditions
127 # Vars and conditions depending on value of this one
128 self.dependants = set([])
129 # Cached value (may be None in case of evaluation error)
131 self.cached_val = None
133 def invalidate(self, depth=0):
135 Invalidate cached data and invalidate all dependants.
136 Does nothing if not cached.
140 log.debug('invalidating %s', self)
142 for d in self.dependants:
143 d.invalidate(depth + 1)
145 def value(self, depth=0):
146 "Caching helper calling self.evaluate(), returns a value or throws an exception."
149 self.cached_val = self.evaluate(depth=depth+1)
151 if self.cached_val == None:
152 raise UndefinedError("Unable to evaluate %r."%(self.name,))
153 return self.cached_val
159 class ConfigCondition(ConfigElem):
161 Condition using equality and logic operators.
162 Clause is a tuple-tree in the following recursive form:
163 ('AND', c1, c1), ('OR', c1, c2), ('NOT', c1),
164 ('==', e1, e2), ('!=', e1, e2) where e1, e2 are `ConfigExpression`s.
167 def __init__(self, formula, text=None, parent=None):
169 Condition defined by `text` (informative), `formula` as in class definition,
170 `parent` is the parent condition (if any).
173 text = self.formula_string(formula)
174 super(ConfigCondition, self).__init__(text)
175 self.formula = formula
177 # Setup dependencies on used variables (not on the parent condition)
178 for v in self.variables():
179 v.dependants.add(self)
181 self.parent.dependants.add(self)
183 def variables(self, cl=None):
184 "Return an iterator of variables used in formula `cl`"
187 if cl[0] in ['==','!=']:
188 return itertools.chain(cl[1].variables(), cl[2].variables())
189 if cl[0] in ['AND','OR']:
190 return itertools.chain(self.variables(cl[1]), self.variables(cl[2]))
191 return self.variables(cl[1]) # only 'NOT' left
193 def remove_dependencies(self):
194 "Remove self as a dependant from all used variables"
195 for v in self.variables():
196 v.dependants.discard(self)
198 self.parent.dependants.discard(self)
200 def evaluate(self, cl=None, depth=0):
201 """Evaluate formula `cl` (or the entire condition).
202 Partial evaluation for AND and OR. Tests the parent condition first."""
206 if self.parent and not self.parent.value():
208 if cl[0] in ['==','!=']:
209 v = cl[1].evaluate(depth=depth+1) == cl[2].evaluate(depth=depth+1)
210 if cl[0] == '!=': v = not v
212 v1 = self.evaluate(cl=cl[1], depth=depth+1)
215 if cl[0] == 'OR' and v1: return True
216 if cl[0] == 'AND' and not v1: return False
217 return self.evaluate(cl=cl[2], depth=depth+1)
219 def formula_string(self, formula):
220 "Create a string representation of a formula."
221 if formula[0] == 'AND':
222 return itertools.chain(['('], self.formula_string(formula[1]), [' and '], self.formula_string(formula[2]),[')'])
223 elif formula[0] == 'OR':
224 return itertools.chain(['('], self.formula_string(formula[1]), [' or '], self.formula_string(formula[2]),[')'])
225 elif formula[0] == 'NOT':
226 return itertools.chain(['(not '], self.formula_string(formula[1]),[')'])
227 elif formula[0] in ['==', '!=']:
228 return itertools.chain(formula[1], formula[0], formula[2])
229 return iter(['<invalid formula>'])
231 def str(self, parents=False):
232 "Retur the defining expression, if `parents` set, then prefixed with parent conditions."
233 if parents and self.parent:
234 return self.parent.str(parents=True) + u' && ' + self.name
238 return self.str(parents=False)
241 class Operation(object):
242 "Helper class for operation data. Must not be present in more variables or present multiple times."
244 def __init__(self, operation, condition, expression, level=0, source='?'):
245 # operation is currently 'SET' and 'APPEND'
246 self.operation = operation
247 self.condition = condition
248 self.expression = expression
253 return "%s <%d, %s> [%s] %r" % ( {'SET':'=', 'APPEND':'+'}[self.operation], self.level, self.source,
254 (self.condition and self.condition.str(parents=True)) or '', unicode(self.expression))
257 class ConfigVar(ConfigElem):
259 def __init__(self, name):
260 super(ConfigVar, self).__init__(name)
261 # Ordered list of `Operations` (ascending by `level`)
263 # Fixed to value (may be None)
265 self.fixed_val = None
268 "Return a set of variables used in the expressions"
269 return set(sum([ list(op.expression.variables()) for op in self.operations ], []))
273 Fixes the value of the variable. Exception is raised should the variable
274 evaluate to a different value while fixed.
278 self.fixed_val = self.value()
282 "Set the variable to be modifiable again."
285 def value(self, depth=0):
286 "Handle the case when fixed, raise exc. on different evaluation"
287 val = super(ConfigVar,self).value(depth)
288 if self.fixed and self.fixed_val != val:
289 raise VariableFixedError("value of var %s was fixed to %r but evaluated to %r", self.name, self.fixed_val, val)
292 def add_operation(self, operation):
294 Inserts an operation. The operations are sorted by `level` (ascending), new operation goes last among
295 these with the same level.
296 Adds the variable as a dependant of the conditions and variables used in the expressions.
298 # Invalidate cached value
301 pos = bisect.bisect_right([o.level for o in self.operations], operation.level)
302 self.operations.insert(pos, operation)
303 # Create dependencies
304 for v in operation.expression.variables():
305 v.dependants.add(self)
306 if operation.condition:
307 operation.condition.dependants.add(self)
309 def remove_operation(self, operation):
311 Remove the Operation.
312 Also removes the variable as dependant from all conditions and variables used in this
313 operation that are no longer used.
315 # Invalidate cached value
317 # Remove the operation
318 self.operations.remove(operation)
319 # Remove dependencies on variables unused in other operations
320 vs = self.variables()
321 for v in operation.expression.variables():
323 v.dependants.remove(self)
324 # Remove the dependency on the conditions (if not used in another operation)
325 if operation.condition and operation.condition not in [op.condition for op in self.operations]:
326 operation.condition.dependants.remove(self)
328 def evaluate(self, depth=0):
330 Find the last 'SET' operation that applies and return the result of concatenating with all
331 subsequent applicable 'APPEND' operations. The result is the same as performing the operations
333 NOTE: undefined if some 'APPEND' apply but no 'SET' applies.
336 log.debug('evaluating var %r', self.name)
337 # List of strings to be concatenated
339 # Scan for last applicable expression - try each starting from the end, concatenate extensions
340 for i in range(len(self.operations)-1, -1, -1):
341 op = self.operations[i]
342 # Check the guarding condition
343 if (not op.condition) or op.condition.value(depth+1):
344 val.insert(0, op.expression.evaluate(depth=depth+1))
345 if op.operation == 'SET':
349 def dump(self, prefix=''):
351 Pretty printing of the variable. Includes all operations.
352 Returns iterator of lines (unicode strings).
354 # Try to evaluate the variable, but avoid undefined exceptions
357 v = self.value(depth=0)
360 yield prefix+u'%s = %r' % (self.name, v)
361 for op in self.operations:
362 #yield prefix+u' %s [%s] %s' % (op.operation, op.condition and op.condition.str(parents=True), op.expression)
363 yield prefix + u' ' + unicode(op)
366 class ConfigExpression(object):
368 String expression with some unexpanded config variables. Used in variable operations and conditions.
369 Expression is given as a list of unicode strings and ConfigVar variables to be expanded.
372 def __init__(self, exprlist, original = u'<unknown>'):
373 self.exprlist = list(exprlist)
374 # Original defining string
375 self.original = original
376 # Replace strings with unicode
377 for i in range(len(self.exprlist)):
379 if isinstance(e, types.StringTypes):
380 if not isinstance(e, unicode):
381 self.exprlist[i] = unicode(e, 'ascii')
384 "Return an iterator of variables user in the expression"
385 return itertools.ifilter(lambda e: isinstance(e, ConfigVar), self.exprlist)
390 def evaluate(self, depth):
392 "Return unicode result of expansion of the variables."
394 for e in self.exprlist:
395 if isinstance(e, ConfigVar):
396 s.append(e.value(depth+1))
397 elif isinstance(e, unicode):
400 raise ConfigError('Invalid type %s in expression \'%s\'.'%(type(e), self))