]> mj.ucw.cz Git - moe.git/blob - t/moe/conf.py
Changed clause->formula in ConfigCondition, added formula pretty-printing
[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
28 import logging as log
29 from confparser import VARNAME_re
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 VARNAME_re.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 ConfigVar(ConfigElem):
189   def __init__(self, name):
190     super(ConfigVar, self).__init__(name)
191     # Ordered list of operations
192     # (operation, condition, expression)
193     # operation is currently 'SET' and 'APPEND'
194     self.operations = []
195     # Fixed to value (may be None) # TODO
196     self.fixed = False
197     self.fixed_val = None
198   def variables(self):
199     "Return a set of variables used in the expressions"
200     return set(sum([ list(e[2].variables()) for e in self.operations ], []))
201   def add_operation(self, operation, condition, expression, index=None):
202     """
203     Inserts a new operation to position `index` (`None` appends).
204     Adds the variable as a dependant of the conditions and variables used in the expressions. 
205     """
206     # Invalidate cached value
207     self.invalidate()
208     # Add the operation 
209     expr = (operation, condition, expression)
210     if index:
211       self.operations.insert(index, expr)
212     else:
213       self.operations.append(expr)
214     # Create dependencies
215     for v in expression.variables():
216       v.dependants.add(self)
217     if condition:
218       condition.dependants.add(self)
219   def remove_operation(self, index):
220     """
221     Remove the operation at given index.
222     Also removes the variable as dependant from all conditions and variables used in this 
223     operation that are no longer used.
224     """
225     # Invalidate cached value
226     self.invalidate()
227     # Remove the operation 
228     operation, condition, expression =  self.operations[index] 
229     self.operations.pop(index)
230     # Remove dependencies on variables unused in other operations
231     vs = self.variables()
232     for v in expression.variables():
233       if v not in vs:
234         v.dependants.remove(self)
235     # Remove the dependency on the conditions (if not used in another operation)
236     if condition and condition not in [e[1] for e in self.operations]:
237       condition.dependants.remove(self)
238   def evaluate(self, depth=0):
239     """
240     Find the last 'SET' operation that applies and return the result of concatenating with all
241     subsequent applicable 'APPEND' operations. The result is the same as performing the operations 
242     first-to-last.
243     NOTE: undefined if some 'APPEND' apply but no 'SET' applies.
244     """
245     check_depth(depth)
246     log.debug('evaluating var %r', self.name)
247     # List of strings to be concatenated
248     val = []
249     # Scan for last applicable expression - try each starting from the end, concatenate extensions
250     for i in range(len(self.operations)-1, -1, -1):
251       operation, condition, expr = self.operations[i]
252       # Check the guarding condition
253       if (not condition) or condition.value(depth+1):
254         val.insert(0, expr.evaluate(depth+1))
255         if operation == 'SET':
256           return u''.join(val)
257     return None
258   def dump(self, prefix=''):
259     """
260     Pretty printing of the variable. Includes all operations.
261     Returns iterator of lines (unicode strings).
262     """
263     # Try to evaluate the variable, but avoid undefined exceptions 
264     v = None
265     try: 
266       v = self.value(depth=0)
267     except ConfigError: 
268       pass
269     yield prefix+u'%s = %r' % (self.name, v)
270     for operation, condition, expr in self.operations:
271       yield prefix+u'  %s [%s] %s' % (operation, condition and condition.str(parents=True), expr)
272
273 class ConfigExpression(object):
274   """
275   String expression with some unexpanded config variables. Used in variable operations and conditions.
276   Expression is given as a list of unicode strings and ConfigVar variables to be expanded.
277   """
278   def __init__(self, exprlist, original = u'<unknown>'):
279     self.exprlist = exprlist
280     # Original defining string 
281     self.original = original
282     # Replace strings with unicode
283     for i in range(len(self.exprlist)):
284       e = self.exprlist[i]
285       if isinstance(e, types.StringTypes):
286         if not isinstance(e, unicode):
287           self.exprlist[i] = unicode(e, 'ascii')
288   def variables(self):
289     "Return an iterator of variables user in the expression"
290     return itertools.ifilter(lambda e: isinstance(e, ConfigVar), self.exprlist)
291   def __str__(self):
292     return self.original
293   def evaluate(self, depth):
294     check_depth(depth)
295     "Return unicode result of expansion of the variables."
296     s = []
297     for e in self.exprlist:
298       if isinstance(e, ConfigVar):
299         s.append(e.value(depth+1))
300       elif isinstance(e, unicode):
301         s.append(e)
302       else:
303         raise ConfigError('Invalid type %s in expression \'%s\'.'%(type(e), self))
304     return u''.join(s)
305
306