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, 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