1 import types, itertools, re
5 Lazy conditional string evaluation module for configuration variables.
8 * Each variable has ordered list of operations (definitions), each SETs or APPENDs an expression
9 to the value. Each operation may be guarded by condition.
11 NOTE: Variable is undefined even if some 'APPEND' apply but no 'SET' applies. This might change.
13 * Each condition is a formula (tree consisting of 'AND', 'OR', 'NOT' and '==', '!=' between
16 * Expression is a list of strings and variables to be expanded.
18 NOTE: All expanded data should be (or is converted to) unicode
20 TODO: Cleanup of unused undefined variables.
21 TODO: Better variable name checking (no name '.'-structural prefix of another)
22 TODO: Implemet "subtree" listing.
23 TODO: Test conditions and unicode
32 "Variable name regexp, dots (separators) must be separated from edges and each other."
33 re_key = re.compile(r'\A([A-Za-z0-9_-]+\.)*[A-Za-z0-9_-]+\Z')
35 "Allowed depth of recursion -- includes ALL recursive calls, so should quite high."
38 "Maximum attained depth of recursion"
41 def check_depth(depth):
42 "Helper to check for recursion depth."
44 if depth > c_maxdepth:
45 raise ConfigError('Too deep recursion in config evaluation (cyclic substitution?)')
46 if depth > debug_maxdepth:
47 debug_maxdepth = depth
50 class ConfigError(Exception):
54 class ConfigTree(object):
56 Configuration tree containing all the variables.
58 The variables in `self.variables` are referenced directly by the full name.
62 def lookup(self, key, create = True):
64 Lookup and return a variable.
65 If not found and `create` set, check the name and transparently create a new one.
67 if not key in self.variables:
69 raise ConfigError('Config variable %r undefined.', key)
70 if not re_key.match(key):
71 raise ConfigError('Invalid variable identifier %r in config', key)
72 self.variables[key] = ConfigVar(key)
73 return self.variables[key]
74 def dump(self, prefix=''):
76 Pretty printing of the tree.
77 Returns an iterator of lines (strings).
79 return itertools.chain(*[
80 self.variables[k].dump(prefix) for k in sorted(self.variables.keys())
83 class ConfigElem(object):
85 Base class for cahed config elements - variables and conditions
87 def __init__(self, name):
88 # Full name with separators, definition for conditions
90 # Vars and conditions depending on value of this one
91 self.dependants = set([])
92 # Cached value (may be None in case of evaluation error)
94 self.cached_val = None
95 def invalidate(self, depth=0):
97 Invalidate cached data and invalidate all dependants.
98 Does nothing if not cached.
102 log.debug('invalidating %r', self.name)
104 for d in self.dependants:
105 d.invalidate(depth + 1)
106 def value(self, depth=0):
107 "Caching helper calling self.evaluate(), returns a value or throws an exception."
110 self.cached_val = self.evaluate(depth+1)
112 if self.cached_val == None:
113 raise ConfigError("Unable to evaluate %r."%(self.name,))
114 return self.cached_val
118 class ConfigCondition(ConfigElem):
120 Condition using equality and logic operators.
121 Clause is a tuple-tree in the following recursive form:
122 ('AND', c1, c1), ('OR', c1, c2), ('NOT', c1),
123 ('==', e1, e2), ('!=', e1, e2) where e1, e2 are `ConfigExpression`s.
125 def __init__(self, text, clause, parent=None):
127 Condition defined by `text` (informative), `clause` as in class definition,
128 `parent` is the parent condition (if any).
130 super(ConfigVar, self).__init__(text)
133 # Setup dependencies on used variables (not on the parent condition)
134 for v in self.variables():
135 v.dependants.add(self)
137 self.parent.dependants.add(self)
138 def variables(self, cl=None):
139 "Return an iterator of variables used in clause `cl`"
142 if cl[0] in ['==','!=']:
143 return itertools.chain(cl[1].variables(), cl[2].variables())
144 if cl[0] in ['AND','OR']:
145 return itertools.chain(self.variables(cl[1]), self.variables(cl[2]))
146 return self.variables(cl[1]) # only 'NOT' left
147 def remove_dependencies(self):
148 "Remove self as a dependant from all used variables"
149 for v in self.variables():
150 v.dependants.discard(self)
152 self.parent.dependants.discard(self)
153 def evaluate(self, cl=None, depth=0):
154 """Evaluate clause `cl` (or the entire condition).
155 Partial evaluation for AND and OR. Tests the parent condition first."""
159 if self.parent and not self.parent.value():
161 if cl[0] in ['==','!=']:
162 v = cl[1].evaluate(depth+1) == cl[2].evaluate(depth+1)
163 if cl[0] == '!=': v = not v
165 v1 = self.evaluate(cl[1], depth+1)
168 if cl[0] == 'OR' and v1: return True
169 if cl[0] == 'AND' and not v1: return False
170 return self.evaluate(cl[2], depth+1)
171 def str(self, parents=False):
172 "Retur the defining expression, if `parents` set, then prefixed with parent conditions."
173 if parents and self.parent:
174 return self.parent.str(parents=True) + u' && ' + self.name
177 return self.str(parents=False)
179 class ConfigVar(ConfigElem):
180 def __init__(self, name):
181 super(ConfigVar, self).__init__(name)
182 # Ordered list of operations
183 # (operation, condition, expression)
184 # operation is currently 'SET' and 'APPEND'
186 # Fixed to value (may be None) # TODO
188 self.fixed_val = None
190 "Return a set of variables used in the expressions"
191 return set(sum([ list(e[2].variables()) for e in self.operations ], []))
192 def add_operation(self, operation, condition, expression, index=None):
194 Inserts a new operation to position `index` (`None` appends).
195 Adds the variable as a dependant of the conditions and variables used in the expressions.
197 # Invalidate cached value
200 expr = (operation, condition, expression)
202 self.operations.insert(index, expr)
204 self.operations.append(expr)
205 # Create dependencies
206 for v in expression.variables():
207 v.dependants.add(self)
209 condition.dependants.add(self)
210 def remove_operation(self, index):
212 Remove the operation at given index.
213 Also removes the variable as dependant from all conditions and variables used in this
214 operation that are no longer used.
216 # Invalidate cached value
218 # Remove the operation
219 operation, condition, expression = self.operations[index]
220 self.operations.pop(index)
221 # Remove dependencies on variables unused in other operations
222 vs = self.variables()
223 for v in expression.variables():
225 v.dependants.remove(self)
226 # Remove the dependency on the conditions (if not used in another operation)
227 if condition and condition not in [e[1] for e in self.operations]:
228 condition.dependants.remove(self)
229 def evaluate(self, depth=0):
231 Find the last 'SET' operation that applies and return the result of concatenating with all
232 subsequent applicable 'APPEND' operations. The result is the same as performing the operations
234 NOTE: undefined if some 'APPEND' apply but no 'SET' applies.
237 log.debug('evaluating var %r', self.name)
238 # List of strings to be concatenated
240 # Scan for last applicable expression - try each starting from the end, concatenate extensions
241 for i in range(len(self.operations)-1, -1, -1):
242 operation, condition, expr = self.operations[i]
243 # Check the guarding condition
244 if (not condition) or condition.value(depth+1):
245 val.insert(0, expr.evaluate(depth+1))
246 if operation == 'SET':
249 def dump(self, prefix=''):
251 Pretty printing of the variable. Includes all operations.
252 Returns iterator of lines (unicode strings).
254 # Try to evaluate the variable, but avoid undefined exceptions
257 v = self.value(depth=0)
260 yield prefix+u'%s = %r' % (self.name, v)
261 for operation, condition, expr in self.operations:
262 yield prefix+u' %s [%s] %s' % (operation, condition and condition.str(parents=True), expr)
264 class ConfigExpression(object):
266 String expression with some unexpanded config variables. Used in variable operations and conditions.
267 Expression is given as a list of unicode strings and ConfigVar variables to be expanded.
269 def __init__(self, exprlist, original = u'<unknown>'):
270 self.exprlist = exprlist
271 # Original defining string
272 self.original = original
273 # Replace strings with unicode
274 for i in range(len(self.exprlist)):
276 if isinstance(e, types.StringTypes):
277 if not isinstance(e, unicode):
278 self.exprlist[i] = unicode(e, 'ascii')
280 "Return an iterator of variables user in the expression"
281 return itertools.ifilter(lambda e: isinstance(e, ConfigVar), self.exprlist)
284 def evaluate(self, depth):
286 "Return unicode result of expansion of the variables."
288 for e in self.exprlist:
289 if isinstance(e, ConfigVar):
290 s.append(e.value(depth+1))
291 elif isinstance(e, unicode):
294 raise ConfigError('Invalid type %s in expression \'%s\'.'%(type(e), self))