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
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(Exception):
51 class UndefinedError(ConfigError):
54 class VariableNameError(ConfigError):
57 class CyclicConfigError(ConfigError):
61 class ConfigTree(object):
63 Configuration tree containing all the variables.
65 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]
81 def dump(self, prefix=''):
83 Pretty printing of the tree.
84 Returns an iterator of lines (strings).
86 return itertools.chain(*[
87 self.variables[k].dump(prefix) for k in sorted(self.variables.keys())
90 class ConfigElem(object):
92 Base class for cahed config elements - variables and conditions
94 def __init__(self, name):
95 # Full name with separators, definition for conditions
97 # Vars and conditions depending on value of this one
98 self.dependants = set([])
99 # Cached value (may be None in case of evaluation error)
101 self.cached_val = None
102 def invalidate(self, depth=0):
104 Invalidate cached data and invalidate all dependants.
105 Does nothing if not cached.
109 log.debug('invalidating %s', self)
111 for d in self.dependants:
112 d.invalidate(depth + 1)
113 def value(self, depth=0):
114 "Caching helper calling self.evaluate(), returns a value or throws an exception."
117 self.cached_val = self.evaluate(depth=depth+1)
119 if self.cached_val == None:
120 raise UndefinedError("Unable to evaluate %r."%(self.name,))
121 return self.cached_val
125 class ConfigCondition(ConfigElem):
127 Condition using equality and logic operators.
128 Clause is a tuple-tree in the following recursive form:
129 ('AND', c1, c1), ('OR', c1, c2), ('NOT', c1),
130 ('==', e1, e2), ('!=', e1, e2) where e1, e2 are `ConfigExpression`s.
132 def __init__(self, formula, text=None, parent=None):
134 Condition defined by `text` (informative), `formula` as in class definition,
135 `parent` is the parent condition (if any).
138 text = self.formula_string(formula)
139 super(ConfigCondition, self).__init__(text)
140 self.formula = formula
142 # Setup dependencies on used variables (not on the parent condition)
143 for v in self.variables():
144 v.dependants.add(self)
146 self.parent.dependants.add(self)
147 def variables(self, cl=None):
148 "Return an iterator of variables used in formula `cl`"
151 if cl[0] in ['==','!=']:
152 return itertools.chain(cl[1].variables(), cl[2].variables())
153 if cl[0] in ['AND','OR']:
154 return itertools.chain(self.variables(cl[1]), self.variables(cl[2]))
155 return self.variables(cl[1]) # only 'NOT' left
156 def remove_dependencies(self):
157 "Remove self as a dependant from all used variables"
158 for v in self.variables():
159 v.dependants.discard(self)
161 self.parent.dependants.discard(self)
162 def evaluate(self, cl=None, depth=0):
163 """Evaluate formula `cl` (or the entire condition).
164 Partial evaluation for AND and OR. Tests the parent condition first."""
168 if self.parent and not self.parent.value():
170 if cl[0] in ['==','!=']:
171 v = cl[1].evaluate(depth=depth+1) == cl[2].evaluate(depth=depth+1)
172 if cl[0] == '!=': v = not v
174 v1 = self.evaluate(cl=cl[1], depth=depth+1)
177 if cl[0] == 'OR' and v1: return True
178 if cl[0] == 'AND' and not v1: return False
179 return self.evaluate(cl=cl[2], depth=depth+1)
180 def formula_string(self, formula):
181 "Create a string representation of a formula."
182 if formula[0] == 'AND':
183 return itertools.chain(['('], self.formula_string(formula[1]), [' and '], self.formula_string(formula[2]),[')'])
184 elif formula[0] == 'OR':
185 return itertools.chain(['('], self.formula_string(formula[1]), [' or '], self.formula_string(formula[2]),[')'])
186 elif formula[0] == 'NOT':
187 return itertools.chain(['(not '], self.formula_string(formula[1]),[')'])
188 elif formula[0] in ['==', '!=']:
189 return itertools.chain(formula[1], formula[0], formula[2])
190 return iter(['<invalid formula>'])
191 def str(self, parents=False):
192 "Retur the defining expression, if `parents` set, then prefixed with parent conditions."
193 if parents and self.parent:
194 return self.parent.str(parents=True) + u' && ' + self.name
197 return self.str(parents=False)
199 class Operation(object):
200 "Helper class for operation data. Must not be present in more variables or present multiple times."
201 def __init__(self, operation, condition, expression, level=0, source='?'):
202 # operation is currently 'SET' and 'APPEND'
203 self.operation = operation
204 self.condition = condition
205 self.expression = expression
209 return "%s <%d, %s> [%s] %r" % ( {'SET':'=', 'APPEND':'+'}[self.operation], self.level, self.source,
210 (self.condition and self.condition.str(parents=True)) or '', unicode(self.expression))
212 class ConfigVar(ConfigElem):
213 def __init__(self, name):
214 super(ConfigVar, self).__init__(name)
215 # Ordered list of `Operations` (ascending by `level`)
217 # Fixed to value (may be None)
220 self.fixed_val = None
222 "Return a set of variables used in the expressions"
223 return set(sum([ list(op.expression.variables()) for op in self.operations ], []))
224 def add_operation(self, operation):
226 Inserts an operation. The operations are sorted by `level` (ascending), new operation goes last among
227 these with the same level.
228 Adds the variable as a dependant of the conditions and variables used in the expressions.
230 # Invalidate cached value
233 pos = bisect.bisect_right([o.level for o in self.operations], operation.level)
234 self.operations.insert(pos, operation)
235 # Create dependencies
236 for v in operation.expression.variables():
237 v.dependants.add(self)
238 if operation.condition:
239 operation.condition.dependants.add(self)
240 def remove_operation(self, operation):
242 Remove the Operation.
243 Also removes the variable as dependant from all conditions and variables used in this
244 operation that are no longer used.
246 # Invalidate cached value
248 # Remove the operation
249 self.operations.remove(operation)
250 # Remove dependencies on variables unused in other operations
251 vs = self.variables()
252 for v in operation.expression.variables():
254 v.dependants.remove(self)
255 # Remove the dependency on the conditions (if not used in another operation)
256 if operation.condition and operation.condition not in [op.condition for op in self.operations]:
257 condition.dependants.remove(self)
258 def evaluate(self, depth=0):
260 Find the last 'SET' operation that applies and return the result of concatenating with all
261 subsequent applicable 'APPEND' operations. The result is the same as performing the operations
263 NOTE: undefined if some 'APPEND' apply but no 'SET' applies.
266 log.debug('evaluating var %r', self.name)
267 # List of strings to be concatenated
269 # Scan for last applicable expression - try each starting from the end, concatenate extensions
270 for i in range(len(self.operations)-1, -1, -1):
271 op = self.operations[i]
272 # Check the guarding condition
273 if (not op.condition) or op.condition.value(depth+1):
274 val.insert(0, op.expression.evaluate(depth=depth+1))
275 if op.operation == 'SET':
278 def dump(self, prefix=''):
280 Pretty printing of the variable. Includes all operations.
281 Returns iterator of lines (unicode strings).
283 # Try to evaluate the variable, but avoid undefined exceptions
286 v = self.value(depth=0)
289 yield prefix+u'%s = %r' % (self.name, v)
290 for op in self.operations:
291 #yield prefix+u' %s [%s] %s' % (op.operation, op.condition and op.condition.str(parents=True), op.expression)
292 yield prefix + u' ' + unicode(op)
294 class ConfigExpression(object):
296 String expression with some unexpanded config variables. Used in variable operations and conditions.
297 Expression is given as a list of unicode strings and ConfigVar variables to be expanded.
299 def __init__(self, exprlist, original = u'<unknown>'):
300 self.exprlist = exprlist
301 # Original defining string
302 self.original = original
303 # Replace strings with unicode
304 for i in range(len(self.exprlist)):
306 if isinstance(e, types.StringTypes):
307 if not isinstance(e, unicode):
308 self.exprlist[i] = unicode(e, 'ascii')
310 "Return an iterator of variables user in the expression"
311 return itertools.ifilter(lambda e: isinstance(e, ConfigVar), self.exprlist)
314 def evaluate(self, depth):
316 "Return unicode result of expansion of the variables."
318 for e in self.exprlist:
319 if isinstance(e, ConfigVar):
320 s.append(e.value(depth+1))
321 elif isinstance(e, unicode):
324 raise ConfigError('Invalid type %s in expression \'%s\'.'%(type(e), self))