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
7This holds the classes that support the hierarchical variable management used
8in the cr tool to provide all the command configuration controls.
9"""
10
11import string
12
13import 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.
20GLOBALS = []
21
22
23class _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
42class _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
69class 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