2 Lazy conditional string evaluation module for Moe configuration variables.
4 * Each variable has ordered list of operations (definitions), each defining operation either
5 assigns (SET) or appends (APPEND) value of an expression to the variable. Each operation may be guarded by condition(s).
7 * Each condition is a formula (tree consisting of 'AND', 'OR', 'NOT' and '==', '!=' between two expressions.
9 * Expression is a list of strings and variables to be expanded.
11 .. note:: If no 'SET' applies, a variable is still undefined even if some 'APPEND' applies. This might change.
12 .. note:: All expanded data should be (or is converted to) unicode
13 .. todo:: (OPT) Cleanup of unused undefined variables.
14 .. todo:: (OPT) Better variable name checking (no name '.'-structural prefix of another)
15 .. todo:: (OPT) Implemet "subtree" listing.
18 import types, itertools, re, bisect
21 from moe import MoeError
24 "Allowed depth of recursion - includes ALL recursive calls, so should quite high."
27 "Maximum attained depth of recursion - for debug/testing"
30 "Variable name regexp, dots (separators) must be separated from edges and each other."
31 re_VARNAME = re.compile(r'\A([A-Za-z0-9_-]+\.)*[A-Za-z0-9_-]+\Z')
34 def check_depth(depth):
35 "Helper to check for recursion depth."
37 if depth > c_maxdepth:
38 raise CyclicConfigError('Too deep recursion in config evaluation (cyclic substitution?)')
39 if depth > debug_maxdepth:
40 debug_maxdepth = depth
43 class ConfigError(MoeError):
46 class UndefinedError(ConfigError):
49 class VariableNameError(ConfigError):
52 class VariableFixedError(ConfigError):
55 class CyclicConfigError(ConfigError):
59 class ConfigTree(object):
61 Configuration tree containing all the variables.
63 The variables in `self.variables` are referenced directly by the full name.
69 def lookup(self, key, create = True):
71 Lookup and return a variable.
72 If not found and `create` set, check the name and transparently create a new one.
74 if key not in self.variables:
75 if not re_VARNAME.match(key):
76 raise VariableNameError('Invalid variable identifier %r in config', key)
78 raise UndefinedError('Config variable %r undefined.', key)
79 self.variables[key] = ConfigVar(key)
80 return self.variables[key]
82 def __getitem__(self, key):
84 Return the value of an existing variable.
86 return self.lookup(key, create=False).value()
88 def dump(self, prefix=''):
90 Pretty printing of the tree.
91 Returns an iterator of lines (strings).
93 return itertools.chain(*[
94 self.variables[k].dump(prefix) for k in sorted(self.variables.keys())
98 "Fix value of variable or list of variables. Fixing undefined variable raises `UndefinedError`."
99 if isinstance(keys, types.StringTypes):
102 self.lookup(key, create=True).fix()
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.config_parser
107 p = moe.config_parser.ConfigParser(s, 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 with open(filename, 'rt') as f:
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::
164 ('AND', c1, c1), ('OR', c1, c2), ('NOT', c1), ('==', e1, e2), ('!=', e1, e2)
166 where e1, e2 are `ConfigExpression`, c1, c2, `ConfigCondition`.
169 def __init__(self, formula, text=None, parent=None):
171 Condition defined by `text` (informative), `formula` as in class definition,
172 `parent` is the parent condition (if any).
175 text = self.formula_string(formula)
176 super(ConfigCondition, self).__init__(text)
177 self.formula = formula
179 # Setup dependencies on used variables (not on the parent condition)
180 for v in self.variables():
181 v.dependants.add(self)
183 self.parent.dependants.add(self)
185 def variables(self, cl=None):
186 "Return an iterator of variables used in formula `cl`"
189 if cl[0] in ['==','!=']:
190 return itertools.chain(cl[1].variables(), cl[2].variables())
191 if cl[0] in ['AND','OR']:
192 return itertools.chain(self.variables(cl[1]), self.variables(cl[2]))
193 return self.variables(cl[1]) # only 'NOT' left
195 def remove_dependencies(self):
196 "Remove self as a dependant from all used variables"
197 for v in self.variables():
198 v.dependants.discard(self)
200 self.parent.dependants.discard(self)
202 def evaluate(self, cl=None, depth=0):
203 """Evaluate formula `cl` (or the entire condition).
204 Partial evaluation for AND and OR. Tests the parent condition first."""
208 if self.parent and not self.parent.value():
210 if cl[0] in ['==','!=']:
211 v = cl[1].evaluate(depth=depth+1) == cl[2].evaluate(depth=depth+1)
212 if cl[0] == '!=': v = not v
214 v1 = self.evaluate(cl=cl[1], depth=depth+1)
217 if cl[0] == 'OR' and v1: return True
218 if cl[0] == 'AND' and not v1: return False
219 return self.evaluate(cl=cl[2], depth=depth+1)
221 def formula_string(self, formula):
222 "Create a string representation of a formula."
223 if formula[0] == 'AND':
224 return itertools.chain(['('], self.formula_string(formula[1]), [' and '], self.formula_string(formula[2]),[')'])
225 elif formula[0] == 'OR':
226 return itertools.chain(['('], self.formula_string(formula[1]), [' or '], self.formula_string(formula[2]),[')'])
227 elif formula[0] == 'NOT':
228 return itertools.chain(['(not '], self.formula_string(formula[1]),[')'])
229 elif formula[0] in ['==', '!=']:
230 return itertools.chain(formula[1], formula[0], formula[2])
231 return iter(['<invalid formula>'])
233 def str(self, parents=False):
234 "Retur the defining expression, if `parents` set, then prefixed with parent conditions."
235 if parents and self.parent:
236 return self.parent.str(parents=True) + u' && ' + self.name
240 return self.str(parents=False)
243 class Operation(object):
244 "Helper class for operation data. Must not be present in more variables or present multiple times."
246 def __init__(self, operation, condition, expression, level=0, source='?'):
247 # operation is currently 'SET' and 'APPEND'
248 self.operation = operation
249 self.condition = condition
250 self.expression = expression
255 return "%s <%d, %s> [%s] %r" % ( {'SET':'=', 'APPEND':'+'}[self.operation], self.level, self.source,
256 (self.condition and self.condition.str(parents=True)) or '', unicode(self.expression))
259 class ConfigVar(ConfigElem):
261 def __init__(self, name):
262 super(ConfigVar, self).__init__(name)
263 # Ordered list of `Operations` (ascending by `level`)
265 # Fixed to value (may be None)
267 self.fixed_val = None
270 "Return a set of variables used in the expressions"
271 if not self.operations:
273 return set.union(*[ op.expression.variables() for op in self.operations ])
277 Fixes the value of the variable. Exception is raised should the variable
278 evaluate to a different value while fixed.
282 self.fixed_val = self.value()
286 "Make the variable modifiable again."
289 def value(self, depth=0):
290 "Handle the case when fixed, raise exc. on different evaluation"
291 val = super(ConfigVar,self).value(depth)
292 if self.fixed and self.fixed_val != val:
293 raise VariableFixedError("value of var %r was fixed to %r but evaluated to %r", self.name, self.fixed_val, val)
296 def add_operation(self, operation):
298 Inserts an operation. The operations are sorted by `level` (ascending), new operation goes last among
299 these with the same level.
300 Adds the variable as a dependant of the conditions and variables used in the expressions.
302 # Invalidate cached value
305 pos = bisect.bisect_right([o.level for o in self.operations], operation.level)
306 self.operations.insert(pos, operation)
307 # Create dependencies
308 for v in operation.expression.variables():
309 v.dependants.add(self)
310 if operation.condition:
311 operation.condition.dependants.add(self)
313 def remove_operation(self, operation):
315 Remove the Operation.
316 Also removes the variable as dependant from all conditions and variables used in this
317 operation that are no longer used.
319 # Invalidate cached value
321 # Remove the operation
322 self.operations.remove(operation)
323 # Remove dependencies on variables unused in other defining operations
324 vs = self.variables()
325 for v in operation.expression.variables():
327 v.dependants.remove(self)
328 # Remove the dependency on the conditions (if not used in another operation)
329 if operation.condition and operation.condition not in [op.condition for op in self.operations]:
330 operation.condition.dependants.remove(self)
332 def evaluate(self, depth=0):
334 Find the last 'SET' operation that applies and return the result of concatenating with all
335 subsequent applicable 'APPEND' operations. The result is the same as performing the operations
337 NOTE: undefined if some 'APPEND' apply but no 'SET' applies.
340 log.debug('evaluating var %r', self.name)
341 # List of strings to be concatenated
343 # Scan for last applicable expression - try each starting from the end, concatenate extensions
344 for i in range(len(self.operations)-1, -1, -1):
345 op = self.operations[i]
346 # Check the guarding condition
347 if (not op.condition) or op.condition.value(depth+1):
348 val.insert(0, op.expression.evaluate(depth=depth+1))
349 if op.operation == 'SET':
353 def dump(self, prefix=''):
355 Pretty printing of the variable. Includes all operations.
356 Returns iterator of lines (unicode strings).
358 # Try to evaluate the variable, but avoid undefined exceptions
361 v = self.value(depth=0)
364 yield prefix+u'%s = %r' % (self.name, v)
365 for op in self.operations:
366 #yield prefix+u' %s [%s] %s' % (op.operation, op.condition and op.condition.str(parents=True), op.expression)
367 yield prefix + u' ' + unicode(op)
370 class ConfigExpression(object):
372 String expression with some unexpanded config variables. Used in variable operations and conditions.
373 Expression is given as a list of unicode strings and ConfigVar variables to be expanded.
376 def __init__(self, exprlist, original = u'<unknown>'):
377 self.exprlist = list(exprlist)
378 # Original defining string
379 self.original = original
380 # Replace strings with unicode
381 for i in range(len(self.exprlist)):
383 if isinstance(e, types.StringTypes):
384 if not isinstance(e, unicode):
385 self.exprlist[i] = unicode(e, 'ascii')
388 "Return a set of variables used in the expression"
389 return set([e for e in self.exprlist if isinstance(e, ConfigVar)])
394 def evaluate(self, depth):
396 "Return unicode result of expansion of the variables."
398 for e in self.exprlist:
399 if isinstance(e, ConfigVar):
400 s.append(e.value(depth+1))
401 elif isinstance(e, unicode):
404 raise ConfigError('Invalid type %s in expression \'%s\'.'%(type(e), self))