]> mj.ucw.cz Git - eval.git/blob - t/moe/config.py
813976c3980f756bc41306e769b6520d4e0cb523
[eval.git] / t / moe / config.py
1 """
2 config.py
3 -------
4
5 Lazy conditional string evaluation module for Moe configuration variables.
6
7
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). 
10
11 NOTE: If no 'SET' applies, a variable is still undefined even if some 'APPEND' applies. This might change.
12
13 * Each condition is a formula (tree consisting of 'AND', 'OR', 'NOT' and '==', '!=' between two expressions.
14
15 * Expression is a list of strings and variables to be expanded.
16
17 NOTE: All expanded data should be (or is converted to) unicode 
18
19
20 TODO (OPT): Cleanup of unused undefined variables.
21 TODO (OPT): Better variable name checking (no name '.'-structural prefix of another)
22 TODO (OPT): Implemet "subtree" listing.
23 """
24
25 import types, itertools, re, bisect
26 import logging as log
27
28 from moe import MoeError
29
30
31 "Allowed depth of recursion - includes ALL recursive calls, so should quite high."
32 c_maxdepth = 256
33
34 "Maximum attained depth of recursion - for debug/testing"
35 debug_maxdepth = 0 
36
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')
39
40
41 def check_depth(depth):
42   "Helper to check for recursion depth."
43   global debug_maxdepth
44   if depth > c_maxdepth:
45     raise CyclicConfigError('Too deep recursion in config evaluation (cyclic substitution?)')
46   if depth > debug_maxdepth:
47     debug_maxdepth = depth
48
49
50 class ConfigError(MoeError):
51   pass
52
53 class UndefinedError(ConfigError):
54   pass
55
56 class VariableNameError(ConfigError):
57   pass
58
59 class VariableFixedError(ConfigError):
60   pass
61
62 class CyclicConfigError(ConfigError):
63   pass
64
65
66 class ConfigTree(object):
67   """
68   Configuration tree containing all the variables.
69
70   The variables in `self.variables` are referenced directly by the full name.
71   """
72
73   def __init__(self):
74     self.variables = {}
75
76   def lookup(self, key, create = True):
77     """
78     Lookup and return a variable. 
79     If not found and `create` set, check the name and transparently create a new one.
80     """
81     if key not in self.variables:
82       if not re_VARNAME.match(key):
83         raise VariableNameError('Invalid variable identifier %r in config', key)
84       if not create:
85         raise UndefinedError('Config variable %r undefined.', key)
86       self.variables[key] = ConfigVar(key)
87     return self.variables[key]
88
89   def __getitem__(self, key):
90     """
91     Return the value of an existing variable.
92     """
93     return self.lookup(key, create=False).value()
94
95   def dump(self, prefix=''):
96     """
97     Pretty printing of the tree.
98     Returns an iterator of lines (strings).
99     """
100     return itertools.chain(*[
101       self.variables[k].dump(prefix) for k in sorted(self.variables.keys())
102       ])
103
104   def parse(self, s, source=None, level=0):
105     """Parse `s` (stream/string) into the tree, see `moe.confparser.ConfigParser` for details."""
106     import moe.confparser
107     p = moe.confparser.ConfigParser(text, self, source=source, level=level)
108     p.parse()
109
110   def parse_file(self, filename, desc=None, level=0):
111     """Parse an utf-8 file into the tree, see `moe.confparser.ConfigParser` for details. 
112     Names the source "`filename` <`desc`>". """
113     f = open(filename, 'rt')
114     if desc: 
115       filename += " <" + desc + ">" 
116     self.parse(f, source=filename, level=level)
117
118
119 class ConfigElem(object):
120   """
121   Base class for cahed config elements - variables and conditions
122   """
123
124   def __init__(self, name):
125     # Full name with separators, definition for conditions
126     self.name = name
127     # Vars and conditions depending on value of this one
128     self.dependants = set([])
129     # Cached value (may be None in case of evaluation error)
130     self.cached = False
131     self.cached_val = None
132
133   def invalidate(self, depth=0):
134     """
135     Invalidate cached data and invalidate all dependants. 
136     Does nothing if not cached.
137     """
138     check_depth(depth)
139     if self.cached:
140       log.debug('invalidating %s', self)
141       self.cached = False
142       for d in self.dependants:
143         d.invalidate(depth + 1)
144
145   def value(self, depth=0):
146     "Caching helper calling self.evaluate(), returns a value or throws an exception."
147     check_depth(depth)
148     if not self.cached:
149       self.cached_val = self.evaluate(depth=depth+1)
150       self.cached = True
151     if self.cached_val == None:
152       raise UndefinedError("Unable to evaluate %r."%(self.name,))
153     return self.cached_val 
154
155   def __str__(self):
156     return self.name
157
158
159 class ConfigCondition(ConfigElem):
160   """
161   Condition using equality and logic operators.
162   Clause is a tuple-tree in the following recursive form:
163   ('AND', c1, c1), ('OR', c1, c2), ('NOT', c1), 
164   ('==', e1, e2), ('!=', e1, e2) where e1, e2 are `ConfigExpression`s.
165   """
166
167   def __init__(self, formula, text=None, parent=None):
168     """
169     Condition defined by `text` (informative), `formula` as in class definition, 
170     `parent` is the parent condition (if any).
171     """
172     if not text:
173       text = self.formula_string(formula)
174     super(ConfigCondition, self).__init__(text)
175     self.formula = formula
176     self.parent = parent
177     # Setup dependencies on used variables (not on the parent condition)
178     for v in self.variables():
179       v.dependants.add(self)
180     if self.parent:
181       self.parent.dependants.add(self)
182
183   def variables(self, cl=None):
184     "Return an iterator of variables used in formula `cl`"
185     if not cl: 
186       cl = self.formula
187     if cl[0] in ['==','!=']:
188       return itertools.chain(cl[1].variables(), cl[2].variables())
189     if cl[0] in ['AND','OR']:
190       return itertools.chain(self.variables(cl[1]), self.variables(cl[2]))
191     return self.variables(cl[1]) # only 'NOT' left
192
193   def remove_dependencies(self):
194     "Remove self as a dependant from all used variables"
195     for v in self.variables():
196       v.dependants.discard(self)
197     if self.parent:
198       self.parent.dependants.discard(self)
199
200   def evaluate(self, cl=None, depth=0):
201     """Evaluate formula `cl` (or the entire condition).
202     Partial evaluation for AND and OR. Tests the parent condition first."""
203     check_depth(depth)
204     if not cl: 
205       cl = self.formula
206     if self.parent and not self.parent.value():
207       return False
208     if cl[0] in ['==','!=']:
209       v = cl[1].evaluate(depth=depth+1) == cl[2].evaluate(depth=depth+1)
210       if cl[0] == '!=': v = not v
211       return v
212     v1 = self.evaluate(cl=cl[1], depth=depth+1)
213     if cl[0] == 'NOT':
214       return not v1
215     if cl[0] == 'OR' and v1: return True
216     if cl[0] == 'AND' and not v1: return False
217     return self.evaluate(cl=cl[2], depth=depth+1)
218
219   def formula_string(self, formula):
220     "Create a string representation of a formula."
221     if formula[0] == 'AND':
222       return itertools.chain(['('], self.formula_string(formula[1]), [' and '], self.formula_string(formula[2]),[')'])
223     elif formula[0] == 'OR':
224       return itertools.chain(['('], self.formula_string(formula[1]), [' or '], self.formula_string(formula[2]),[')'])
225     elif formula[0] == 'NOT':
226       return itertools.chain(['(not '], self.formula_string(formula[1]),[')'])
227     elif formula[0] in ['==', '!=']:
228       return itertools.chain(formula[1], formula[0], formula[2])
229     return iter(['<invalid formula>'])
230
231   def str(self, parents=False):
232     "Retur the defining expression, if `parents` set, then prefixed with parent conditions."
233     if parents and self.parent:
234       return self.parent.str(parents=True) + u' && ' + self.name
235     return self.name
236
237   def __str__(self):
238     return self.str(parents=False)
239
240
241 class Operation(object):
242   "Helper class for operation data. Must not be present in more variables or present multiple times."
243
244   def __init__(self, operation, condition, expression, level=0, source='?'):
245     # operation is currently 'SET' and 'APPEND'
246     self.operation = operation
247     self.condition = condition
248     self.expression = expression
249     self.level = level
250     self.source = source
251
252   def __str__(self):
253     return "%s <%d, %s> [%s] %r" % ( {'SET':'=', 'APPEND':'+'}[self.operation], self.level, self.source, 
254       (self.condition and self.condition.str(parents=True)) or '', unicode(self.expression))
255
256
257 class ConfigVar(ConfigElem):
258
259   def __init__(self, name):
260     super(ConfigVar, self).__init__(name)
261     # Ordered list of `Operations` (ascending by `level`)
262     self.operations = []
263     # Fixed to value (may be None) 
264     self.fixed = False
265     self.fixed_val = None
266
267   def variables(self):
268     "Return a set of variables used in the expressions"
269     return set(sum([ list(op.expression.variables()) for op in self.operations ], []))
270
271   def fix(self):
272     """
273     Fixes the value of the variable. Exception is raised should the variable
274     evaluate to a different value while fixed. 
275     """
276     if self.fixed: 
277       return 
278     self.fixed_val = self.value()
279     self.fixed = True
280
281   def unfix(self):
282     "Set the variable to be modifiable again."
283     self.fixed = False
284
285   def value(self, depth=0):
286     "Handle the case when fixed, raise exc. on different evaluation"
287     val = super(ConfigVar,self).value(depth)
288     if self.fixed and self.fixed_val != val:
289       raise VariableFixedError("value of var %s was fixed to %r but evaluated to %r", self.name, self.fixed_val, val)
290     return val
291
292   def add_operation(self, operation):
293     """
294     Inserts an operation. The operations are sorted by `level` (ascending), new operation goes last among
295     these with the same level.
296     Adds the variable as a dependant of the conditions and variables used in the expressions. 
297     """
298     # Invalidate cached value
299     self.invalidate()
300     # Add the operation 
301     pos = bisect.bisect_right([o.level for o in self.operations], operation.level)
302     self.operations.insert(pos, operation)
303     # Create dependencies
304     for v in operation.expression.variables():
305       v.dependants.add(self)
306     if operation.condition:
307       operation.condition.dependants.add(self)
308
309   def remove_operation(self, operation):
310     """
311     Remove the Operation.
312     Also removes the variable as dependant from all conditions and variables used in this 
313     operation that are no longer used. 
314     """
315     # Invalidate cached value
316     self.invalidate()
317     # Remove the operation 
318     self.operations.remove(operation)
319     # Remove dependencies on variables unused in other operations
320     vs = self.variables()
321     for v in operation.expression.variables():
322       if v not in vs:
323         v.dependants.remove(self)
324     # Remove the dependency on the conditions (if not used in another operation)
325     if operation.condition and operation.condition not in [op.condition for op in self.operations]:
326       operation.condition.dependants.remove(self)
327
328   def evaluate(self, depth=0):
329     """
330     Find the last 'SET' operation that applies and return the result of concatenating with all
331     subsequent applicable 'APPEND' operations. The result is the same as performing the operations 
332     first-to-last.
333     NOTE: undefined if some 'APPEND' apply but no 'SET' applies.
334     """
335     check_depth(depth)
336     log.debug('evaluating var %r', self.name)
337     # List of strings to be concatenated
338     val = []
339     # Scan for last applicable expression - try each starting from the end, concatenate extensions
340     for i in range(len(self.operations)-1, -1, -1):
341       op = self.operations[i]
342       # Check the guarding condition
343       if (not op.condition) or op.condition.value(depth+1):
344         val.insert(0, op.expression.evaluate(depth=depth+1))
345         if op.operation == 'SET':
346           return u''.join(val)
347     return None
348
349   def dump(self, prefix=''):
350     """
351     Pretty printing of the variable. Includes all operations.
352     Returns iterator of lines (unicode strings).
353     """
354     # Try to evaluate the variable, but avoid undefined exceptions 
355     v = None
356     try: 
357       v = self.value(depth=0)
358     except ConfigError: 
359       pass
360     yield prefix+u'%s = %r' % (self.name, v)
361     for op in self.operations:
362       #yield prefix+u'  %s [%s] %s' % (op.operation, op.condition and op.condition.str(parents=True), op.expression)
363       yield prefix + u'  ' + unicode(op)
364
365
366 class ConfigExpression(object):
367   """
368   String expression with some unexpanded config variables. Used in variable operations and conditions.
369   Expression is given as a list of unicode strings and ConfigVar variables to be expanded.
370   """
371
372   def __init__(self, exprlist, original = u'<unknown>'):
373     self.exprlist = list(exprlist)
374     # Original defining string 
375     self.original = original
376     # Replace strings with unicode
377     for i in range(len(self.exprlist)):
378       e = self.exprlist[i]
379       if isinstance(e, types.StringTypes):
380         if not isinstance(e, unicode):
381           self.exprlist[i] = unicode(e, 'ascii')
382
383   def variables(self):
384     "Return an iterator of variables user in the expression"
385     return itertools.ifilter(lambda e: isinstance(e, ConfigVar), self.exprlist)
386
387   def __str__(self):
388     return self.original
389
390   def evaluate(self, depth):
391     check_depth(depth)
392     "Return unicode result of expansion of the variables."
393     s = []
394     for e in self.exprlist:
395       if isinstance(e, ConfigVar):
396         s.append(e.value(depth+1))
397       elif isinstance(e, unicode):
398         s.append(e)
399       else:
400         raise ConfigError('Invalid type %s in expression \'%s\'.'%(type(e), self))
401     return u''.join(s)
402
403