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