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 confparser import re_VARNAME
31 "Allowed depth of recursion - includes ALL recursive calls, so should quite high."
34 "Maximum attained depth of recursion - for debug/testing"
37 def check_depth(depth):
38 "Helper to check for recursion depth."
40 if depth > c_maxdepth:
41 raise ConfigError('Too deep recursion in config evaluation (cyclic substitution?)')
42 if depth > debug_maxdepth:
43 debug_maxdepth = depth
46 class ConfigError(Exception):
50 class ConfigTree(object):
52 Configuration tree containing all the variables.
54 The variables in `self.variables` are referenced directly by the full name.
58 def lookup(self, key, create = True):
60 Lookup and return a variable.
61 If not found and `create` set, check the name and transparently create a new one.
63 if not key in self.variables:
65 raise ConfigError('Config variable %r undefined.', key)
66 if not re_VARNAME.match(key):
67 raise ConfigError('Invalid variable identifier %r in config', key)
68 self.variables[key] = ConfigVar(key)
69 return self.variables[key]
70 def dump(self, prefix=''):
72 Pretty printing of the tree.
73 Returns an iterator of lines (strings).
75 return itertools.chain(*[
76 self.variables[k].dump(prefix) for k in sorted(self.variables.keys())
79 class ConfigElem(object):
81 Base class for cahed config elements - variables and conditions
83 def __init__(self, name):
84 # Full name with separators, definition for conditions
86 # Vars and conditions depending on value of this one
87 self.dependants = set([])
88 # Cached value (may be None in case of evaluation error)
90 self.cached_val = None
91 def invalidate(self, depth=0):
93 Invalidate cached data and invalidate all dependants.
94 Does nothing if not cached.
98 log.debug('invalidating %r', self.name)
100 for d in self.dependants:
101 d.invalidate(depth + 1)
102 def value(self, depth=0):
103 "Caching helper calling self.evaluate(), returns a value or throws an exception."
106 self.cached_val = self.evaluate(depth+1)
108 if self.cached_val == None:
109 raise ConfigError("Unable to evaluate %r."%(self.name,))
110 return self.cached_val
114 class ConfigCondition(ConfigElem):
116 Condition using equality and logic operators.
117 Clause is a tuple-tree in the following recursive form:
118 ('AND', c1, c1), ('OR', c1, c2), ('NOT', c1),
119 ('==', e1, e2), ('!=', e1, e2) where e1, e2 are `ConfigExpression`s.
121 def __init__(self, formula, text=None, parent=None):
123 Condition defined by `text` (informative), `formula` as in class definition,
124 `parent` is the parent condition (if any).
127 text = self.formula_string(formula)
128 super(ConfigVar, self).__init__(text)
129 self.formula = formula
131 # Setup dependencies on used variables (not on the parent condition)
132 for v in self.variables():
133 v.dependants.add(self)
135 self.parent.dependants.add(self)
136 def variables(self, cl=None):
137 "Return an iterator of variables used in formula `cl`"
140 if cl[0] in ['==','!=']:
141 return itertools.chain(cl[1].variables(), cl[2].variables())
142 if cl[0] in ['AND','OR']:
143 return itertools.chain(self.variables(cl[1]), self.variables(cl[2]))
144 return self.variables(cl[1]) # only 'NOT' left
145 def remove_dependencies(self):
146 "Remove self as a dependant from all used variables"
147 for v in self.variables():
148 v.dependants.discard(self)
150 self.parent.dependants.discard(self)
151 def evaluate(self, cl=None, depth=0):
152 """Evaluate formula `cl` (or the entire condition).
153 Partial evaluation for AND and OR. Tests the parent condition first."""
157 if self.parent and not self.parent.value():
159 if cl[0] in ['==','!=']:
160 v = cl[1].evaluate(depth+1) == cl[2].evaluate(depth+1)
161 if cl[0] == '!=': v = not v
163 v1 = self.evaluate(cl[1], depth+1)
166 if cl[0] == 'OR' and v1: return True
167 if cl[0] == 'AND' and not v1: return False
168 return self.evaluate(cl[2], depth+1)
169 def formula_string(self, formula):
170 "Create a string representation of a formula."
171 if formula[0] == 'AND':
172 return itertools.chain(['('], self.formula_string(formula[1]), [' and '], self.formula_string(formula[2]),[')'])
173 elif formula[0] == 'OR':
174 return itertools.chain(['('], self.formula_string(formula[1]), [' or '], self.formula_string(formula[2]),[')'])
175 elif formula[0] == 'NOT':
176 return itertools.chain(['(not '], self.formula_string(formula[1]),[')'])
177 elif formula[0] in ['==', '!=']:
178 return itertools.chain(formula[1], formula[0], formula[2])
179 return iter(['<invalid formula>'])
180 def str(self, parents=False):
181 "Retur the defining expression, if `parents` set, then prefixed with parent conditions."
182 if parents and self.parent:
183 return self.parent.str(parents=True) + u' && ' + self.name
186 return self.str(parents=False)
188 class Operation(object):
189 "Helper class for operation data. Must not be present in more variables or present multiple times."
190 def __init__(self, operation, condition, expression, level=0, source='?'):
191 # operation is currently 'SET' and 'APPEND'
192 self.operation = operation
193 self.condition = condition
194 self.expression = expression
198 return "%s <%d, %s> [%s] %r" % ( {'SET':'=', 'APPEND':'+'}[self.operation], self.level, self.source,
199 (self.condition and self.condition.str(parents=True)) or '', unicode(self.expression))
201 class ConfigVar(ConfigElem):
202 def __init__(self, name):
203 super(ConfigVar, self).__init__(name)
204 # Ordered list of `Operations` (ascending by `level`)
206 # Fixed to value (may be None)
209 self.fixed_val = None
211 "Return a set of variables used in the expressions"
212 return set(sum([ list(op.expression.variables()) for op in self.operations ], []))
213 def add_operation(self, operation):
215 Inserts an operation. The operations are sorted by `level` (ascending), new operation goes last among
216 these with the same level.
217 Adds the variable as a dependant of the conditions and variables used in the expressions.
219 # Invalidate cached value
222 pos = bisect.bisect_right([o.level for o in self.operations], operation.level)
223 self.operations.insert(pos, operation)
224 # Create dependencies
225 for v in operation.expression.variables():
226 v.dependants.add(self)
227 if operation.condition:
228 operation.condition.dependants.add(self)
229 def remove_operation(self, operation):
231 Remove the Operation.
232 Also removes the variable as dependant from all conditions and variables used in this
233 operation that are no longer used.
235 # Invalidate cached value
237 # Remove the operation
238 self.operations.remove(operation)
239 # Remove dependencies on variables unused in other operations
240 vs = self.variables()
241 for v in operation.expression.variables():
243 v.dependants.remove(self)
244 # Remove the dependency on the conditions (if not used in another operation)
245 if operation.condition and operation.condition not in [op.condition for op in self.operations]:
246 condition.dependants.remove(self)
247 def evaluate(self, depth=0):
249 Find the last 'SET' operation that applies and return the result of concatenating with all
250 subsequent applicable 'APPEND' operations. The result is the same as performing the operations
252 NOTE: undefined if some 'APPEND' apply but no 'SET' applies.
255 log.debug('evaluating var %r', self.name)
256 # List of strings to be concatenated
258 # Scan for last applicable expression - try each starting from the end, concatenate extensions
259 for i in range(len(self.operations)-1, -1, -1):
260 op = self.operations[i]
261 # Check the guarding condition
262 if (not op.condition) or op.condition.value(depth+1):
263 val.insert(0, op.expression.evaluate(depth+1))
264 if op.operation == 'SET':
267 def dump(self, prefix=''):
269 Pretty printing of the variable. Includes all operations.
270 Returns iterator of lines (unicode strings).
272 # Try to evaluate the variable, but avoid undefined exceptions
275 v = self.value(depth=0)
278 yield prefix+u'%s = %r' % (self.name, v)
279 for op in self.operations:
280 #yield prefix+u' %s [%s] %s' % (op.operation, op.condition and op.condition.str(parents=True), op.expression)
281 yield prefix + u' ' + unicode(op)
283 class ConfigExpression(object):
285 String expression with some unexpanded config variables. Used in variable operations and conditions.
286 Expression is given as a list of unicode strings and ConfigVar variables to be expanded.
288 def __init__(self, exprlist, original = u'<unknown>'):
289 self.exprlist = exprlist
290 # Original defining string
291 self.original = original
292 # Replace strings with unicode
293 for i in range(len(self.exprlist)):
295 if isinstance(e, types.StringTypes):
296 if not isinstance(e, unicode):
297 self.exprlist[i] = unicode(e, 'ascii')
299 "Return an iterator of variables user in the expression"
300 return itertools.ifilter(lambda e: isinstance(e, ConfigVar), self.exprlist)
303 def evaluate(self, depth):
305 "Return unicode result of expansion of the variables."
307 for e in self.exprlist:
308 if isinstance(e, ConfigVar):
309 s.append(e.value(depth+1))
310 elif isinstance(e, unicode):
313 raise ConfigError('Invalid type %s in expression \'%s\'.'%(type(e), self))