chromium/tools/cr/cr/config.py

# Copyright 2013 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Configuration variable management for the cr tool.

This holds the classes that support the hierarchical variable management used
in the cr tool to provide all the command configuration controls.
"""

import string

import cr.visitor

_PARSE_CONSTANT_VALUES = [None, True, False]
_PARSE_CONSTANTS = dict((str(value), value) for value in _PARSE_CONSTANT_VALUES)

# GLOBALS is the singleton used to tie static global configuration objects
# together.
GLOBALS = []


class _MissingToErrorFormatter(string.Formatter):
  """A string formatter used in value resolve.

  The main extra it adds is a new conversion specifier 'e' that throws a
  KeyError if it could not find the value.
  This allows a string value to use {A_KEY!e} to indicate that it is a
  formatting error if A_KEY is not present.
  """

  def convert_field(self, value, conversion):
    if conversion == 'e':
      result = str(value)
      if not result:
        raise KeyError('unknown')
      return result
    return super(_MissingToErrorFormatter, self).convert_field(
        value, conversion)


class _Tracer(object):
  """Traces variable lookups.

  This adds a hook to a config object, and uses it to track all variable
  lookups that happen and add them to a trail. When done, it removes the hook
  again. This is used to provide debugging information about what variables are
  used in an operation.
  """

  def __init__(self, config):
    self.config = config
    self.trail = []

  def __enter__(self):
    self.config.fixup_hooks.append(self._Trace)
    return self

  def __exit__(self, *_):
    self.config.fixup_hooks.remove(self._Trace)
    self.config.trail = self.trail
    return False

  def _Trace(self, _, key, value):
    self.trail.append((key, value))
    return value


class Config(cr.visitor.Node, cr.loader.AutoExport):
  """The main variable holding class.

  This holds a set of unresolved key value pairs, and the set of child Config
  objects that should be referenced when looking up a key.
  Key search is one in a pre-order traversal, and new children are prepended.
  This means parents override children, and the most recently added child
  overrides the rest.

  Values can be simple python types, callable dynamic values, or strings.
  If the value is a string, it is assumed to be a standard python format string
  where the root config object is used to resolve the keys. This allows values
  to refer to variables that are overriden in another part of the hierarchy.
  """

  @classmethod
  def From(cls, *args, **kwargs):
    """Builds an unnamed config object from a set of key,value args."""
    return Config('??').Apply(args, kwargs)

  @classmethod
  def If(cls, condition, true_value, false_value=''):
    """Returns a config value that selects a value based on the condition.

    Args:
        condition: The variable name to select a value on.
        true_value: The value to use if the variable is True.
        false_value: The value to use if the resolved variable is False.
    Returns:
        A dynamic value.
    """
    def Resolve(base):
      test = base.Get(condition)
      if test:
        value = true_value
      else:
        value = false_value
      return base.Substitute(value)
    return Resolve

  @classmethod
  def Optional(cls, value, alternate=''):
    """Returns a dynamic value that defaults to an alternate.

    Args:
        value: The main value to resolve.
        alternate: The value to use if the main value does not resolve.
    Returns:
        value if it resolves, alternate otherwise.
    """
    def Resolve(base):
      try:
        return base.Substitute(value)
      except KeyError:
        return base.Substitute(alternate)
    return Resolve

  def __init__(self, name='--', literal=False, export=None, enabled=True):
    super(Config, self).__init__(name=name, enabled=enabled, export=export)
    self._literal = literal
    self._formatter = _MissingToErrorFormatter()
    self.fixup_hooks = []
    self.trail = []

  @property
  def literal(self):
    return self._literal

  def Substitute(self, value):
    return self._formatter.vformat(str(value), (), self)

  def Resolve(self, visitor, key, value):
    """Resolves a value to it's final form.

    Raw values can be callable, simple values, or contain format strings.
    Args:
      visitor: The visitor asking to resolve a value.
      key: The key being visited.
      value: The unresolved value associated with the key.
    Returns:
      the fully resolved value.
    """
    error = None
    if callable(value):
      value = value(self)
    # Using existence of value.swapcase as a proxy for is a string
    elif hasattr(value, 'swapcase'):
      if not visitor.current_node.literal:
        try:
          value = self.Substitute(value)
        except KeyError as e:
          error = e
    return self.Fixup(key, value), error

  def Fixup(self, key, value):
    for hook in self.fixup_hooks:
      value = hook(self, key, value)
    return value

  def Missing(self, key):
    for hook in self.fixup_hooks:
      hook(self, key, None)
    raise KeyError(key)

  @staticmethod
  def ParseValue(value):
    """Converts a string to a value.

    Takes a string from something like an environment variable, and tries to
    build an internal typed value. Recognizes Null, booleans, and numbers as
    special.
    Args:
        value: The the string value to interpret.
    Returns:
        the parsed form of the value.
    """
    if value in _PARSE_CONSTANTS:
      return _PARSE_CONSTANTS[value]
    try:
      return int(value)
    except ValueError:
      pass
    try:
      return float(value)
    except ValueError:
      pass
    return value

  def _Set(self, key, value):
    # early out if the value did not change, so we don't call change callbacks
    if value == self._values.get(key, None):
      return
    self._values[key] = value
    self.NotifyChanged()
    return self

  def ApplyMap(self, arg):
    for key, value in arg.items():
      self._Set(key, value)
    return self

  def Apply(self, args, kwargs):
    """Bulk set variables from arguments.

    Intended for internal use by the Set and From methods.
    Args:
        args: must be either a dict or something that can build a dict.
        kwargs: must be a dict.
    Returns:
        self for easy chaining.
    """
    if len(args) == 1:
      arg = args[0]
      if isinstance(arg, dict):
        self.ApplyMap(arg)
      else:
        self.ApplyMap(dict(arg))
    elif len(args) > 1:
      self.ApplyMap(dict(args))
    self.ApplyMap(kwargs)
    return self

  def Set(self, *args, **kwargs):
    return self.Apply(args, kwargs)

  def Trace(self):
    return _Tracer(self)

  def __getitem__(self, key):
    return self.Get(key)

  def __setitem__(self, key, value):
    self._Set(key, value)

  def __contains__(self, key):
    return self.Find(key) is not None