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
29 from confparser import VARNAME_re
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 VARNAME_re.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, text, clause, parent=None):
123 Condition defined by `text` (informative), `clause` as in class definition,
124 `parent` is the parent condition (if any).
126 super(ConfigVar, self).__init__(text)
129 # Setup dependencies on used variables (not on the parent condition)
130 for v in self.variables():
131 v.dependants.add(self)
133 self.parent.dependants.add(self)
134 def variables(self, cl=None):
135 "Return an iterator of variables used in clause `cl`"
138 if cl[0] in ['==','!=']:
139 return itertools.chain(cl[1].variables(), cl[2].variables())
140 if cl[0] in ['AND','OR']:
141 return itertools.chain(self.variables(cl[1]), self.variables(cl[2]))
142 return self.variables(cl[1]) # only 'NOT' left
143 def remove_dependencies(self):
144 "Remove self as a dependant from all used variables"
145 for v in self.variables():
146 v.dependants.discard(self)
148 self.parent.dependants.discard(self)
149 def evaluate(self, cl=None, depth=0):
150 """Evaluate clause `cl` (or the entire condition).
151 Partial evaluation for AND and OR. Tests the parent condition first."""
155 if self.parent and not self.parent.value():
157 if cl[0] in ['==','!=']:
158 v = cl[1].evaluate(depth+1) == cl[2].evaluate(depth+1)
159 if cl[0] == '!=': v = not v
161 v1 = self.evaluate(cl[1], depth+1)
164 if cl[0] == 'OR' and v1: return True
165 if cl[0] == 'AND' and not v1: return False
166 return self.evaluate(cl[2], depth+1)
167 def str(self, parents=False):
168 "Retur the defining expression, if `parents` set, then prefixed with parent conditions."
169 if parents and self.parent:
170 return self.parent.str(parents=True) + u' && ' + self.name
173 return self.str(parents=False)
175 class ConfigVar(ConfigElem):
176 def __init__(self, name):
177 super(ConfigVar, self).__init__(name)
178 # Ordered list of operations
179 # (operation, condition, expression)
180 # operation is currently 'SET' and 'APPEND'
182 # Fixed to value (may be None) # TODO
184 self.fixed_val = None
186 "Return a set of variables used in the expressions"
187 return set(sum([ list(e[2].variables()) for e in self.operations ], []))
188 def add_operation(self, operation, condition, expression, index=None):
190 Inserts a new operation to position `index` (`None` appends).
191 Adds the variable as a dependant of the conditions and variables used in the expressions.
193 # Invalidate cached value
196 expr = (operation, condition, expression)
198 self.operations.insert(index, expr)
200 self.operations.append(expr)
201 # Create dependencies
202 for v in expression.variables():
203 v.dependants.add(self)
205 condition.dependants.add(self)
206 def remove_operation(self, index):
208 Remove the operation at given index.
209 Also removes the variable as dependant from all conditions and variables used in this
210 operation that are no longer used.
212 # Invalidate cached value
214 # Remove the operation
215 operation, condition, expression = self.operations[index]
216 self.operations.pop(index)
217 # Remove dependencies on variables unused in other operations
218 vs = self.variables()
219 for v in expression.variables():
221 v.dependants.remove(self)
222 # Remove the dependency on the conditions (if not used in another operation)
223 if condition and condition not in [e[1] for e in self.operations]:
224 condition.dependants.remove(self)
225 def evaluate(self, depth=0):
227 Find the last 'SET' operation that applies and return the result of concatenating with all
228 subsequent applicable 'APPEND' operations. The result is the same as performing the operations
230 NOTE: undefined if some 'APPEND' apply but no 'SET' applies.
233 log.debug('evaluating var %r', self.name)
234 # List of strings to be concatenated
236 # Scan for last applicable expression - try each starting from the end, concatenate extensions
237 for i in range(len(self.operations)-1, -1, -1):
238 operation, condition, expr = self.operations[i]
239 # Check the guarding condition
240 if (not condition) or condition.value(depth+1):
241 val.insert(0, expr.evaluate(depth+1))
242 if operation == 'SET':
245 def dump(self, prefix=''):
247 Pretty printing of the variable. Includes all operations.
248 Returns iterator of lines (unicode strings).
250 # Try to evaluate the variable, but avoid undefined exceptions
253 v = self.value(depth=0)
256 yield prefix+u'%s = %r' % (self.name, v)
257 for operation, condition, expr in self.operations:
258 yield prefix+u' %s [%s] %s' % (operation, condition and condition.str(parents=True), expr)
260 class ConfigExpression(object):
262 String expression with some unexpanded config variables. Used in variable operations and conditions.
263 Expression is given as a list of unicode strings and ConfigVar variables to be expanded.
265 def __init__(self, exprlist, original = u'<unknown>'):
266 self.exprlist = exprlist
267 # Original defining string
268 self.original = original
269 # Replace strings with unicode
270 for i in range(len(self.exprlist)):
272 if isinstance(e, types.StringTypes):
273 if not isinstance(e, unicode):
274 self.exprlist[i] = unicode(e, 'ascii')
276 "Return an iterator of variables user in the expression"
277 return itertools.ifilter(lambda e: isinstance(e, ConfigVar), self.exprlist)
280 def evaluate(self, depth):
282 "Return unicode result of expansion of the variables."
284 for e in self.exprlist:
285 if isinstance(e, ConfigVar):
286 s.append(e.value(depth+1))
287 elif isinstance(e, unicode):
290 raise ConfigError('Invalid type %s in expression \'%s\'.'%(type(e), self))