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: Fixing value of variables.
21 TODO: Cleanup of unused undefined variables.
22 TODO: Better variable name checking (no name '.'-structural prefix of another)
23 TODO: Implemet "subtree" listing.
24 TODO: Test conditions and unicode
27 import types, itertools, re, bisect
29 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')
40 def check_depth(depth):
41 "Helper to check for recursion depth."
43 if depth > c_maxdepth:
44 raise CyclicConfigError('Too deep recursion in config evaluation (cyclic substitution?)')
45 if depth > debug_maxdepth:
46 debug_maxdepth = depth
49 class ConfigError(MoeError):
52 class UndefinedError(ConfigError):
55 class VariableNameError(ConfigError):
58 class CyclicConfigError(ConfigError):
62 class ConfigTree(object):
64 Configuration tree containing all the variables.
66 The variables in `self.variables` are referenced directly by the full name.
70 def lookup(self, key, create = True):
72 Lookup and return a variable.
73 If not found and `create` set, check the name and transparently create a new one.
75 if key not in self.variables:
76 if not re_VARNAME.match(key):
77 raise VariableNameError('Invalid variable identifier %r in config', key)
79 raise UndefinedError('Config variable %r undefined.', key)
80 self.variables[key] = ConfigVar(key)
81 return self.variables[key]
82 def dump(self, prefix=''):
84 Pretty printing of the tree.
85 Returns an iterator of lines (strings).
87 return itertools.chain(*[
88 self.variables[k].dump(prefix) for k in sorted(self.variables.keys())
91 class ConfigElem(object):
93 Base class for cahed config elements - variables and conditions
95 def __init__(self, name):
96 # Full name with separators, definition for conditions
98 # Vars and conditions depending on value of this one
99 self.dependants = set([])
100 # Cached value (may be None in case of evaluation error)
102 self.cached_val = None
103 def invalidate(self, depth=0):
105 Invalidate cached data and invalidate all dependants.
106 Does nothing if not cached.
110 log.debug('invalidating %s', self)
112 for d in self.dependants:
113 d.invalidate(depth + 1)
114 def value(self, depth=0):
115 "Caching helper calling self.evaluate(), returns a value or throws an exception."
118 self.cached_val = self.evaluate(depth=depth+1)
120 if self.cached_val == None:
121 raise UndefinedError("Unable to evaluate %r."%(self.name,))
122 return self.cached_val
126 class ConfigCondition(ConfigElem):
128 Condition using equality and logic operators.
129 Clause is a tuple-tree in the following recursive form:
130 ('AND', c1, c1), ('OR', c1, c2), ('NOT', c1),
131 ('==', e1, e2), ('!=', e1, e2) where e1, e2 are `ConfigExpression`s.
133 def __init__(self, formula, text=None, parent=None):
135 Condition defined by `text` (informative), `formula` as in class definition,
136 `parent` is the parent condition (if any).
139 text = self.formula_string(formula)
140 super(ConfigCondition, self).__init__(text)
141 self.formula = formula
143 # Setup dependencies on used variables (not on the parent condition)
144 for v in self.variables():
145 v.dependants.add(self)
147 self.parent.dependants.add(self)
148 def variables(self, cl=None):
149 "Return an iterator of variables used in formula `cl`"
152 if cl[0] in ['==','!=']:
153 return itertools.chain(cl[1].variables(), cl[2].variables())
154 if cl[0] in ['AND','OR']:
155 return itertools.chain(self.variables(cl[1]), self.variables(cl[2]))
156 return self.variables(cl[1]) # only 'NOT' left
157 def remove_dependencies(self):
158 "Remove self as a dependant from all used variables"
159 for v in self.variables():
160 v.dependants.discard(self)
162 self.parent.dependants.discard(self)
163 def evaluate(self, cl=None, depth=0):
164 """Evaluate formula `cl` (or the entire condition).
165 Partial evaluation for AND and OR. Tests the parent condition first."""
169 if self.parent and not self.parent.value():
171 if cl[0] in ['==','!=']:
172 v = cl[1].evaluate(depth=depth+1) == cl[2].evaluate(depth=depth+1)
173 if cl[0] == '!=': v = not v
175 v1 = self.evaluate(cl=cl[1], depth=depth+1)
178 if cl[0] == 'OR' and v1: return True
179 if cl[0] == 'AND' and not v1: return False
180 return self.evaluate(cl=cl[2], depth=depth+1)
181 def formula_string(self, formula):
182 "Create a string representation of a formula."
183 if formula[0] == 'AND':
184 return itertools.chain(['('], self.formula_string(formula[1]), [' and '], self.formula_string(formula[2]),[')'])
185 elif formula[0] == 'OR':
186 return itertools.chain(['('], self.formula_string(formula[1]), [' or '], self.formula_string(formula[2]),[')'])
187 elif formula[0] == 'NOT':
188 return itertools.chain(['(not '], self.formula_string(formula[1]),[')'])
189 elif formula[0] in ['==', '!=']:
190 return itertools.chain(formula[1], formula[0], formula[2])
191 return iter(['<invalid formula>'])
192 def str(self, parents=False):
193 "Retur the defining expression, if `parents` set, then prefixed with parent conditions."
194 if parents and self.parent:
195 return self.parent.str(parents=True) + u' && ' + self.name
198 return self.str(parents=False)
200 class Operation(object):
201 "Helper class for operation data. Must not be present in more variables or present multiple times."
202 def __init__(self, operation, condition, expression, level=0, source='?'):
203 # operation is currently 'SET' and 'APPEND'
204 self.operation = operation
205 self.condition = condition
206 self.expression = expression
210 return "%s <%d, %s> [%s] %r" % ( {'SET':'=', 'APPEND':'+'}[self.operation], self.level, self.source,
211 (self.condition and self.condition.str(parents=True)) or '', unicode(self.expression))
213 class ConfigVar(ConfigElem):
214 def __init__(self, name):
215 super(ConfigVar, self).__init__(name)
216 # Ordered list of `Operations` (ascending by `level`)
218 # Fixed to value (may be None)
221 self.fixed_val = None
223 "Return a set of variables used in the expressions"
224 return set(sum([ list(op.expression.variables()) for op in self.operations ], []))
225 def add_operation(self, operation):
227 Inserts an operation. The operations are sorted by `level` (ascending), new operation goes last among
228 these with the same level.
229 Adds the variable as a dependant of the conditions and variables used in the expressions.
231 # Invalidate cached value
234 pos = bisect.bisect_right([o.level for o in self.operations], operation.level)
235 self.operations.insert(pos, operation)
236 # Create dependencies
237 for v in operation.expression.variables():
238 v.dependants.add(self)
239 if operation.condition:
240 operation.condition.dependants.add(self)
241 def remove_operation(self, operation):
243 Remove the Operation.
244 Also removes the variable as dependant from all conditions and variables used in this
245 operation that are no longer used.
247 # Invalidate cached value
249 # Remove the operation
250 self.operations.remove(operation)
251 # Remove dependencies on variables unused in other operations
252 vs = self.variables()
253 for v in operation.expression.variables():
255 v.dependants.remove(self)
256 # Remove the dependency on the conditions (if not used in another operation)
257 if operation.condition and operation.condition not in [op.condition for op in self.operations]:
258 operation.condition.dependants.remove(self)
259 def evaluate(self, depth=0):
261 Find the last 'SET' operation that applies and return the result of concatenating with all
262 subsequent applicable 'APPEND' operations. The result is the same as performing the operations
264 NOTE: undefined if some 'APPEND' apply but no 'SET' applies.
267 log.debug('evaluating var %r', self.name)
268 # List of strings to be concatenated
270 # Scan for last applicable expression - try each starting from the end, concatenate extensions
271 for i in range(len(self.operations)-1, -1, -1):
272 op = self.operations[i]
273 # Check the guarding condition
274 if (not op.condition) or op.condition.value(depth+1):
275 val.insert(0, op.expression.evaluate(depth=depth+1))
276 if op.operation == 'SET':
279 def dump(self, prefix=''):
281 Pretty printing of the variable. Includes all operations.
282 Returns iterator of lines (unicode strings).
284 # Try to evaluate the variable, but avoid undefined exceptions
287 v = self.value(depth=0)
290 yield prefix+u'%s = %r' % (self.name, v)
291 for op in self.operations:
292 #yield prefix+u' %s [%s] %s' % (op.operation, op.condition and op.condition.str(parents=True), op.expression)
293 yield prefix + u' ' + unicode(op)
295 class ConfigExpression(object):
297 String expression with some unexpanded config variables. Used in variable operations and conditions.
298 Expression is given as a list of unicode strings and ConfigVar variables to be expanded.
300 def __init__(self, exprlist, original = u'<unknown>'):
301 self.exprlist = exprlist
302 # Original defining string
303 self.original = original
304 # Replace strings with unicode
305 for i in range(len(self.exprlist)):
307 if isinstance(e, types.StringTypes):
308 if not isinstance(e, unicode):
309 self.exprlist[i] = unicode(e, 'ascii')
311 "Return an iterator of variables user in the expression"
312 return itertools.ifilter(lambda e: isinstance(e, ConfigVar), self.exprlist)
315 def evaluate(self, depth):
317 "Return unicode result of expansion of the variables."
319 for e in self.exprlist:
320 if isinstance(e, ConfigVar):
321 s.append(e.value(depth+1))
322 elif isinstance(e, unicode):
325 raise ConfigError('Invalid type %s in expression \'%s\'.'%(type(e), self))