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