Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(216)

Unified Diff: third_party/recipe_engine/config.py

Issue 1347263002: Revert of Cross-repo recipe package system. (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/tools/build
Patch Set: Created 5 years, 3 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
« no previous file with comments | « third_party/recipe_engine/__init__.py ('k') | third_party/recipe_engine/config_types.py » ('j') | no next file with comments »
Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
Index: third_party/recipe_engine/config.py
diff --git a/third_party/recipe_engine/config.py b/third_party/recipe_engine/config.py
new file mode 100644
index 0000000000000000000000000000000000000000..693b129670fe34442fbcb407598320b61c84d4e4
--- /dev/null
+++ b/third_party/recipe_engine/config.py
@@ -0,0 +1,736 @@
+# Copyright 2013-2015 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.
+
+"""Recipe Configuration Meta DSL.
+
+This module contains, essentially, a DSL for writing composable configurations.
+You start by defining a schema which describes how your configuration blobs will
+be structured, and what data they can contain. For example:
+
+ FakeSchema = lambda main_val=True, mode='Happy': ConfigGroup(
+ config_group = ConfigGroup(
+ item_a = SimpleConfig(int),
+ item_b = DictConfig(),
+ ),
+ extra_setting = SetConfig(str),
+
+ MAIN_DETERMINANT = StaticConfig(main_val),
+ CONFIG_MODE = StaticConfig(mode),
+ )
+
+In short, a 'schema' is a callable which can take zero arguments (it can contain
+default arguments as well, for setting defaults, tweaking the schema, etc.), and
+returning a ConfigGroup.
+
+Every type used in the schema derives from ConfigBase. It has the general
+characteristics that it's a fixed-type container. It tends to impersonate the
+data type that it stores (so you can manipulate the config objects like normal
+python data), but also provides type checking and type conversion assistence
+(so you can easily render your configurations to JSON).
+
+Then you can create a configuration context:
+
+ config_ctx = config_item_context(FakeSchema)
+
+config_ctx is a python decorator which you can use to create composable
+configuration functions. For example:
+
+ @config_ctx()
+ def cool(c):
+ if c.CONFIG_MODE == 'Happy':
+ c.config_group.item_a = 100
+ else:
+ c.config_group.item_a = -100
+
+ @config_ctx()
+ def gnarly(c):
+ c.extra_setting = 'gnarly!'
+
+ @config_ctx(includes=('cool', 'gnarly'))
+ def combo(c):
+ if c.MAIN_DETERMINANT:
+ c.config_group.item_b['nickname'] = 'purple'
+ c.extra_setting += ' cows!'
+ else:
+ c.config_group.item_b['nickname'] = 'sad times'
+
+If I now call:
+
+ combo()
+
+I will get back a configuraton object whose schema is FakeSchema, and whose
+data is the accumulation of cool(), gnarly(), and combo(). I can continue to
+manipulate this configuraton object, use its data, or render it to json.
+
+Using this system should allow you to create rich, composible,
+modular configurations. See the documentation on config_item_context and the
+BaseConfig derivatives for more info.
+"""
+
+import collections
+import functools
+import types
+
+from infra.libs import infra_types
+
+class BadConf(Exception):
+ pass
+
+def typeAssert(obj, typearg):
+ if not isinstance(obj, typearg):
+ raise TypeError("Expected %r to be of type %r" % (obj, typearg))
+
+
+class ConfigContext(object):
+ """A configuration context for a recipe module.
+
+ Holds configuration schema and also acts as a config_ctx decorator.
+ A recipe module can define at most one such context.
+ """
+
+ def __init__(self, CONFIG_SCHEMA):
+ self.CONFIG_ITEMS = {}
+ self.MUTEX_GROUPS = {}
+ self.CONFIG_SCHEMA = CONFIG_SCHEMA
+ self.ROOT_CONFIG_ITEM = None
+
+ def __call__(self, group=None, includes=None, deps=None,
+ is_root=False, config_vars=None):
+ """
+ A decorator for functions which modify a given schema of configs.
+ Examples continue using the schema and config_items defined in the module
+ docstring.
+
+ This decorator provides a series of related functions:
+ * Any function decorated with this will be registered into this config
+ context by __name__. This enables some of the other following features
+ to work.
+ * Alters the signature of the function so that it can receive an extra
+ parameter 'final'. See the documentation for final on inner().
+ * Provides various convenience and error checking facilities.
+ * In particular, this decorator will prevent you from calling the same
+ config_ctx on a given config blob more than once (with the exception
+ of setting final=False. See inner())
+
+ Args:
+ group(str) - Using this decorator with the `group' argument causes the
+ decorated function to be a member of that group. Members of a group are
+ mutually exclusive on the same configuration blob. For example, only
+ one of these two functions could be applied to the config blob c:
+ @config_ctx(group='a')
+ def bob(c):
+ c.extra_setting = "bob mode"
+
+ @config_ctx(group='a')
+ def bill(c):
+ c.extra_setting = "bill mode"
+
+ includes(iterable(str)) - Any config items named in the includes list will
+ be run against the config blob before the decorated function can modify
+ it. If an inclusion is already applied to the config blob, it's skipped
+ without applying/raising BadConf. Example:
+ @config_ctx(includes=('bob', 'cool'))
+ def charlie(c):
+ c.config_group.item_b = 25
+ The result of this config_ctx (assuming default values for the schema)
+ would be:
+ {'config_group': { 'item_a': 100, 'item_b': 25 },
+ 'extra_setting': 'gnarly!'}
+
+ deps(iterable(str)) - One or more groups which must be satisfied before
+ this config_ctx can be applied to a config_blob. If you invoke
+ a config_ctx on a blob without having all of its deps satisfied,
+ you'll get a BadConf exception.
+
+ is_root(bool) - If set to True on an item, this item will become the
+ 'basis' item for all other configurations in this group. That means that
+ it will be implicitly included in all other config_items. There may only
+ ever be one root item.
+
+ config_vars(dict) - A dictionary mapping of { CONFIG_VAR: <value> }. This
+ sets the input contidions for the CONFIG_SCHEMA.
+
+ Returns a new decorated version of this function (see inner()).
+ """
+ def decorator(f):
+ name = f.__name__
+ @functools.wraps(f)
+ def inner(config=None, final=True, optional=False, **kwargs):
+ """This is the function which is returned from the config_ctx
+ decorator.
+
+ It applies all of the logic mentioned in the config_ctx docstring
+ above, and alters the function signature slightly.
+
+ Args:
+ config - The config blob that we intend to manipulate. This is passed
+ through to the function after checking deps and including includes.
+ After the function manipulates it, it is automatically returned.
+
+ final(bool) - Set to True by default, this will record the application
+ of this config_ctx to `config', which will prevent the config_ctx
+ from being applied to `config' again. It also is used to see if the
+ config blob satisfies deps for subsequent config_ctx applications
+ (i.e. in order for a config_ctx to satisfy a dependency, it must
+ be applied with final=True).
+
+ This is useful to apply default values while allowing the config to
+ later override those values.
+
+ However, it's best if each config_ctx is final, because then you
+ can implement the config items with less error checking, since you
+ know that the item may only be applied once. For example, if your
+ item appends something to a list, but is called with final=False,
+ you'll have to make sure that you don't append the item twice, etc.
+
+ **kwargs - Passed through to the decorated function without harm.
+
+ Returns config and ignores the return value of the decorated function.
+ """
+ if config is None:
+ config = self.CONFIG_SCHEMA()
+ assert isinstance(config, ConfigGroup)
+ inclusions = config._inclusions # pylint: disable=W0212
+
+ # inner.IS_ROOT will be True or False at the time of invocation.
+ if (self.ROOT_CONFIG_ITEM and not inner.IS_ROOT and
+ self.ROOT_CONFIG_ITEM.__name__ not in inclusions):
+ self.ROOT_CONFIG_ITEM(config)
+
+ if name in inclusions:
+ if optional:
+ return config
+ raise BadConf('config_ctx "%s" is already in this config "%s"' %
+ (name, config.as_jsonish(include_hidden=True)))
+ if final:
+ inclusions.add(name)
+
+ for include in includes or []:
+ if include in inclusions:
+ continue
+ try:
+ self.CONFIG_ITEMS[include](config)
+ except BadConf as e:
+ raise BadConf('config "%s" includes "%s", but [%s]' %
+ (name, include, e))
+
+ # deps are a list of group names. All groups must be represented
+ # in config already.
+ for dep_group in deps or []:
+ if not inclusions & self.MUTEX_GROUPS[dep_group]:
+ raise BadConf('dep group "%s" is unfulfilled for "%s"' %
+ (dep_group, name))
+
+ if group:
+ overlap = inclusions & self.MUTEX_GROUPS[group]
+ overlap.discard(name)
+ if overlap:
+ raise BadConf('"%s" is a member of group "%s", but %s already ran' %
+ (name, group, tuple(overlap)))
+
+ ret = f(config, **kwargs)
+ assert ret is None, 'Got return value (%s) from "%s"?' % (ret, name)
+
+ return config
+ inner.WRAPPED = f
+ inner.INCLUDES = includes or []
+
+ def default_config_vars():
+ ret = {}
+ for include in includes or []:
+ item = self.CONFIG_ITEMS[include]
+ ret.update(item.DEFAULT_CONFIG_VARS())
+ if config_vars:
+ ret.update(config_vars)
+ return ret
+ inner.DEFAULT_CONFIG_VARS = default_config_vars
+
+ assert name not in self.CONFIG_ITEMS, (
+ '%s is already in CONFIG_ITEMS' % name)
+ self.CONFIG_ITEMS[name] = inner
+ if group:
+ self.MUTEX_GROUPS.setdefault(group, set()).add(name)
+ inner.IS_ROOT = is_root
+ if is_root:
+ assert not self.ROOT_CONFIG_ITEM, (
+ 'may only have one root config_ctx!')
+ self.ROOT_CONFIG_ITEM = inner
+ inner.IS_ROOT = True
+ return inner
+ return decorator
+
+
+def config_item_context(CONFIG_SCHEMA):
+ """Create a configuration context.
+
+ Args:
+ CONFIG_SCHEMA: This is a function which can take a minimum of zero arguments
+ and returns an instance of BaseConfig. This BaseConfig
+ defines the schema for all configuration objects manipulated
+ in this context.
+
+ Returns a config_ctx decorator for this context.
+ """
+ return ConfigContext(CONFIG_SCHEMA)
+
+
+class AutoHide(object):
+ pass
+AutoHide = AutoHide()
+
+
+class ConfigBase(object):
+ """This is the root interface for all config schema types."""
+
+ def __init__(self, hidden=AutoHide):
+ """
+ Args:
+ hidden -
+ True: This object will be excluded from printing when the config blob
+ is rendered with ConfigGroup.as_jsonish(). You still have full
+ read/write access to this blob otherwise though.
+ False: This will be printed as part of ConfigGroup.as_jsonish()
+ AutoHide: This will be printed as part of ConfigGroup.as_jsonish() only
+ if self._is_default() is false.
+ """
+ # work around subclasses which override __setattr__
+ object.__setattr__(self, '_hidden_mode', hidden)
+ object.__setattr__(self, '_inclusions', set())
+
+ def get_val(self):
+ """Gets the native value of this config object."""
+ return self
+
+ def set_val(self, val):
+ """Resets the value of this config object using data in val."""
+ raise NotImplementedError
+
+ def reset(self):
+ """Resets the value of this config object to it's initial state."""
+ raise NotImplementedError
+
+ def as_jsonish(self, include_hidden=False):
+ """Returns the value of this config object as simple types."""
+ raise NotImplementedError
+
+ def complete(self):
+ """Returns True iff this configuraton blob is fully viable."""
+ raise NotImplementedError
+
+ def _is_default(self):
+ """Returns True iff this configuraton blob is the default value."""
+ raise NotImplementedError
+
+ @property
+ def _hidden(self):
+ """Returns True iff this configuraton blob is hidden."""
+ if self._hidden_mode is AutoHide:
+ return self._is_default()
+ return self._hidden_mode
+
+
+class ConfigGroup(ConfigBase):
+ """Allows you to provide hierarchy to a configuration schema.
+
+ Example usage:
+ config_blob = ConfigGroup(
+ some_item = SimpleConfig(str),
+ group = ConfigGroup(
+ numbahs = SetConfig(int),
+ ),
+ )
+ config_blob.some_item = "hello"
+ config_blob.group.numbahs.update(range(10))
+ """
+
+ def __init__(self, hidden=AutoHide, **type_map):
+ """Expects type_map to be {python_name -> ConfigBase} instance."""
+ super(ConfigGroup, self).__init__(hidden)
+ assert type_map, 'A ConfigGroup with no type_map is meaningless.'
+
+ object.__setattr__(self, '_type_map', type_map)
+ for name, typeval in self._type_map.iteritems():
+ typeAssert(typeval, ConfigBase)
+ object.__setattr__(self, name, typeval)
+
+ def __getattribute__(self, name):
+ obj = object.__getattribute__(self, name)
+ if isinstance(obj, ConfigBase):
+ return obj.get_val()
+ else:
+ return obj
+
+ def __setattr__(self, name, val):
+ obj = object.__getattribute__(self, name)
+ typeAssert(obj, ConfigBase)
+ obj.set_val(val)
+
+ def __delattr__(self, name):
+ obj = object.__getattribute__(self, name)
+ typeAssert(obj, ConfigBase)
+ obj.reset()
+
+ def set_val(self, val):
+ if isinstance(val, ConfigBase):
+ val = val.as_jsonish(include_hidden=True)
+ if isinstance(val, infra_types.FrozenDict):
+ val = infra_types.thaw(val)
+ typeAssert(val, dict)
+
+ val = dict(val) # because we pop later.
+ for name, config_obj in self._type_map.iteritems():
+ if name in val:
+ try:
+ config_obj.set_val(val.pop(name))
+ except Exception as e:
+ raise Exception('While assigning key %r: %s' % (name, e))
+
+ if val:
+ raise TypeError("Got extra keys while setting ConfigGroup: %s" % val)
+
+ def as_jsonish(self, include_hidden=False):
+ return dict(
+ (n, v.as_jsonish(include_hidden)) for n, v in self._type_map.iteritems()
+ if include_hidden or not v._hidden) # pylint: disable=W0212
+
+ def reset(self):
+ for v in self._type_map.values():
+ v.reset()
+
+ def complete(self):
+ return all(v.complete() for v in self._type_map.values())
+
+ def _is_default(self):
+ # pylint: disable=W0212
+ return all(v._is_default() for v in self._type_map.values())
+
+
+class ConfigList(ConfigBase, collections.MutableSequence):
+ """Allows you to provide an ordered repetition to a configuration schema.
+
+ Example usage:
+ config_blob = ConfigGroup(
+ some_items = ConfigList(
+ lambda: ConfigGroup(
+ herp = SimpleConfig(int),
+ derp = SimpleConfig(str)
+ )
+ )
+ )
+ config_blob.some_items.append({'herp': 1})
+ config_blob.some_items[0].derp = 'bob'
+ """
+
+ def __init__(self, item_schema, hidden=AutoHide):
+ """
+ Args:
+ item_schema: The schema of each object. Should be a function which returns
+ an instance of ConfigGroup.
+ """
+ super(ConfigList, self).__init__(hidden=hidden)
+ typeAssert(item_schema, types.FunctionType)
+ typeAssert(item_schema(), ConfigGroup)
+ self.item_schema = item_schema
+ self.data = []
+
+ def __getitem__(self, index):
+ return self.data.__getitem__(index)
+
+ def __setitem__(self, index, value):
+ datum = self.item_schema()
+ datum.set_val(value)
+ return self.data.__setitem__(index, datum)
+
+ def __delitem__(self, index):
+ return self.data.__delitem__(index)
+
+ def __len__(self):
+ return len(self.data)
+
+ def insert(self, index, value):
+ datum = self.item_schema()
+ datum.set_val(value)
+ return self.data.insert(index, datum)
+
+ def add(self):
+ self.append({})
+ return self[-1]
+
+ def reset(self):
+ self.data = []
+
+ def complete(self):
+ return all(i.complete() for i in self.data)
+
+ def set_val(self, data):
+ if isinstance(data, ConfigList):
+ data = data.as_jsonish(include_hidden=True)
+
+ typeAssert(data, list)
+ self.reset()
+ for item in data:
+ self.append(item)
+
+ def as_jsonish(self, include_hidden=False):
+ return [i.as_jsonish(include_hidden) for i in self.data
+ if include_hidden or not i._hidden] # pylint: disable=W0212
+
+ def _is_default(self):
+ # pylint: disable=W0212
+ return all(v._is_default() for v in self.data)
+
+
+class Dict(ConfigBase, collections.MutableMapping):
+ """Provides a semi-homogenous dict()-like configuration object."""
+
+ def __init__(self, item_fn=lambda i: i, jsonish_fn=dict, value_type=None,
+ hidden=AutoHide):
+ """
+ Args:
+ item_fn - A function which renders (k, v) pairs to input items for
+ jsonish_fn. Defaults to the identity function.
+ jsonish_fn - A function which renders a list of outputs of item_fn to a
+ JSON-compatiple python datatype. Defaults to dict().
+ value_type - A type object used for constraining/validating the values
+ assigned to this dictionary.
+ hidden - See ConfigBase.
+ """
+ super(Dict, self).__init__(hidden)
+ self.value_type = value_type
+ self.item_fn = item_fn
+ self.jsonish_fn = jsonish_fn
+ self.data = {}
+
+ def __getitem__(self, k):
+ return self.data.__getitem__(k)
+
+ def __setitem__(self, k, v):
+ if self.value_type:
+ typeAssert(v, self.value_type)
+ return self.data.__setitem__(k, v)
+
+ def __delitem__(self, k):
+ return self.data.__delitem__(k)
+
+ def __iter__(self):
+ return iter(self.data)
+
+ def __len__(self):
+ return len(self.data)
+
+ def set_val(self, val):
+ if isinstance(val, Dict):
+ val = val.data
+ if isinstance(val, infra_types.FrozenDict):
+ val = dict(val)
+
+ typeAssert(val, dict)
+ for v in val.itervalues():
+ typeAssert(v, self.value_type)
+ self.data = val
+
+ def as_jsonish(self, _include_hidden=None):
+ return self.jsonish_fn(map(
+ self.item_fn, sorted(self.data.iteritems(), key=lambda x: x[0])))
+
+ def reset(self):
+ self.data.clear()
+
+ def complete(self):
+ return True
+
+ def _is_default(self):
+ return not self.data
+
+
+class List(ConfigBase, collections.MutableSequence):
+ """Provides a semi-homogenous list()-like configuration object."""
+
+ def __init__(self, inner_type, jsonish_fn=list, hidden=AutoHide):
+ """
+ Args:
+ inner_type - The type of data contained in this set, e.g. str, int, ...
+ Can also be a tuple of types to allow more than one type.
+ jsonish_fn - A function used to reduce the list() to a JSON-compatible
+ python datatype. Defaults to list().
+ hidden - See ConfigBase.
+ """
+ super(List, self).__init__(hidden)
+ self.inner_type = inner_type
+ self.jsonish_fn = jsonish_fn
+ self.data = []
+
+ def __getitem__(self, index):
+ return self.data[index]
+
+ def __setitem__(self, index, value):
+ typeAssert(value, self.inner_type)
+ self.data[index] = value
+
+ def __delitem__(self, index):
+ del self.data
+
+ def __len__(self):
+ return len(self.data)
+
+ def __radd__(self, other):
+ if not isinstance(other, list):
+ other = list(other)
+ return other + self.data
+
+ def insert(self, index, value):
+ typeAssert(value, self.inner_type)
+ self.data.insert(index, value)
+
+ def set_val(self, val):
+ for v in val:
+ typeAssert(v, self.inner_type)
+ self.data = list(val)
+
+ def as_jsonish(self, _include_hidden=None):
+ return self.jsonish_fn(self.data)
+
+ def reset(self):
+ self.data = []
+
+ def complete(self):
+ return True
+
+ def _is_default(self):
+ return not self.data
+
+
+class Set(ConfigBase, collections.MutableSet):
+ """Provides a semi-homogenous set()-like configuration object."""
+
+ def __init__(self, inner_type, jsonish_fn=list, hidden=AutoHide):
+ """
+ Args:
+ inner_type - The type of data contained in this set, e.g. str, int, ...
+ Can also be a tuple of types to allow more than one type.
+ jsonish_fn - A function used to reduce the set() to a JSON-compatible
+ python datatype. Defaults to list().
+ hidden - See ConfigBase.
+ """
+ super(Set, self).__init__(hidden)
+ self.inner_type = inner_type
+ self.jsonish_fn = jsonish_fn
+ self.data = set()
+
+ def __contains__(self, val):
+ return val in self.data
+
+ def __iter__(self):
+ return iter(self.data)
+
+ def __len__(self):
+ return len(self.data)
+
+ def add(self, value):
+ typeAssert(value, self.inner_type)
+ self.data.add(value)
+
+ def update(self, values):
+ for value in values:
+ if value not in self:
+ self.add(value)
+
+ def discard(self, value):
+ self.data.discard(value)
+
+ def set_val(self, val):
+ for v in val:
+ typeAssert(v, self.inner_type)
+ self.data = set(val)
+
+ def as_jsonish(self, _include_hidden=None):
+ return self.jsonish_fn(sorted(self.data))
+
+ def reset(self):
+ self.data = set()
+
+ def complete(self):
+ return True
+
+ def _is_default(self):
+ return not self.data
+
+
+class Single(ConfigBase):
+ """Provides a configuration object which holds a single 'simple' type."""
+
+ def __init__(self, inner_type, jsonish_fn=lambda x: x, empty_val=None,
+ required=True, hidden=AutoHide):
+ """
+ Args:
+ inner_type - The type of data contained in this object, e.g. str, int, ...
+ Can also be a tuple of types to allow more than one type.
+ jsonish_fn - A function used to reduce the data to a JSON-compatible
+ python datatype. Default is the identity function.
+ empty_val - The value to use when initializing this object or when calling
+ reset().
+ required(bool) - True iff this config item is required to have a
+ non-empty_val in order for it to be considered complete().
+ hidden - See ConfigBase.
+ """
+ super(Single, self).__init__(hidden)
+ self.inner_type = inner_type
+ self.jsonish_fn = jsonish_fn
+ self.empty_val = empty_val
+ self.data = empty_val
+ self.required = required
+
+ def get_val(self):
+ return self.data
+
+ def set_val(self, val):
+ if isinstance(val, Single):
+ val = val.data
+ if val is not self.empty_val:
+ typeAssert(val, self.inner_type)
+ self.data = val
+
+ def as_jsonish(self, _include_hidden=None):
+ return self.jsonish_fn(self.data)
+
+ def reset(self):
+ self.data = self.empty_val
+
+ def complete(self):
+ return not self.required or self.data is not self.empty_val
+
+ def _is_default(self):
+ return self.data is self.empty_val
+
+
+class Static(ConfigBase):
+ """Holds a single, hidden, immutible data object.
+
+ This is very useful for holding the 'input' configuration values.
+ """
+
+ def __init__(self, value, hidden=AutoHide):
+ super(Static, self).__init__(hidden=hidden)
+ # Attempt to hash the value, which will ensure that it's immutable all the
+ # way down :).
+ hash(value)
+ self.data = value
+
+ def get_val(self):
+ return self.data
+
+ def set_val(self, val):
+ raise TypeError("Cannot assign to a Static config member")
+
+ def as_jsonish(self, _include_hidden=None):
+ return self.data
+
+ def reset(self):
+ assert False
+
+ def complete(self):
+ return True
+
+ def _is_default(self):
+ return True
« no previous file with comments | « third_party/recipe_engine/__init__.py ('k') | third_party/recipe_engine/config_types.py » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698