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