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