]> mj.ucw.cz Git - moe.git/blob - t/moe/config.py
Fixed ConfigVar.variables in case of no operations
[moe.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
59 class ConfigTree(object):
60   """
61   Configuration tree containing all the variables.
62
63   The variables in `self.variables` are referenced directly by the full name.
64   """
65
66   def __init__(self):
67     self.variables = {}
68
69   def lookup(self, key, create = True):
70     """
71     Lookup and return a variable. 
72     If not found and `create` set, check the name and transparently create a new one.
73     """
74     if key not in self.variables:
75       if not re_VARNAME.match(key):
76         raise VariableNameError('Invalid variable identifier %r in config', key)
77       if not create:
78         raise UndefinedError('Config variable %r undefined.', key)
79       self.variables[key] = ConfigVar(key)
80     return self.variables[key]
81
82   def __getitem__(self, key):
83     """
84     Return the value of an existing variable.
85     """
86     return self.lookup(key, create=False).value()
87
88   def dump(self, prefix=''):
89     """
90     Pretty printing of the tree.
91     Returns an iterator of lines (strings).
92     """
93     return itertools.chain(*[
94       self.variables[k].dump(prefix) for k in sorted(self.variables.keys())
95       ])
96
97   def fix(self, keys):
98     "Fix value of variable or list of variables. Fixing undefined variable raises `UndefinedError`."
99     if isinstance(keys, types.StringTypes):
100       keys = [keys]
101     for key in keys:
102       self.lookup(key, create=True).fix()
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.config_parser
107     p = moe.config_parser.ConfigParser(s, 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     with open(filename, 'rt') as f:
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     
164     ('AND', c1, c1), ('OR', c1, c2), ('NOT', c1), ('==', e1, e2), ('!=', e1, e2) 
165     
166   where e1, e2 are `ConfigExpression`, c1, c2, `ConfigCondition`.
167   """
168
169   def __init__(self, formula, text=None, parent=None):
170     """
171     Condition defined by `text` (informative), `formula` as in class definition, 
172     `parent` is the parent condition (if any).
173     """
174     if not text:
175       text = self.formula_string(formula)
176     super(ConfigCondition, self).__init__(text)
177     self.formula = formula
178     self.parent = parent
179     # Setup dependencies on used variables (not on the parent condition)
180     for v in self.variables():
181       v.dependants.add(self)
182     if self.parent:
183       self.parent.dependants.add(self)
184
185   def variables(self, cl=None):
186     "Return an iterator of variables used in formula `cl`"
187     if not cl: 
188       cl = self.formula
189     if cl[0] in ['==','!=']:
190       return itertools.chain(cl[1].variables(), cl[2].variables())
191     if cl[0] in ['AND','OR']:
192       return itertools.chain(self.variables(cl[1]), self.variables(cl[2]))
193     return self.variables(cl[1]) # only 'NOT' left
194
195   def remove_dependencies(self):
196     "Remove self as a dependant from all used variables"
197     for v in self.variables():
198       v.dependants.discard(self)
199     if self.parent:
200       self.parent.dependants.discard(self)
201
202   def evaluate(self, cl=None, depth=0):
203     """Evaluate formula `cl` (or the entire condition).
204     Partial evaluation for AND and OR. Tests the parent condition first."""
205     check_depth(depth)
206     if not cl: 
207       cl = self.formula
208     if self.parent and not self.parent.value():
209       return False
210     if cl[0] in ['==','!=']:
211       v = cl[1].evaluate(depth=depth+1) == cl[2].evaluate(depth=depth+1)
212       if cl[0] == '!=': v = not v
213       return v
214     v1 = self.evaluate(cl=cl[1], depth=depth+1)
215     if cl[0] == 'NOT':
216       return not v1
217     if cl[0] == 'OR' and v1: return True
218     if cl[0] == 'AND' and not v1: return False
219     return self.evaluate(cl=cl[2], depth=depth+1)
220
221   def formula_string(self, formula):
222     "Create a string representation of a formula."
223     if formula[0] == 'AND':
224       return itertools.chain(['('], self.formula_string(formula[1]), [' and '], self.formula_string(formula[2]),[')'])
225     elif formula[0] == 'OR':
226       return itertools.chain(['('], self.formula_string(formula[1]), [' or '], self.formula_string(formula[2]),[')'])
227     elif formula[0] == 'NOT':
228       return itertools.chain(['(not '], self.formula_string(formula[1]),[')'])
229     elif formula[0] in ['==', '!=']:
230       return itertools.chain(formula[1], formula[0], formula[2])
231     return iter(['<invalid formula>'])
232
233   def str(self, parents=False):
234     "Retur the defining expression, if `parents` set, then prefixed with parent conditions."
235     if parents and self.parent:
236       return self.parent.str(parents=True) + u' && ' + self.name
237     return self.name
238
239   def __str__(self):
240     return self.str(parents=False)
241
242
243 class Operation(object):
244   "Helper class for operation data. Must not be present in more variables or present multiple times."
245
246   def __init__(self, operation, condition, expression, level=0, source='?'):
247     # operation is currently 'SET' and 'APPEND'
248     self.operation = operation
249     self.condition = condition
250     self.expression = expression
251     self.level = level
252     self.source = source
253
254   def __str__(self):
255     return "%s <%d, %s> [%s] %r" % ( {'SET':'=', 'APPEND':'+'}[self.operation], self.level, self.source, 
256       (self.condition and self.condition.str(parents=True)) or '', unicode(self.expression))
257
258
259 class ConfigVar(ConfigElem):
260
261   def __init__(self, name):
262     super(ConfigVar, self).__init__(name)
263     # Ordered list of `Operations` (ascending by `level`)
264     self.operations = []
265     # Fixed to value (may be None) 
266     self.fixed = False
267     self.fixed_val = None
268
269   def variables(self):
270     "Return a set of variables used in the expressions"
271     if not self.operations:
272       return set([])
273     return set.union(*[ op.expression.variables() for op in self.operations ])
274
275   def fix(self):
276     """
277     Fixes the value of the variable. Exception is raised should the variable
278     evaluate to a different value while fixed. 
279     """
280     if self.fixed: 
281       return 
282     self.fixed_val = self.value()
283     self.fixed = True
284
285   def unfix(self):
286     "Make the variable modifiable again."
287     self.fixed = False
288
289   def value(self, depth=0):
290     "Handle the case when fixed, raise exc. on different evaluation"
291     val = super(ConfigVar,self).value(depth)
292     if self.fixed and self.fixed_val != val:
293       raise VariableFixedError("value of var %r was fixed to %r but evaluated to %r", self.name, self.fixed_val, val)
294     return val
295
296   def add_operation(self, operation):
297     """
298     Inserts an operation. The operations are sorted by `level` (ascending), new operation goes last among
299     these with the same level.
300     Adds the variable as a dependant of the conditions and variables used in the expressions. 
301     """
302     # Invalidate cached value
303     self.invalidate()
304     # Add the operation 
305     pos = bisect.bisect_right([o.level for o in self.operations], operation.level)
306     self.operations.insert(pos, operation)
307     # Create dependencies
308     for v in operation.expression.variables():
309       v.dependants.add(self)
310     if operation.condition:
311       operation.condition.dependants.add(self)
312
313   def remove_operation(self, operation):
314     """
315     Remove the Operation.
316     Also removes the variable as dependant from all conditions and variables used in this 
317     operation that are no longer used. 
318     """
319     # Invalidate cached value
320     self.invalidate()
321     # Remove the operation 
322     self.operations.remove(operation)
323     # Remove dependencies on variables unused in other defining operations
324     vs = self.variables()
325     for v in operation.expression.variables():
326       if v not in vs:
327         v.dependants.remove(self)
328     # Remove the dependency on the conditions (if not used in another operation)
329     if operation.condition and operation.condition not in [op.condition for op in self.operations]:
330       operation.condition.dependants.remove(self)
331
332   def evaluate(self, depth=0):
333     """
334     Find the last 'SET' operation that applies and return the result of concatenating with all
335     subsequent applicable 'APPEND' operations. The result is the same as performing the operations 
336     first-to-last.
337     NOTE: undefined if some 'APPEND' apply but no 'SET' applies.
338     """
339     check_depth(depth)
340     log.debug('evaluating var %r', self.name)
341     # List of strings to be concatenated
342     val = []
343     # Scan for last applicable expression - try each starting from the end, concatenate extensions
344     for i in range(len(self.operations)-1, -1, -1):
345       op = self.operations[i]
346       # Check the guarding condition
347       if (not op.condition) or op.condition.value(depth+1):
348         val.insert(0, op.expression.evaluate(depth=depth+1))
349         if op.operation == 'SET':
350           return u''.join(val)
351     return None
352
353   def dump(self, prefix=''):
354     """
355     Pretty printing of the variable. Includes all operations.
356     Returns iterator of lines (unicode strings).
357     """
358     # Try to evaluate the variable, but avoid undefined exceptions 
359     v = None
360     try: 
361       v = self.value(depth=0)
362     except ConfigError: 
363       pass
364     yield prefix+u'%s = %r' % (self.name, v)
365     for op in self.operations:
366       #yield prefix+u'  %s [%s] %s' % (op.operation, op.condition and op.condition.str(parents=True), op.expression)
367       yield prefix + u'  ' + unicode(op)
368
369
370 class ConfigExpression(object):
371   """
372   String expression with some unexpanded config variables. Used in variable operations and conditions.
373   Expression is given as a list of unicode strings and ConfigVar variables to be expanded.
374   """
375
376   def __init__(self, exprlist, original = u'<unknown>'):
377     self.exprlist = list(exprlist)
378     # Original defining string 
379     self.original = original
380     # Replace strings with unicode
381     for i in range(len(self.exprlist)):
382       e = self.exprlist[i]
383       if isinstance(e, types.StringTypes):
384         if not isinstance(e, unicode):
385           self.exprlist[i] = unicode(e, 'ascii')
386
387   def variables(self):
388     "Return a set of variables used in the expression"
389     return set([e for e in self.exprlist if isinstance(e, ConfigVar)])
390
391   def __str__(self):
392     return self.original
393
394   def evaluate(self, depth):
395     check_depth(depth)
396     "Return unicode result of expansion of the variables."
397     s = []
398     for e in self.exprlist:
399       if isinstance(e, ConfigVar):
400         s.append(e.value(depth+1))
401       elif isinstance(e, unicode):
402         s.append(e)
403       else:
404         raise ConfigError('Invalid type %s in expression \'%s\'.'%(type(e), self))
405     return u''.join(s)
406
407