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