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