Home | History | Annotate | Download | only in cr
      1 # Copyright 2013 The Chromium Authors. All rights reserved.
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 """Configuration variable management for the cr tool.
      6 
      7 This holds the classes that support the hierarchical variable management used
      8 in the cr tool to provide all the command configuration controls.
      9 """
     10 
     11 import string
     12 
     13 import cr.visitor
     14 
     15 _PARSE_CONSTANT_VALUES = [None, True, False]
     16 _PARSE_CONSTANTS = dict((str(value), value) for value in _PARSE_CONSTANT_VALUES)
     17 
     18 # GLOBALS is the singleton used to tie static global configuration objects
     19 # together.
     20 GLOBALS = []
     21 
     22 
     23 class _MissingToErrorFormatter(string.Formatter):
     24   """A string formatter used in value resolve.
     25 
     26   The main extra it adds is a new conversion specifier 'e' that throws a
     27   KeyError if it could not find the value.
     28   This allows a string value to use {A_KEY!e} to indicate that it is a
     29   formatting error if A_KEY is not present.
     30   """
     31 
     32   def convert_field(self, value, conversion):
     33     if conversion == 'e':
     34       result = str(value)
     35       if not result:
     36         raise KeyError('unknown')
     37       return result
     38     return super(_MissingToErrorFormatter, self).convert_field(
     39         value, conversion)
     40 
     41 
     42 class _Tracer(object):
     43   """Traces variable lookups.
     44 
     45   This adds a hook to a config object, and uses it to track all variable
     46   lookups that happen and add them to a trail. When done, it removes the hook
     47   again. This is used to provide debugging information about what variables are
     48   used in an operation.
     49   """
     50 
     51   def __init__(self, config):
     52     self.config = config
     53     self.trail = []
     54 
     55   def __enter__(self):
     56     self.config.fixup_hooks.append(self._Trace)
     57     return self
     58 
     59   def __exit__(self, *_):
     60     self.config.fixup_hooks.remove(self._Trace)
     61     self.config.trail = self.trail
     62     return False
     63 
     64   def _Trace(self, _, key, value):
     65     self.trail.append((key, value))
     66     return value
     67 
     68 
     69 class Config(cr.visitor.Node):
     70   """The main variable holding class.
     71 
     72   This holds a set of unresolved key value pairs, and the set of child Config
     73   objects that should be referenced when looking up a key.
     74   Key search is one in a pre-order traversal, and new children are prepended.
     75   This means parents override children, and the most recently added child
     76   overrides the rest.
     77 
     78   Values can be simple python types, callable dynamic values, or strings.
     79   If the value is a string, it is assumed to be a standard python format string
     80   where the root config object is used to resolve the keys. This allows values
     81   to refer to variables that are overriden in another part of the hierarchy.
     82   """
     83 
     84   @classmethod
     85   def From(cls, *args, **kwargs):
     86     """Builds an unnamed config object from a set of key,value args."""
     87     return Config('??').Apply(args, kwargs)
     88 
     89   @classmethod
     90   def If(cls, condition, true_value, false_value=''):
     91     """Returns a config value that selects a value based on the condition.
     92 
     93     Args:
     94         condition: The variable name to select a value on.
     95         true_value: The value to use if the variable is True.
     96         false_value: The value to use if the resolved variable is False.
     97     Returns:
     98         A dynamic value.
     99     """
    100     def Resolve(context):
    101       test = context.Get(condition)
    102       if test:
    103         value = true_value
    104       else:
    105         value = false_value
    106       return context.Substitute(value)
    107     return Resolve
    108 
    109   @classmethod
    110   def Optional(cls, value, alternate=''):
    111     """Returns a dynamic value that defaults to an alternate.
    112 
    113     Args:
    114         value: The main value to resolve.
    115         alternate: The value to use if the main value does not resolve.
    116     Returns:
    117         value if it resolves, alternate otherwise.
    118     """
    119     def Resolve(context):
    120       try:
    121         return context.Substitute(value)
    122       except KeyError:
    123         return context.Substitute(alternate)
    124     return Resolve
    125 
    126   def __init__(self, name='--', literal=False, export=None, enabled=True):
    127     super(Config, self).__init__(name=name, enabled=enabled, export=export)
    128     self._literal = literal
    129     self._formatter = _MissingToErrorFormatter()
    130     self.fixup_hooks = []
    131     self.trail = []
    132 
    133   @property
    134   def literal(self):
    135     return self._literal
    136 
    137   def Substitute(self, value):
    138     return self._formatter.vformat(str(value), (), self)
    139 
    140   def Resolve(self, visitor, key, value):
    141     """Resolves a value to it's final form.
    142 
    143     Raw values can be callable, simple values, or contain format strings.
    144     Args:
    145       visitor: The vistior asking to resolve a value.
    146       key: The key being visited.
    147       value: The unresolved value associated with the key.
    148     Returns:
    149       the fully resolved value.
    150     """
    151     error = None
    152     if callable(value):
    153       value = value(self)
    154     # Using existence of value.swapcase as a proxy for is a string
    155     elif hasattr(value, 'swapcase'):
    156       if not visitor.current_node.literal:
    157         try:
    158           value = self.Substitute(value)
    159         except KeyError as e:
    160           error = e
    161     return self.Fixup(key, value), error
    162 
    163   def Fixup(self, key, value):
    164     for hook in self.fixup_hooks:
    165       value = hook(self, key, value)
    166     return value
    167 
    168   @staticmethod
    169   def ParseValue(value):
    170     """Converts a string to a value.
    171 
    172     Takes a string from something like an environment variable, and tries to
    173     build an internal typed value. Recognizes Null, booleans, and numbers as
    174     special.
    175     Args:
    176         value: The the string value to interpret.
    177     Returns:
    178         the parsed form of the value.
    179     """
    180     if value in _PARSE_CONSTANTS:
    181       return _PARSE_CONSTANTS[value]
    182     try:
    183       return int(value)
    184     except ValueError:
    185       pass
    186     try:
    187       return float(value)
    188     except ValueError:
    189       pass
    190     return value
    191 
    192   def _Set(self, key, value):
    193     # early out if the value did not change, so we don't call change callbacks
    194     if value == self._values.get(key, None):
    195       return
    196     self._values[key] = value
    197     self.NotifyChanged()
    198     return self
    199 
    200   def ApplyMap(self, arg):
    201     for key, value in arg.items():
    202       self._Set(key, value)
    203     return self
    204 
    205   def Apply(self, args, kwargs):
    206     """Bulk set variables from arguments.
    207 
    208     Intended for internal use by the Set and From methods.
    209     Args:
    210         args: must be either a dict or something that can build a dict.
    211         kwargs: must be a dict.
    212     Returns:
    213         self for easy chaining.
    214     """
    215     if len(args) == 1:
    216       arg = args[0]
    217       if isinstance(arg, dict):
    218         self.ApplyMap(arg)
    219       else:
    220         self.ApplyMap(dict(arg))
    221     elif len(args) > 1:
    222       self.ApplyMap(dict(args))
    223     self.ApplyMap(kwargs)
    224     return self
    225 
    226   def Set(self, *args, **kwargs):
    227     return self.Apply(args, kwargs)
    228 
    229   def Trace(self):
    230     return _Tracer(self)
    231 
    232   def __getitem__(self, key):
    233     return self.Get(key)
    234 
    235   def __setitem__(self, key, value):
    236     self._Set(key, value)
    237 
    238   def __contains__(self, key):
    239     return self.Find(key) is not None
    240 
    241