]> mj.ucw.cz Git - moe.git/blob - t/moe/conf.py
New module for config evaluation. Basic testing.
[moe.git] / t / moe / conf.py
1 import types, itertools
2 import logging as log
3
4 # ALL in unicode
5
6 c_tree_sep = u'.'
7 c_comment = u'\n'
8 c_open = u'{'
9 c_close = u'}'
10 c_if = u'if'
11 c_maxdepth = 42
12 c_pprint_indent = 2
13
14 class ConfigError(Exception):
15   pass
16
17 # WARNING: Currently, only appending to a variable results in undefined 
18
19 class ConfigTree(object):
20   """
21   Configuration subtree (namespace). Contains subtrees and variables.
22   """
23   def __init__(self, name, tree):
24     """
25     Create configuration subtree `name` under `tree`.
26     Use `name=None` and `tree=None` for the root tree.
27     """
28     # Name of the tree
29     self.name = name    
30     # Parent tree 
31     self.tree = tree
32     # Elements of this tree (variables and subtrees)
33     self.elements = {}
34     # Full name with separators (None for root tree)
35     if self.tree and self.tree.fullname:
36       self.fullname = self.tree.fullname + c_tree_sep + self.name
37     else:
38       self.fullname = self.name
39     if self.tree:
40       self.tree.add(self.name, self)
41   def add(self, name, element):
42     "Add an element (subtree or a variable)."
43     if self.elements.has_key(name):
44       raise ConfigError('Element %s already present in tree %s'%(name, self.fullname))
45     self.elements[name] = element
46   def remove(self, name):
47     "Remove an element (subtree or a variable)."
48     self.elements.pop(name)
49   def pprint(self, indent=0):
50     """
51     Pretty printing of the tree.
52     Returns list of lines (strings).
53     """
54     if self.name:
55       s = [u'%*sSubtree %s'%(indent, u'', self.name)]
56     else:
57       s = [u'%*sRoot tree'%(indent, u'')]
58     for e in sorted(self.elements.values()):
59       s.extend(e.pprint(indent + c_pprint_indent))
60     return s
61
62 class ConfigElem(object):
63   """
64   Base class for cahed config elements - variables and conditions
65   """
66   def __init__(self, name, tree):
67     # Name in the subtree, for conditions the defining text
68     self.name = name    
69     # Parent tree 
70     self.tree = tree
71     # Full name with separators (only informative for conditions)
72     if self.tree.fullname:
73       self.fullname = self.tree.fullname + c_tree_sep + self.name
74     else:
75       self.fullname = self.name
76     # Vars and conditions depending on value of this one
77     self.dependants = set([])
78     # Cached value (may be None in case of evaluation error)
79     self.cached = False
80     self.cached_val = None
81   def depth_check(self, depth):
82     """
83     Helper to check for recursion depth.
84     """
85     if depth > c_maxdepth:
86       raise ConfigError('Too deep recursion in config evaluation (cyclic substitution?)')
87   def invalidate(self, depth=0):
88     """
89     Invalidate cached data and invalidate all dependants. 
90     Does nothing if not cached.
91     """
92     self.depth_check(depth)
93     if self.cached:
94       log.debug('invalidating %r', self.fullname)
95       self.cached = False
96       for d in self.dependants:
97         d.invalidate(depth + 1)
98   def evaluate(self, depth=0):
99     """
100     Cached interface for get_value(), returns a value or throws an exception.
101     """
102     self.depth_check(depth)
103     if not self.cached:
104       self.cached_val = self.get_value(depth+1)
105       self.cached = True
106     if self.cached_val == None:
107       raise ConfigError("Unable to evaluate %r."%(self.fullname,))
108     return self.cached_val 
109   def get_value(self, depth=0):
110     raise ConfigError('get_value() not implemented')
111
112 class ConfigEqCondition(ConfigElem):
113   "Simple (in)equality condition"
114   def __init__(self, name, tree, parent_cond, ex1, ex2, equality=True):
115     super(ConfigVar, self).__init__(name, tree)
116     # Tuple of two compared expressions
117     self.expressions = (ex1, ex2)
118     # If true, test for equality, otherwise inequality
119     self.equality = equality
120     # Parent condition in the configuration tree (or None)
121     self.parent_cond = parent_cond
122     # Setup dependencies on used variables (not on the parent condition)
123     for e in self.expressions:
124       for v in e.variables():
125         v.dependants.add(self)
126   def remove_dependencies(self):
127     "Remove self as dependant from all dependencies"
128     for e in self.expressions:
129       for v in e.variables():
130         v.dependants.remove(self)
131   def get_value(self, depth=0):
132     "Evaluate the condition (not cached)"
133     v = (ex1.evaluate(depth+1) == ex2.evaluate(depth+1))
134     if self.equality:
135       return v
136     return not v
137   def __str__(self):
138     return self.name
139
140 class ConfigVar(ConfigElem):
141   def __init__(self, name, tree):
142     super(ConfigVar, self).__init__(name, tree)
143     # Ordered list of operations
144     # (operation, [condition], expression )
145     # operation is currently restricted to 'SET' and 'APPEND'
146     self.expressions = []
147     # Fixed to value (may bi None) 
148     self.fixed = False
149     self.fixed_val = None
150     # Register in a subtree 
151     if self.tree:
152       self.tree.add(name, self)
153   def variables(self):
154     "Return a set of variables used in the expressions"
155     return set(sum([ list(e[2].variables()) for e in self.expressions ], []))
156   def conditions(self):
157     "Return a set of conditions used in the expressions"
158     return set(sum([ e[1] for e in self.expressions ], []))
159   def add_operation(self, operation, conditions, expression, index=None):
160     """
161     Adds a new operation to the given index (None for appending).
162     Adds the variable as a dependant to the conditions and variables in the expressions. 
163     """
164     # First invalidate cached value
165     self.invalidate()
166     # Add the operation 
167     expr = (operation, conditions, expression)
168     if index:
169       self.expressions.insert(index, expr)
170     else:
171       self.expressions.append(expr)
172     # Create dependencies
173     for v in expression.variables():
174       v.dependants.add(self)
175     for c in conditions:
176       c.dependants.add(self)
177   def remove_operation(self, index):
178     """
179     Remove the operation at given index.
180     Also removes the variable as dependant from all conditions and variables used in this 
181     operation that are no longer used. NOTE: this may be slow.
182     """
183     # First invalidate cached value
184     self.invalidate()
185     # Remove the operation 
186     operation, conditions, expression =  self.expressions[index] 
187     self.expressions.pop(index)
188     # Remove obsolete dependencies on variables
189     vs = self.variables()
190     for v in expression.variables():
191       if not v in vs:
192         v.dependants.remove(self)
193     # Remove obsolete dependencies on variables
194     cs = self.conditions()
195     for c in conditions:
196       if not c in cs():
197         c.dependants.remove(self)
198   def get_value(self, depth=0):
199     log.debug('evaluating var %r', self.fullname)
200     # List of strings to be concatenated
201     val = []
202     # Scan for last applicable expression - try each starting from the end, concatenate extensions
203     for i in range(len(self.expressions)-1, -1, -1):
204       operation, conditions, expr = self.expressions[i]
205       # Check all conditions guarding an expression 
206       if all([c.evaluate(depth+1) for c in conditions]):
207         val.insert(0, expr.evaluate(depth+1))
208         if operation == 'SET':
209           return u''.join(val)
210     return None
211   def pprint(self, indent=0):
212     """
213     Pretty printing of the variable. Includes all operations.
214     Returns iterator of lines (unicode strings).
215     """
216     yield u'%*s%s = \'%s\'' % (indent, u'', self.name, self.evaluate(0))
217     for operation, conditions, expr in self.expressions:
218       yield u'%*s%s %s %s' % (indent + c_pprint_indent, '', operation, conditions, expr)
219
220 class ConfigExpression(object):
221   """
222   String expression with some unexpanded config variables. Used in variable operations and conditions.
223   Expression is given as a list of unicode strings and ConfigVar variables to be expanded.
224   """
225   def __init__(self, exprlist, original = ''):
226     self.exprlist = exprlist
227     # Original defining string 
228     self.original = original
229     # Replace strings with 
230     for i in range(len(self.exprlist)):
231       e = self.exprlist[i]
232       if isinstance(e, types.StringTypes):
233         if not isinstance(e, unicode):
234           self.exprlist[i] = unicode(e, 'ascii')
235   def variables(self):
236     "Return an iterator of variables user in the expression"
237     return itertools.ifilter(lambda e: isinstance(e, ConfigVar), self.exprlist)
238   def __str__(self):
239     return self.original
240   def evaluate(self, depth):
241     "Return a unicode result of expansion of the variables."
242     s = []
243     for e in self.exprlist:
244       if isinstance(e, ConfigVar):
245         s.append(e.evaluate(depth+1))
246       elif isinstance(e, unicode):
247         s.append(e)
248       else:
249         raise ConfigError('Unsupported type %s in expression \'%s\'.'%(type(e), self))
250     return u''.join(s)
251
252