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