]> mj.ucw.cz Git - moe.git/blob - t/moe/conf.py
New file for conf parser, move and update conf file syntax
[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, text, clause, parent=None):
122     """
123     Condition defined by `text` (informative), `clause` as in class definition, 
124     `parent` is the parent condition (if any).
125     """
126     super(ConfigVar, self).__init__(text)
127     self.clause = clause
128     self.parent = parent
129     # Setup dependencies on used variables (not on the parent condition)
130     for v in self.variables():
131       v.dependants.add(self)
132     if self.parent:
133       self.parent.dependants.add(self)
134   def variables(self, cl=None):
135     "Return an iterator of variables used in clause `cl`"
136     if not cl: 
137       cl = self.clause
138     if cl[0] in ['==','!=']:
139       return itertools.chain(cl[1].variables(), cl[2].variables())
140     if cl[0] in ['AND','OR']:
141       return itertools.chain(self.variables(cl[1]), self.variables(cl[2]))
142     return self.variables(cl[1]) # only 'NOT' left
143   def remove_dependencies(self):
144     "Remove self as a dependant from all used variables"
145     for v in self.variables():
146       v.dependants.discard(self)
147     if self.parent:
148       self.parent.dependants.discard(self)
149   def evaluate(self, cl=None, depth=0):
150     """Evaluate clause `cl` (or the entire condition).
151     Partial evaluation for AND and OR. Tests the parent condition first."""
152     check_depth(depth)
153     if not cl: 
154       cl = self.clause
155     if self.parent and not self.parent.value():
156       return False
157     if cl[0] in ['==','!=']:
158       v = cl[1].evaluate(depth+1) == cl[2].evaluate(depth+1)
159       if cl[0] == '!=': v = not v
160       return v
161     v1 = self.evaluate(cl[1], depth+1)
162     if cl[0] == 'NOT':
163       return not v1
164     if cl[0] == 'OR' and v1: return True
165     if cl[0] == 'AND' and not v1: return False
166     return self.evaluate(cl[2], depth+1)
167   def str(self, parents=False):
168     "Retur the defining expression, if `parents` set, then prefixed with parent conditions."
169     if parents and self.parent:
170       return self.parent.str(parents=True) + u' && ' + self.name
171     return self.name
172   def __str__(self):
173     return self.str(parents=False)
174
175 class ConfigVar(ConfigElem):
176   def __init__(self, name):
177     super(ConfigVar, self).__init__(name)
178     # Ordered list of operations
179     # (operation, condition, expression)
180     # operation is currently 'SET' and 'APPEND'
181     self.operations = []
182     # Fixed to value (may be None) # TODO
183     self.fixed = False
184     self.fixed_val = None
185   def variables(self):
186     "Return a set of variables used in the expressions"
187     return set(sum([ list(e[2].variables()) for e in self.operations ], []))
188   def add_operation(self, operation, condition, expression, index=None):
189     """
190     Inserts a new operation to position `index` (`None` appends).
191     Adds the variable as a dependant of the conditions and variables used in the expressions. 
192     """
193     # Invalidate cached value
194     self.invalidate()
195     # Add the operation 
196     expr = (operation, condition, expression)
197     if index:
198       self.operations.insert(index, expr)
199     else:
200       self.operations.append(expr)
201     # Create dependencies
202     for v in expression.variables():
203       v.dependants.add(self)
204     if condition:
205       condition.dependants.add(self)
206   def remove_operation(self, index):
207     """
208     Remove the operation at given index.
209     Also removes the variable as dependant from all conditions and variables used in this 
210     operation that are no longer used.
211     """
212     # Invalidate cached value
213     self.invalidate()
214     # Remove the operation 
215     operation, condition, expression =  self.operations[index] 
216     self.operations.pop(index)
217     # Remove dependencies on variables unused in other operations
218     vs = self.variables()
219     for v in expression.variables():
220       if v not in vs:
221         v.dependants.remove(self)
222     # Remove the dependency on the conditions (if not used in another operation)
223     if condition and condition not in [e[1] for e in self.operations]:
224       condition.dependants.remove(self)
225   def evaluate(self, depth=0):
226     """
227     Find the last 'SET' operation that applies and return the result of concatenating with all
228     subsequent applicable 'APPEND' operations. The result is the same as performing the operations 
229     first-to-last.
230     NOTE: undefined if some 'APPEND' apply but no 'SET' applies.
231     """
232     check_depth(depth)
233     log.debug('evaluating var %r', self.name)
234     # List of strings to be concatenated
235     val = []
236     # Scan for last applicable expression - try each starting from the end, concatenate extensions
237     for i in range(len(self.operations)-1, -1, -1):
238       operation, condition, expr = self.operations[i]
239       # Check the guarding condition
240       if (not condition) or condition.value(depth+1):
241         val.insert(0, expr.evaluate(depth+1))
242         if operation == 'SET':
243           return u''.join(val)
244     return None
245   def dump(self, prefix=''):
246     """
247     Pretty printing of the variable. Includes all operations.
248     Returns iterator of lines (unicode strings).
249     """
250     # Try to evaluate the variable, but avoid undefined exceptions 
251     v = None
252     try: 
253       v = self.value(depth=0)
254     except ConfigError: 
255       pass
256     yield prefix+u'%s = %r' % (self.name, v)
257     for operation, condition, expr in self.operations:
258       yield prefix+u'  %s [%s] %s' % (operation, condition and condition.str(parents=True), expr)
259
260 class ConfigExpression(object):
261   """
262   String expression with some unexpanded config variables. Used in variable operations and conditions.
263   Expression is given as a list of unicode strings and ConfigVar variables to be expanded.
264   """
265   def __init__(self, exprlist, original = u'<unknown>'):
266     self.exprlist = exprlist
267     # Original defining string 
268     self.original = original
269     # Replace strings with unicode
270     for i in range(len(self.exprlist)):
271       e = self.exprlist[i]
272       if isinstance(e, types.StringTypes):
273         if not isinstance(e, unicode):
274           self.exprlist[i] = unicode(e, 'ascii')
275   def variables(self):
276     "Return an iterator of variables user in the expression"
277     return itertools.ifilter(lambda e: isinstance(e, ConfigVar), self.exprlist)
278   def __str__(self):
279     return self.original
280   def evaluate(self, depth):
281     check_depth(depth)
282     "Return unicode result of expansion of the variables."
283     s = []
284     for e in self.exprlist:
285       if isinstance(e, ConfigVar):
286         s.append(e.value(depth+1))
287       elif isinstance(e, unicode):
288         s.append(e)
289       else:
290         raise ConfigError('Invalid type %s in expression \'%s\'.'%(type(e), self))
291     return u''.join(s)
292
293