| Index: swarm_client/utils/short_expression_finder.py
|
| ===================================================================
|
| --- swarm_client/utils/short_expression_finder.py (revision 235167)
|
| +++ swarm_client/utils/short_expression_finder.py (working copy)
|
| @@ -1,176 +0,0 @@
|
| -# Copyright 2013 The Chromium Authors. All rights reserved.
|
| -# Use of this source code is governed by a BSD-style license that can be
|
| -# found in the LICENSE file.
|
| -
|
| -"""Generates short, hopefully readable Python expressions that test variables
|
| -for certain combinations of values.
|
| -"""
|
| -
|
| -import itertools
|
| -import re
|
| -
|
| -
|
| -class ShortExpressionFinder(object):
|
| - """Usage:
|
| - >>> sef = ShortExpressionFinder([('foo', ("a", "b", "c", "d")),
|
| - ... ('bar', (1, 2))])
|
| - >>> sef.get_expr([("a", 1)])
|
| - foo=="a" and bar==1
|
| - >>> sef.get_expr([("a", 1), ("b", 2), ("c", 1)])
|
| - ((foo=="a" or foo=="c") and bar==1) or (foo=="b" and bar==2)
|
| -
|
| - The returned expressions are of the form
|
| - EXPR ::= EXPR2 ( "or" EXPR2 )*
|
| - EXPR2 ::= EXPR3 ( "and" EXPR3 )*
|
| - EXPR3 ::= VAR "==" VALUE ( "or" VAR "==" VALUE )*
|
| - where all of the comparisons in an EXPR2 involve the same variable.
|
| - Only positive tests are used so that all expressions will evaluate false if
|
| - given an unanticipated combination of values.
|
| -
|
| - A "cheapest" expression is returned. The cost of an expression is a function
|
| - of the number of var==value comparisons and the number of parentheses.
|
| -
|
| - The expression is found by exhaustive search, but it seems to be adequately
|
| - fast even for fairly large sets of configuration variables.
|
| - """
|
| -
|
| - # These must be positive integers.
|
| - COMPARISON_COST = 1
|
| - PAREN_COST = 1
|
| -
|
| - def __init__(self, variables_and_values):
|
| - assert variables_and_values
|
| - for k, vs in variables_and_values:
|
| - assert re.match(r'\w+\Z', k)
|
| - assert vs
|
| - assert (all(isinstance(v, int) for v in vs) or
|
| - all(re.match(r'\w+\Z', v) for v in vs))
|
| -
|
| - variables, values = zip(*((k, sorted(v)) for k, v in variables_and_values))
|
| - valuecounts = map(len, values)
|
| - base_tests_by_cost = {}
|
| -
|
| - # Loop over nonempty subsets of values of each variable. This is about 2^n
|
| - # cases where n is the total number of values (currently 2+3=5 in Chrome).
|
| - for subsets in itertools.product(*(range(1, 1 << n) for n in valuecounts)):
|
| - # Supposing values == [['a', 'b', 'c'], [1, 2]], there are six
|
| - # configurations: ('a', 1), ('a', 2), ('b', 1), etc. Each gets a bit, in
|
| - # that order starting from the LSB. Start with the equivalent of
|
| - # set([('a', 1)]) and massage that into the correct set of configs.
|
| - bits = 1
|
| - shift = 1
|
| - cost = 0
|
| - for subset, valuecount in zip(subsets, valuecounts):
|
| - oldbits, bits = bits, 0
|
| - while subset:
|
| - if subset & 1:
|
| - bits |= oldbits
|
| - cost += self.COMPARISON_COST
|
| - oldbits <<= shift
|
| - subset >>= 1
|
| - shift *= valuecount
|
| - # Charge an extra set of parens for the whole expression,
|
| - # which is removed later if appropriate.
|
| - cost += self.PAREN_COST * (1 + sum(bool(n & (n-1)) for n in subsets))
|
| - base_tests_by_cost.setdefault(cost, {})[bits] = subsets
|
| -
|
| - self.variables = variables
|
| - self.values = values
|
| - self.base_tests_by_cost = base_tests_by_cost
|
| -
|
| - def get_expr(self, configs):
|
| - assert configs
|
| - for config in configs:
|
| - assert len(config) == len(self.values)
|
| - assert all(val in vals for val, vals in zip(config, self.values))
|
| - return self._format_expr(self._get_expr_internal(configs))
|
| -
|
| - def _get_expr_internal(self, configs):
|
| - bits = 0
|
| - for config in configs:
|
| - bit = 1
|
| - n = 1
|
| - for value, values in zip(config, self.values):
|
| - bit <<= (n * values.index(value))
|
| - n *= len(values)
|
| - bits |= bit
|
| - notbits = ~bits
|
| -
|
| - def try_partitions(parts, bits):
|
| - for cost, subparts in parts:
|
| - if cost is None:
|
| - return None if bits else ()
|
| - try:
|
| - tests = self.base_tests_by_cost[cost]
|
| - except KeyError:
|
| - continue
|
| - for test in tests:
|
| - if (test & bits) and not (test & notbits):
|
| - result = try_partitions(subparts, bits & ~test)
|
| - if result is not None:
|
| - return (tests[test],) + result
|
| - return None
|
| -
|
| - for total_cost in itertools.count(0):
|
| - try:
|
| - return (self.base_tests_by_cost[total_cost + self.PAREN_COST][bits],)
|
| - except KeyError:
|
| - result = try_partitions(tuple(partitions(total_cost, self.PAREN_COST)),
|
| - bits)
|
| - if result is not None:
|
| - return result
|
| -
|
| - def _format_expr(self, expr):
|
| - out = []
|
| - for expr2 in expr:
|
| - out2 = []
|
| - for name, values, expr3 in zip(self.variables, self.values, expr2):
|
| - out3 = []
|
| - for value in values:
|
| - if expr3 & 1:
|
| - if isinstance(value, basestring):
|
| - value = '"%s"' % value
|
| - out3.append('%s==%s' % (name, value))
|
| - expr3 >>= 1
|
| - out2.append(' or '.join(out3))
|
| - if len(out3) > 1 and len(expr2) > 1:
|
| - out2[-1] = '(%s)' % out2[-1]
|
| - out.append(' and '.join(out2))
|
| - if len(out2) > 1 and len(expr) > 1:
|
| - out[-1] = '(%s)' % out[-1]
|
| - return ' or '.join(out)
|
| -
|
| -
|
| -def partitions(n, minimum):
|
| - """Yields all the ways of expressing n as a sum of integers >= minimum,
|
| - in a slightly odd tree format. Most of the tree is left unevaluated.
|
| - Example:
|
| - partitions(4, 1) ==>
|
| - [1, <[1, <[1, <[1, <end>]>],
|
| - [2, <end>]>],
|
| - [3, <end>]>],
|
| - [2, <[2, <end>]>],
|
| - [4, <end>]
|
| - where <...> is a lazily-evaluated list and end == [None, None].
|
| - """
|
| - if n == 0:
|
| - yield (None, None)
|
| - for k in range(n, minimum - 1, -1):
|
| - children = partitions(n - k, k)
|
| - # We could just yield [k, children] here, but that would create a lot of
|
| - # blind alleys with no actual partitions.
|
| - try:
|
| - yield [k, MemoizedIterable(itertools.chain([next(children)], children))]
|
| - except StopIteration:
|
| - pass
|
| -
|
| -
|
| -class MemoizedIterable(object):
|
| - """Wrapper for an iterable that fully evaluates and caches the values the
|
| - first time it is iterated over.
|
| - """
|
| - def __init__(self, iterable):
|
| - self.iterable = iterable
|
| - def __iter__(self):
|
| - self.iterable = tuple(self.iterable)
|
| - return iter(self.iterable)
|
|
|