2 Module for managing and evaluation of Moe configuration variables.
4 .. todo:: (OPT) Cleanup of unused undefined variables.
5 .. todo:: (OPT) Better variable name checking (no name '.'-structural prefix of another)
6 .. todo:: (OPT) Implemet "subtree" listing.
9 import types, itertools, re, bisect
12 from moe import MoeError
15 "Allowed depth of recursion - includes ALL recursive calls, so should quite high."
18 "Maximum attained depth of recursion - for debug/testing"
21 "Variable name regexp, dots (separators) must be separated from edges and each other."
22 re_VARNAME = re.compile(r'\A([A-Za-z0-9_-]+\.)*[A-Za-z0-9_-]+\Z')
25 def check_depth(depth):
26 "Helper to check for recursion depth."
28 if depth > c_maxdepth:
29 raise CyclicConfigError('Too deep recursion in config evaluation (cyclic substitution?)')
30 if depth > debug_maxdepth:
31 debug_maxdepth = depth
34 class ConfigError(MoeError):
35 "Base class for moe.config errors"
38 class UndefinedError(ConfigError):
39 "Raised when no **SET** operation applies to evaluated variable."
42 class VariableNameError(ConfigError):
43 "Raised on invalid config variable name."
46 class VariableFixedError(ConfigError):
47 "Raised when modifying a fixed variable"
50 class CyclicConfigError(ConfigError):
51 "Raised when evaluation recursion is too deep"
55 class ParseProxy(list):
56 """Proxy helper class around values returned by `parse` and `parse_file`,
57 useful in "with" constructs."""
58 def __init__(self, config, parsed_ops):
59 super(ParseProxy, self).__init__(parsed_ops)
63 def __exit__(self, etype, value, traceback):
64 self.config.remove(list(self))
67 class ConfigTree(object):
69 Configuration environment containing the variables.
75 def lookup(self, key, create = True):
77 Lookup and return a variable.
78 If not found and `create` set, check the name and transparently create a new one.
80 if key not in self.variables:
81 if not re_VARNAME.match(key):
82 raise VariableNameError('Invalid variable identifier %r in config', key)
84 raise UndefinedError('Config variable %r undefined.', key)
85 self.variables[key] = ConfigVar(key)
86 return self.variables[key]
88 def __getitem__(self, key):
90 Return the value of an existing variable.
92 return self.lookup(key, create=False).value()
94 def dump(self, prefix=''):
96 Pretty printing of the tree.
97 Returns an iterator of lines (strings).
99 return itertools.chain(*[
100 self.variables[k].dump(prefix) for k in sorted(self.variables.keys())
104 "Fix value of variable or list of variables. Fixing undefined variable raises `UndefinedError`."
105 if isinstance(keys, types.StringTypes):
108 self.lookup(key, create=False).fix()
110 def unfix(self, keys):
111 "Unfix value of variable or list of variables. Unfixing undefined variable raises `UndefinedError`."
112 if isinstance(keys, types.StringTypes):
115 self.lookup(key, create=False).unfix()
117 def remove(self, parsed):
118 """Given a list [(varname, `Operation`)] as returned by `parse` or `parse_file`,
119 removes the operations from the respective variables config tree.
120 Variables/operations not present int the tree raise ValueError.
122 for vname, o in parsed:
123 v = self.lookup(vname, create = True)
124 v.remove_operation(o)
126 def parse(self, s, source=None, level=0, proxy=True):
127 """Parse `s` (stream/string) into the tree, see `moe.config_parser.ConfigParser` for details.
128 Returns list of parset operations: [(varname, `Operation`)].
129 By default returns a proxy list-like object that can be used in "with" constructs:
131 with config.parse("TEST='1'"):
135 import moe.config_parser
136 p = moe.config_parser.ConfigParser(s, self, source=source, level=level)
140 return ParseProxy(self, l)
142 def parse_file(self, filename, desc=None, level=0, proxy=True):
143 """Parse an utf-8 file into the tree using func:`parse`.
144 Names the source "`filename` <`desc`>". """
145 with open(filename, 'rt') as f:
147 filename += " <" + desc + ">"
148 return self.parse(f, source=filename, level=level, proxy=proxy)
151 class ConfigElem(object):
153 Base class for cached config elements - variables and conditions
156 def __init__(self, name):
157 # Full name with separators, definition for conditions
159 # Vars and conditions depending on value of this one
160 self.dependants = set([])
161 # Cached value (may be None in case of evaluation error)
163 self.cached_val = None
165 def invalidate(self, depth=0):
167 Invalidate cached data and invalidate all dependants.
168 Does nothing if not cached.
172 log.debug('invalidating %s', self)
174 for d in self.dependants:
175 d.invalidate(depth + 1)
177 def value(self, depth=0):
178 "Caching helper calling self.evaluate(), returns a value or throws an exception."
181 self.cached_val = self.evaluate(depth=depth+1)
183 if self.cached_val == None:
184 raise UndefinedError("Unable to evaluate %r."%(self.name,))
185 return self.cached_val
191 class ConfigCondition(ConfigElem):
193 Condition using equality and logic operators.
194 Formula is a tuple-tree in the following recursive form::
196 ('AND', c1, c1), ('OR', c1, c2), ('NOT', c1), ('==', e1, e2), ('!=', e1, e2)
198 where ``e1``, ``e2`` are :class:`ConfigExpression`, ``c1``, ``c2``, :class:`ConfigCondition`.
201 def __init__(self, formula, text=None, parent=None):
203 Condition defined by `text` (informative), `formula` as in class definition,
204 `parent` is the parent condition (if any).
207 text = self.formula_string(formula)
208 super(ConfigCondition, self).__init__(text)
209 self.formula = formula
211 # Setup dependencies on used variables (not on the parent condition)
212 for v in self.variables():
213 v.dependants.add(self)
215 self.parent.dependants.add(self)
217 def variables(self, cl=None):
218 "Return an iterator of variables used in formula `cl`"
221 if cl[0] in ['==','!=']:
222 return itertools.chain(cl[1].variables(), cl[2].variables())
223 if cl[0] in ['AND','OR']:
224 return itertools.chain(self.variables(cl[1]), self.variables(cl[2]))
225 return self.variables(cl[1]) # only 'NOT' left
227 def remove_dependencies(self):
228 "Remove self as a dependant from all used variables"
229 for v in self.variables():
230 v.dependants.discard(self)
232 self.parent.dependants.discard(self)
234 def evaluate(self, cl=None, depth=0):
235 """Evaluate formula `cl` (or the entire condition).
236 Partial evaluation for AND and OR. Tests the parent condition first."""
240 if self.parent and not self.parent.value():
242 if cl[0] in ['==','!=']:
243 v = cl[1].evaluate(depth=depth+1) == cl[2].evaluate(depth=depth+1)
244 if cl[0] == '!=': v = not v
246 v1 = self.evaluate(cl=cl[1], depth=depth+1)
249 if cl[0] == 'OR' and v1: return True
250 if cl[0] == 'AND' and not v1: return False
251 return self.evaluate(cl=cl[2], depth=depth+1)
253 def formula_string(self, formula):
254 "Create a string representation of a formula."
255 if formula[0] == 'AND':
256 return itertools.chain(['('], self.formula_string(formula[1]), [' and '], self.formula_string(formula[2]),[')'])
257 elif formula[0] == 'OR':
258 return itertools.chain(['('], self.formula_string(formula[1]), [' or '], self.formula_string(formula[2]),[')'])
259 elif formula[0] == 'NOT':
260 return itertools.chain(['(not '], self.formula_string(formula[1]),[')'])
261 elif formula[0] in ['==', '!=']:
262 return itertools.chain(formula[1], formula[0], formula[2])
263 return iter(['<invalid formula>'])
265 def str(self, parents=False):
266 "Retur the defining expression, if `parents` set, then prefixed with parent conditions."
267 if parents and self.parent:
268 return self.parent.str(parents=True) + u' && ' + self.name
272 return self.str(parents=False)
275 class Operation(object):
277 Helper class for operation data. Must be present at most once in at most one variable.
279 ``operation`` is either ``"SET"`` or ``"APPEND"``, ``condition`` is a :class:`ConfigCondition` instance or ``None``,
280 ``expression`` is a :class:`ConfigExpression` instance, ``level`` is the priority of the operation and ``source``
281 is an informative string describing the operation origin.
284 def __init__(self, operation, condition, expression, level=0, source='?'):
285 self.operation = operation
286 self.condition = condition
287 self.expression = expression
292 return "%s <%d, %s> [%s] %r" % ( {'SET':'=', 'APPEND':'+'}[self.operation], self.level, self.source,
293 (self.condition and self.condition.str(parents=True)) or '', unicode(self.expression))
296 class ConfigVar(ConfigElem):
297 "Class representing a single configuration variable"
299 def __init__(self, name):
300 super(ConfigVar, self).__init__(name)
301 # Ordered list of `Operations` (ascending by `level`)
303 # Fixed to value (may be None)
305 self.fixed_val = None
308 "Return a set of variables used in the expressions of the operations"
309 if not self.operations:
311 return set.union(*[ op.expression.variables() for op in self.operations ])
315 Fixes the value of the variable. Exception is raised should the variable
316 evaluate to a different value while fixed.
320 self.fixed_val = self.value()
324 "Make the variable modifiable again."
327 def value(self, depth=0):
328 "Handle the case when fixed, raise exc. on different evaluation"
329 val = super(ConfigVar,self).value(depth)
330 if self.fixed and self.fixed_val != val:
331 raise VariableFixedError("value of var %r was fixed to %r but evaluated to %r", self.name, self.fixed_val, val)
334 def add_operation(self, operation):
336 Inserts an operation. The operations are sorted by `level` (ascending), new operation goes last among
337 these with the same level.
338 Adds the variable as a dependant of the conditions and variables used in the expressions.
340 # Invalidate cached value
343 pos = bisect.bisect_right([o.level for o in self.operations], operation.level)
344 self.operations.insert(pos, operation)
345 # Create dependencies
346 for v in operation.expression.variables():
347 v.dependants.add(self)
348 if operation.condition:
349 operation.condition.dependants.add(self)
351 def remove_operation(self, operation):
353 Remove the Operation.
354 Also removes the variable as dependant from all conditions and variables used in this
355 operation that are no longer used.
357 # Invalidate cached value
359 # Remove the operation
360 self.operations.remove(operation)
361 # Remove dependencies on variables unused in other defining operations
362 vs = self.variables()
363 for v in operation.expression.variables():
365 v.dependants.remove(self)
366 # Remove the dependency on the conditions (if not used in another operation)
367 if operation.condition and operation.condition not in [op.condition for op in self.operations]:
368 operation.condition.dependants.remove(self)
370 def evaluate(self, depth=0):
372 Find the last 'SET' operation that applies and return the result of concatenating with all
373 subsequent applicable 'APPEND' operations. The result is the same as performing the operations
375 NOTE: undefined if some 'APPEND' apply but no 'SET' applies.
378 log.debug('evaluating var %r', self.name)
379 # List of strings to be concatenated
381 # Scan for last applicable expression - try each starting from the end, concatenate extensions
382 for i in range(len(self.operations)-1, -1, -1):
383 op = self.operations[i]
384 # Check the guarding condition
385 if (not op.condition) or op.condition.value(depth+1):
386 val.insert(0, op.expression.evaluate(depth=depth+1))
387 if op.operation == 'SET':
391 def dump(self, prefix=''):
393 Pretty printing of the variable. Includes all operations.
394 Returns iterator of lines (unicode strings).
396 # Try to evaluate the variable, but avoid undefined exceptions
399 v = self.value(depth=0)
402 yield prefix+u'%s = %r' % (self.name, v)
403 for op in self.operations:
404 #yield prefix+u' %s [%s] %s' % (op.operation, op.condition and op.condition.str(parents=True), op.expression)
405 yield prefix + u' ' + unicode(op)
408 class ConfigExpression(object):
410 String expression with some unexpanded config variables. Used in variable operations and conditions.
411 Expression is given as a list of unicode strings and ConfigVar variables to be expanded.
414 def __init__(self, exprlist, original = u'<unknown>'):
415 self.exprlist = list(exprlist)
416 # Original defining string
417 self.original = original
418 # Replace strings with unicode
419 for i in range(len(self.exprlist)):
421 if isinstance(e, types.StringTypes):
422 if not isinstance(e, unicode):
423 self.exprlist[i] = unicode(e, 'ascii')
426 "Return a set of variables used in the expression"
427 return set([e for e in self.exprlist if isinstance(e, ConfigVar)])
432 def evaluate(self, depth):
434 "Return unicode result of expansion of the variables."
436 for e in self.exprlist:
437 if isinstance(e, ConfigVar):
438 s.append(e.value(depth+1))
439 elif isinstance(e, unicode):
442 raise ConfigError('Invalid type %s in expression \'%s\'.'%(type(e), self))