| OLD | NEW |
| 1 # Copyright 2013 The Chromium Authors. All rights reserved. | 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 | 2 # Use of this source code is governed by a BSD-style license that can be |
| 3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
| 4 | 4 |
| 5 """Recipe Configuration Meta DSL. | 5 """Recipe Configuration Meta DSL. |
| 6 | 6 |
| 7 This module contains, essentially, a DSL for writing composable configurations. | 7 This module contains, essentially, a DSL for writing composable configurations. |
| 8 You start by defining a schema which describes how your configuration blobs will | 8 You start by defining a schema which describes how your configuration blobs will |
| 9 be structured, and what data they can contain. For example: | 9 be structured, and what data they can contain. For example: |
| 10 | 10 |
| (...skipping 70 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 81 | 81 |
| 82 Using this system should allow you to create rich, composible, | 82 Using this system should allow you to create rich, composible, |
| 83 modular configurations. See the documentation on config_item_context and the | 83 modular configurations. See the documentation on config_item_context and the |
| 84 BaseConfig derivatives for more info. | 84 BaseConfig derivatives for more info. |
| 85 """ | 85 """ |
| 86 | 86 |
| 87 import collections | 87 import collections |
| 88 import functools | 88 import functools |
| 89 import types | 89 import types |
| 90 | 90 |
| 91 |
| 91 class BadConf(Exception): | 92 class BadConf(Exception): |
| 92 pass | 93 pass |
| 93 | 94 |
| 94 def config_item_context(CONFIG_SCHEMA, VAR_TEST_MAP, TEST_NAME_FORMAT, | |
| 95 TEST_FILE_FORMAT=None): | |
| 96 """Create a configuration context. | |
| 97 | 95 |
| 98 Args: | 96 class ConfigContext(object): |
| 99 CONFIG_SCHEMA: This is a function which can take a minimum of zero arguments | 97 """A configuration context for a recipe module. |
| 100 and returns an instance of BaseConfig. This BaseConfig | |
| 101 defines the schema for all configuration objects manipulated | |
| 102 in this context. | |
| 103 VAR_TEST_MAP: A dict mapping arg_name to an iterable of values. This | |
| 104 provides the test harness with sufficient information to | |
| 105 generate all possible permutations of inputs for the | |
| 106 CONFIG_SCHEMA function. | |
| 107 TEST_NAME_FORMAT: A string format (or function) for naming tests and test | |
| 108 expectation files. It will be formatted/called with a | |
| 109 dictionary of arg_name to value (using arg_names and | |
| 110 values generated from VAR_TEST_MAP) | |
| 111 TEST_FILE_FORMAT: Similar to TEST_NAME_FORMAT, but for test files. Defaults | |
| 112 to TEST_NAME_FORMAT. | |
| 113 | 98 |
| 114 Returns a config_ctx decorator for this context. | 99 Holds configuration schema and also acts as a config_ctx decorator. |
| 100 A recipe module can define at most one such context. |
| 115 """ | 101 """ |
| 116 | 102 |
| 117 def config_ctx(group=None, includes=None, deps=None, no_test=False, | 103 def __init__(self, CONFIG_SCHEMA, VAR_TEST_MAP, TEST_NAME_FORMAT): |
| 118 is_root=False, config_vars=None): | 104 self.CONFIG_ITEMS = {} |
| 105 self.MUTEX_GROUPS = {} |
| 106 self.CONFIG_SCHEMA = CONFIG_SCHEMA |
| 107 self.ROOT_CONFIG_ITEM = None |
| 108 self.VAR_TEST_MAP = VAR_TEST_MAP |
| 109 self.TEST_NAME_FORMAT = create_formatter(TEST_NAME_FORMAT) |
| 110 |
| 111 def __call__(self, group=None, includes=None, deps=None, no_test=False, |
| 112 is_root=False, config_vars=None): |
| 119 """ | 113 """ |
| 120 A decorator for functions which modify a given schema of configs. | 114 A decorator for functions which modify a given schema of configs. |
| 121 Examples continue using the schema and config_items defined in the module | 115 Examples continue using the schema and config_items defined in the module |
| 122 docstring. | 116 docstring. |
| 123 | 117 |
| 124 This decorator provides a series of related functions: | 118 This decorator provides a series of related functions: |
| 125 * Any function decorated with this will be registered into this config | 119 * Any function decorated with this will be registered into this config |
| 126 context by __name__. This enables some of the other following features | 120 context by __name__. This enables some of the other following features |
| 127 to work. | 121 to work. |
| 128 * Alters the signature of the function so that it can recieve an extra | 122 * Alters the signature of the function so that it can recieve an extra |
| (...skipping 44 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 173 ever be one root item. | 167 ever be one root item. |
| 174 | 168 |
| 175 Additionally, the test harness uses the root item to probe for invalid | 169 Additionally, the test harness uses the root item to probe for invalid |
| 176 configuration combinations by running the root item first (if there is | 170 configuration combinations by running the root item first (if there is |
| 177 one), and skipping the configuration combination if the root config | 171 one), and skipping the configuration combination if the root config |
| 178 item throws BadConf. | 172 item throws BadConf. |
| 179 | 173 |
| 180 config_vars(dict) - A dictionary mapping of { CONFIG_VAR: <value> }. This | 174 config_vars(dict) - A dictionary mapping of { CONFIG_VAR: <value> }. This |
| 181 sets the input contidions for the CONFIG_SCHEMA. | 175 sets the input contidions for the CONFIG_SCHEMA. |
| 182 | 176 |
| 183 Returns a new decorated version of this function (see inner()). | 177 Returns a new decorated version of this function (see inner()). |
| 184 """ | 178 """ |
| 185 def decorator(f): | 179 def decorator(f): |
| 186 name = f.__name__ | 180 name = f.__name__ |
| 187 @functools.wraps(f) | 181 @functools.wraps(f) |
| 188 def inner(config=None, final=True, optional=False, **kwargs): | 182 def inner(config=None, final=True, optional=False, **kwargs): |
| 189 """This is the function which is returned from the config_ctx | 183 """This is the function which is returned from the config_ctx |
| 190 decorator. | 184 decorator. |
| 191 | 185 |
| 192 It applies all of the logic mentioned in the config_ctx docstring | 186 It applies all of the logic mentioned in the config_ctx docstring |
| 193 above, and alters the function signature slightly. | 187 above, and alters the function signature slightly. |
| (...skipping 17 matching lines...) Expand all Loading... |
| 211 can implement the config items with less error checking, since you | 205 can implement the config items with less error checking, since you |
| 212 know that the item may only be applied once. For example, if your | 206 know that the item may only be applied once. For example, if your |
| 213 item appends something to a list, but is called with final=False, | 207 item appends something to a list, but is called with final=False, |
| 214 you'll have to make sure that you don't append the item twice, etc. | 208 you'll have to make sure that you don't append the item twice, etc. |
| 215 | 209 |
| 216 **kwargs - Passed through to the decorated function without harm. | 210 **kwargs - Passed through to the decorated function without harm. |
| 217 | 211 |
| 218 Returns config and ignores the return value of the decorated function. | 212 Returns config and ignores the return value of the decorated function. |
| 219 """ | 213 """ |
| 220 if config is None: | 214 if config is None: |
| 221 config = config_ctx.CONFIG_SCHEMA() | 215 config = self.CONFIG_SCHEMA() |
| 222 assert isinstance(config, ConfigGroup) | 216 assert isinstance(config, ConfigGroup) |
| 223 inclusions = config._inclusions # pylint: disable=W0212 | 217 inclusions = config._inclusions # pylint: disable=W0212 |
| 224 | 218 |
| 225 # inner.IS_ROOT will be True or False at the time of invocation. | 219 # inner.IS_ROOT will be True or False at the time of invocation. |
| 226 if (config_ctx.ROOT_CONFIG_ITEM and not inner.IS_ROOT and | 220 if (self.ROOT_CONFIG_ITEM and not inner.IS_ROOT and |
| 227 config_ctx.ROOT_CONFIG_ITEM.__name__ not in inclusions): | 221 self.ROOT_CONFIG_ITEM.__name__ not in inclusions): |
| 228 config_ctx.ROOT_CONFIG_ITEM(config) | 222 self.ROOT_CONFIG_ITEM(config) |
| 229 | 223 |
| 230 if name in inclusions: | 224 if name in inclusions: |
| 231 if optional: | 225 if optional: |
| 232 return config | 226 return config |
| 233 raise BadConf('config_ctx "%s" is already in this config "%s"' % | 227 raise BadConf('config_ctx "%s" is already in this config "%s"' % |
| 234 (name, config.as_jsonish(include_hidden=True))) | 228 (name, config.as_jsonish(include_hidden=True))) |
| 235 if final: | 229 if final: |
| 236 inclusions.add(name) | 230 inclusions.add(name) |
| 237 | 231 |
| 238 for include in (includes or []): | 232 for include in (includes or []): |
| 239 if include in inclusions: | 233 if include in inclusions: |
| 240 continue | 234 continue |
| 241 try: | 235 try: |
| 242 config_ctx.CONFIG_ITEMS[include](config) | 236 self.CONFIG_ITEMS[include](config) |
| 243 except BadConf, e: | 237 except BadConf as e: |
| 244 raise BadConf('config "%s" includes "%s", but [%s]' % | 238 raise BadConf('config "%s" includes "%s", but [%s]' % |
| 245 (name, include, e)) | 239 (name, include, e)) |
| 246 | 240 |
| 247 # deps are a list of group names. All groups must be represented | 241 # deps are a list of group names. All groups must be represented |
| 248 # in config already. | 242 # in config already. |
| 249 for dep_group in (deps or []): | 243 for dep_group in (deps or []): |
| 250 if not (inclusions & config_ctx.MUTEX_GROUPS[dep_group]): | 244 if not (inclusions & self.MUTEX_GROUPS[dep_group]): |
| 251 raise BadConf('dep group "%s" is unfulfilled for "%s"' % | 245 raise BadConf('dep group "%s" is unfulfilled for "%s"' % |
| 252 (dep_group, name)) | 246 (dep_group, name)) |
| 253 | 247 |
| 254 if group: | 248 if group: |
| 255 overlap = inclusions & config_ctx.MUTEX_GROUPS[group] | 249 overlap = inclusions & self.MUTEX_GROUPS[group] |
| 256 overlap.discard(name) | 250 overlap.discard(name) |
| 257 if overlap: | 251 if overlap: |
| 258 raise BadConf('"%s" is a member of group "%s", but %s already ran' % | 252 raise BadConf('"%s" is a member of group "%s", but %s already ran' % |
| 259 (name, group, tuple(overlap))) | 253 (name, group, tuple(overlap))) |
| 260 | 254 |
| 261 ret = f(config, **kwargs) | 255 ret = f(config, **kwargs) |
| 262 assert ret is None, 'Got return value (%s) from "%s"?' % (ret, name) | 256 assert ret is None, 'Got return value (%s) from "%s"?' % (ret, name) |
| 263 | 257 |
| 264 return config | 258 return config |
| 265 | 259 |
| 266 def default_config_vars(): | 260 def default_config_vars(): |
| 267 ret = {} | 261 ret = {} |
| 268 for include in (includes or []): | 262 for include in (includes or []): |
| 269 item = config_ctx.CONFIG_ITEMS[include] | 263 item = self.CONFIG_ITEMS[include] |
| 270 ret.update(item.DEFAULT_CONFIG_VARS()) | 264 ret.update(item.DEFAULT_CONFIG_VARS()) |
| 271 if config_vars: | 265 if config_vars: |
| 272 ret.update(config_vars) | 266 ret.update(config_vars) |
| 273 return ret | 267 return ret |
| 274 inner.DEFAULT_CONFIG_VARS = default_config_vars | 268 inner.DEFAULT_CONFIG_VARS = default_config_vars |
| 275 | 269 |
| 276 assert name not in config_ctx.CONFIG_ITEMS | 270 assert name not in self.CONFIG_ITEMS |
| 277 config_ctx.CONFIG_ITEMS[name] = inner | 271 self.CONFIG_ITEMS[name] = inner |
| 278 if group: | 272 if group: |
| 279 config_ctx.MUTEX_GROUPS.setdefault(group, set()).add(name) | 273 self.MUTEX_GROUPS.setdefault(group, set()).add(name) |
| 280 inner.IS_ROOT = is_root | 274 inner.IS_ROOT = is_root |
| 281 if is_root: | 275 if is_root: |
| 282 assert not config_ctx.ROOT_CONFIG_ITEM, ( | 276 assert not self.ROOT_CONFIG_ITEM, ( |
| 283 'may only have one root config_ctx!') | 277 'may only have one root config_ctx!') |
| 284 config_ctx.ROOT_CONFIG_ITEM = inner | 278 self.ROOT_CONFIG_ITEM = inner |
| 285 inner.IS_ROOT = True | 279 inner.IS_ROOT = True |
| 286 inner.NO_TEST = no_test or bool(deps) | 280 inner.NO_TEST = no_test or bool(deps) |
| 287 return inner | 281 return inner |
| 288 return decorator | 282 return decorator |
| 289 | 283 |
| 290 # Internal state and testing data | |
| 291 config_ctx.I_AM_A_CONFIG_CTX = True | |
| 292 config_ctx.CONFIG_ITEMS = {} | |
| 293 config_ctx.MUTEX_GROUPS = {} | |
| 294 config_ctx.CONFIG_SCHEMA = CONFIG_SCHEMA | |
| 295 config_ctx.ROOT_CONFIG_ITEM = None | |
| 296 config_ctx.VAR_TEST_MAP = VAR_TEST_MAP | |
| 297 | 284 |
| 298 def formatter(obj, ext=None): | 285 def create_formatter(obj, ext=None): |
| 299 """Converts format obj to a function taking var assignments. | 286 """Converts format obj to a function taking var assignments. |
| 300 | 287 |
| 301 Args: | 288 Args: |
| 302 obj (str or fn(assignments)): If obj is a str, it will be % formatted | 289 obj (str or fn(assignments)): If obj is a str, it will be % formatted |
| 303 with assignments (which is a dict of variables from VAR_TEST_MAP). | 290 with assignments (which is a dict of variables from VAR_TEST_MAP). |
| 304 Otherwise obj will be invoked with assignments, and expected to return | 291 Otherwise obj will be invoked with assignments, and expected to return |
| 305 a fully-rendered string. | 292 a fully-rendered string. |
| 306 ext (None or str): Optionally specify an extension to enforce on the | 293 ext (None or str): Optionally specify an extension to enforce on the |
| 307 format. This enforcement occurs after obj is finalized to a string. If | 294 format. This enforcement occurs after obj is finalized to a string. If |
| 308 the string doesn't end with ext, it will be appended. | 295 the string doesn't end with ext, it will be appended. |
| 309 """ | 296 """ |
| 310 def inner(var_assignments): | 297 def inner(var_assignments): |
| 311 ret = '' | 298 ret = '' |
| 312 if isinstance(obj, basestring): | 299 if isinstance(obj, basestring): |
| 313 ret = obj % var_assignments | 300 ret = obj % var_assignments |
| 314 else: | 301 else: |
| 315 ret = obj(var_assignments) | 302 ret = obj(var_assignments) |
| 316 if ext and not ret.endswith(ext): | 303 if ext and not ret.endswith(ext): |
| 317 ret += ext | 304 ret += ext |
| 318 return ret | 305 return ret |
| 319 return inner | 306 return inner |
| 320 config_ctx.TEST_NAME_FORMAT = formatter(TEST_NAME_FORMAT) | 307 |
| 321 return config_ctx | 308 |
| 309 def config_item_context(CONFIG_SCHEMA, VAR_TEST_MAP, TEST_NAME_FORMAT): |
| 310 """Create a configuration context. |
| 311 |
| 312 Args: |
| 313 CONFIG_SCHEMA: This is a function which can take a minimum of zero arguments |
| 314 and returns an instance of BaseConfig. This BaseConfig |
| 315 defines the schema for all configuration objects manipulated |
| 316 in this context. |
| 317 VAR_TEST_MAP: A dict mapping arg_name to an iterable of values. This |
| 318 provides the test harness with sufficient information to |
| 319 generate all possible permutations of inputs for the |
| 320 CONFIG_SCHEMA function. |
| 321 TEST_NAME_FORMAT: A string format (or function) for naming tests and test |
| 322 expectation files. It will be formatted/called with a |
| 323 dictionary of arg_name to value (using arg_names and |
| 324 values generated from VAR_TEST_MAP). |
| 325 |
| 326 Returns a config_ctx decorator for this context. |
| 327 """ |
| 328 return ConfigContext(CONFIG_SCHEMA, VAR_TEST_MAP, TEST_NAME_FORMAT) |
| 322 | 329 |
| 323 | 330 |
| 324 class AutoHide(object): | 331 class AutoHide(object): |
| 325 pass | 332 pass |
| 326 AutoHide = AutoHide() | 333 AutoHide = AutoHide() |
| 327 | 334 |
| 335 |
| 328 class ConfigBase(object): | 336 class ConfigBase(object): |
| 329 """This is the root interface for all config schema types.""" | 337 """This is the root interface for all config schema types.""" |
| 330 | 338 |
| 331 def __init__(self, hidden=AutoHide): | 339 def __init__(self, hidden=AutoHide): |
| 332 """ | 340 """ |
| 333 Args: | 341 Args: |
| 334 hidden - | 342 hidden - |
| 335 True: This object will be excluded from printing when the config blob | 343 True: This object will be excluded from printing when the config blob |
| 336 is rendered with ConfigGroup.as_jsonish(). You still have full | 344 is rendered with ConfigGroup.as_jsonish(). You still have full |
| 337 read/write access to this blob otherwise though. | 345 read/write access to this blob otherwise though. |
| (...skipping 424 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
| 762 return self.data | 770 return self.data |
| 763 | 771 |
| 764 def reset(self): | 772 def reset(self): |
| 765 assert False | 773 assert False |
| 766 | 774 |
| 767 def complete(self): | 775 def complete(self): |
| 768 return True | 776 return True |
| 769 | 777 |
| 770 def _is_default(self): | 778 def _is_default(self): |
| 771 return True | 779 return True |
| OLD | NEW |