| # Copyright 2013 The Chromium Authors. All rights reserved. |
| # 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 |