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, cr.loader.AutoExport):
     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(base):
    101       test = base.Get(condition)
    102       if test:
    103         value = true_value
    104       else:
    105         value = false_value
    106       return base.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(base):
    120       try:
    121         return base.Substitute(value)
    122       except KeyError:
    123         return base.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 visitor 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   def Missing(self, key):
    169     for hook in self.fixup_hooks:
    170       hook(self, key, None)
    171     raise KeyError(key)
    172 
    173   @staticmethod
    174   def ParseValue(value):
    175     """Converts a string to a value.
    176 
    177     Takes a string from something like an environment variable, and tries to
    178     build an internal typed value. Recognizes Null, booleans, and numbers as
    179     special.
    180     Args:
    181         value: The the string value to interpret.
    182     Returns:
    183         the parsed form of the value.
    184     """
    185     if value in _PARSE_CONSTANTS:
    186       return _PARSE_CONSTANTS[value]
    187     try:
    188       return int(value)
    189     except ValueError:
    190       pass
    191     try:
    192       return float(value)
    193     except ValueError:
    194       pass
    195     return value
    196 
    197   def _Set(self, key, value):
    198     # early out if the value did not change, so we don't call change callbacks
    199     if value == self._values.get(key, None):
    200       return
    201     self._values[key] = value
    202     self.NotifyChanged()
    203     return self
    204 
    205   def ApplyMap(self, arg):
    206     for key, value in arg.items():
    207       self._Set(key, value)
    208     return self
    209 
    210   def Apply(self, args, kwargs):
    211     """Bulk set variables from arguments.
    212 
    213     Intended for internal use by the Set and From methods.
    214     Args:
    215         args: must be either a dict or something that can build a dict.
    216         kwargs: must be a dict.
    217     Returns:
    218         self for easy chaining.
    219     """
    220     if len(args) == 1:
    221       arg = args[0]
    222       if isinstance(arg, dict):
    223         self.ApplyMap(arg)
    224       else:
    225         self.ApplyMap(dict(arg))
    226     elif len(args) > 1:
    227       self.ApplyMap(dict(args))
    228     self.ApplyMap(kwargs)
    229     return self
    230 
    231   def Set(self, *args, **kwargs):
    232     return self.Apply(args, kwargs)
    233 
    234   def Trace(self):
    235     return _Tracer(self)
    236 
    237   def __getitem__(self, key):
    238     return self.Get(key)
    239 
    240   def __setitem__(self, key, value):
    241     self._Set(key, value)
    242 
    243   def __contains__(self, key):
    244     return self.Find(key) is not None
    245