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

Side by Side 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 unified diff | Download patch | Annotate | Revision Log
« 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 »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 # Copyright 2013-2015 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 """Recipe Configuration Meta DSL.
6
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
9 be structured, and what data they can contain. For example:
10
11 FakeSchema = lambda main_val=True, mode='Happy': ConfigGroup(
12 config_group = ConfigGroup(
13 item_a = SimpleConfig(int),
14 item_b = DictConfig(),
15 ),
16 extra_setting = SetConfig(str),
17
18 MAIN_DETERMINANT = StaticConfig(main_val),
19 CONFIG_MODE = StaticConfig(mode),
20 )
21
22 In short, a 'schema' is a callable which can take zero arguments (it can contain
23 default arguments as well, for setting defaults, tweaking the schema, etc.), and
24 returning a ConfigGroup.
25
26 Every type used in the schema derives from ConfigBase. It has the general
27 characteristics that it's a fixed-type container. It tends to impersonate the
28 data type that it stores (so you can manipulate the config objects like normal
29 python data), but also provides type checking and type conversion assistence
30 (so you can easily render your configurations to JSON).
31
32 Then you can create a configuration context:
33
34 config_ctx = config_item_context(FakeSchema)
35
36 config_ctx is a python decorator which you can use to create composable
37 configuration functions. For example:
38
39 @config_ctx()
40 def cool(c):
41 if c.CONFIG_MODE == 'Happy':
42 c.config_group.item_a = 100
43 else:
44 c.config_group.item_a = -100
45
46 @config_ctx()
47 def gnarly(c):
48 c.extra_setting = 'gnarly!'
49
50 @config_ctx(includes=('cool', 'gnarly'))
51 def combo(c):
52 if c.MAIN_DETERMINANT:
53 c.config_group.item_b['nickname'] = 'purple'
54 c.extra_setting += ' cows!'
55 else:
56 c.config_group.item_b['nickname'] = 'sad times'
57
58 If I now call:
59
60 combo()
61
62 I will get back a configuraton object whose schema is FakeSchema, and whose
63 data is the accumulation of cool(), gnarly(), and combo(). I can continue to
64 manipulate this configuraton object, use its data, or render it to json.
65
66 Using this system should allow you to create rich, composible,
67 modular configurations. See the documentation on config_item_context and the
68 BaseConfig derivatives for more info.
69 """
70
71 import collections
72 import functools
73 import types
74
75 from infra.libs import infra_types
76
77 class BadConf(Exception):
78 pass
79
80 def typeAssert(obj, typearg):
81 if not isinstance(obj, typearg):
82 raise TypeError("Expected %r to be of type %r" % (obj, typearg))
83
84
85 class ConfigContext(object):
86 """A configuration context for a recipe module.
87
88 Holds configuration schema and also acts as a config_ctx decorator.
89 A recipe module can define at most one such context.
90 """
91
92 def __init__(self, CONFIG_SCHEMA):
93 self.CONFIG_ITEMS = {}
94 self.MUTEX_GROUPS = {}
95 self.CONFIG_SCHEMA = CONFIG_SCHEMA
96 self.ROOT_CONFIG_ITEM = None
97
98 def __call__(self, group=None, includes=None, deps=None,
99 is_root=False, config_vars=None):
100 """
101 A decorator for functions which modify a given schema of configs.
102 Examples continue using the schema and config_items defined in the module
103 docstring.
104
105 This decorator provides a series of related functions:
106 * Any function decorated with this will be registered into this config
107 context by __name__. This enables some of the other following features
108 to work.
109 * Alters the signature of the function so that it can receive an extra
110 parameter 'final'. See the documentation for final on inner().
111 * Provides various convenience and error checking facilities.
112 * In particular, this decorator will prevent you from calling the same
113 config_ctx on a given config blob more than once (with the exception
114 of setting final=False. See inner())
115
116 Args:
117 group(str) - Using this decorator with the `group' argument causes the
118 decorated function to be a member of that group. Members of a group are
119 mutually exclusive on the same configuration blob. For example, only
120 one of these two functions could be applied to the config blob c:
121 @config_ctx(group='a')
122 def bob(c):
123 c.extra_setting = "bob mode"
124
125 @config_ctx(group='a')
126 def bill(c):
127 c.extra_setting = "bill mode"
128
129 includes(iterable(str)) - Any config items named in the includes list will
130 be run against the config blob before the decorated function can modify
131 it. If an inclusion is already applied to the config blob, it's skipped
132 without applying/raising BadConf. Example:
133 @config_ctx(includes=('bob', 'cool'))
134 def charlie(c):
135 c.config_group.item_b = 25
136 The result of this config_ctx (assuming default values for the schema)
137 would be:
138 {'config_group': { 'item_a': 100, 'item_b': 25 },
139 'extra_setting': 'gnarly!'}
140
141 deps(iterable(str)) - One or more groups which must be satisfied before
142 this config_ctx can be applied to a config_blob. If you invoke
143 a config_ctx on a blob without having all of its deps satisfied,
144 you'll get a BadConf exception.
145
146 is_root(bool) - If set to True on an item, this item will become the
147 'basis' item for all other configurations in this group. That means that
148 it will be implicitly included in all other config_items. There may only
149 ever be one root item.
150
151 config_vars(dict) - A dictionary mapping of { CONFIG_VAR: <value> }. This
152 sets the input contidions for the CONFIG_SCHEMA.
153
154 Returns a new decorated version of this function (see inner()).
155 """
156 def decorator(f):
157 name = f.__name__
158 @functools.wraps(f)
159 def inner(config=None, final=True, optional=False, **kwargs):
160 """This is the function which is returned from the config_ctx
161 decorator.
162
163 It applies all of the logic mentioned in the config_ctx docstring
164 above, and alters the function signature slightly.
165
166 Args:
167 config - The config blob that we intend to manipulate. This is passed
168 through to the function after checking deps and including includes.
169 After the function manipulates it, it is automatically returned.
170
171 final(bool) - Set to True by default, this will record the application
172 of this config_ctx to `config', which will prevent the config_ctx
173 from being applied to `config' again. It also is used to see if the
174 config blob satisfies deps for subsequent config_ctx applications
175 (i.e. in order for a config_ctx to satisfy a dependency, it must
176 be applied with final=True).
177
178 This is useful to apply default values while allowing the config to
179 later override those values.
180
181 However, it's best if each config_ctx is final, because then you
182 can implement the config items with less error checking, since you
183 know that the item may only be applied once. For example, if your
184 item appends something to a list, but is called with final=False,
185 you'll have to make sure that you don't append the item twice, etc.
186
187 **kwargs - Passed through to the decorated function without harm.
188
189 Returns config and ignores the return value of the decorated function.
190 """
191 if config is None:
192 config = self.CONFIG_SCHEMA()
193 assert isinstance(config, ConfigGroup)
194 inclusions = config._inclusions # pylint: disable=W0212
195
196 # inner.IS_ROOT will be True or False at the time of invocation.
197 if (self.ROOT_CONFIG_ITEM and not inner.IS_ROOT and
198 self.ROOT_CONFIG_ITEM.__name__ not in inclusions):
199 self.ROOT_CONFIG_ITEM(config)
200
201 if name in inclusions:
202 if optional:
203 return config
204 raise BadConf('config_ctx "%s" is already in this config "%s"' %
205 (name, config.as_jsonish(include_hidden=True)))
206 if final:
207 inclusions.add(name)
208
209 for include in includes or []:
210 if include in inclusions:
211 continue
212 try:
213 self.CONFIG_ITEMS[include](config)
214 except BadConf as e:
215 raise BadConf('config "%s" includes "%s", but [%s]' %
216 (name, include, e))
217
218 # deps are a list of group names. All groups must be represented
219 # in config already.
220 for dep_group in deps or []:
221 if not inclusions & self.MUTEX_GROUPS[dep_group]:
222 raise BadConf('dep group "%s" is unfulfilled for "%s"' %
223 (dep_group, name))
224
225 if group:
226 overlap = inclusions & self.MUTEX_GROUPS[group]
227 overlap.discard(name)
228 if overlap:
229 raise BadConf('"%s" is a member of group "%s", but %s already ran' %
230 (name, group, tuple(overlap)))
231
232 ret = f(config, **kwargs)
233 assert ret is None, 'Got return value (%s) from "%s"?' % (ret, name)
234
235 return config
236 inner.WRAPPED = f
237 inner.INCLUDES = includes or []
238
239 def default_config_vars():
240 ret = {}
241 for include in includes or []:
242 item = self.CONFIG_ITEMS[include]
243 ret.update(item.DEFAULT_CONFIG_VARS())
244 if config_vars:
245 ret.update(config_vars)
246 return ret
247 inner.DEFAULT_CONFIG_VARS = default_config_vars
248
249 assert name not in self.CONFIG_ITEMS, (
250 '%s is already in CONFIG_ITEMS' % name)
251 self.CONFIG_ITEMS[name] = inner
252 if group:
253 self.MUTEX_GROUPS.setdefault(group, set()).add(name)
254 inner.IS_ROOT = is_root
255 if is_root:
256 assert not self.ROOT_CONFIG_ITEM, (
257 'may only have one root config_ctx!')
258 self.ROOT_CONFIG_ITEM = inner
259 inner.IS_ROOT = True
260 return inner
261 return decorator
262
263
264 def config_item_context(CONFIG_SCHEMA):
265 """Create a configuration context.
266
267 Args:
268 CONFIG_SCHEMA: This is a function which can take a minimum of zero arguments
269 and returns an instance of BaseConfig. This BaseConfig
270 defines the schema for all configuration objects manipulated
271 in this context.
272
273 Returns a config_ctx decorator for this context.
274 """
275 return ConfigContext(CONFIG_SCHEMA)
276
277
278 class AutoHide(object):
279 pass
280 AutoHide = AutoHide()
281
282
283 class ConfigBase(object):
284 """This is the root interface for all config schema types."""
285
286 def __init__(self, hidden=AutoHide):
287 """
288 Args:
289 hidden -
290 True: This object will be excluded from printing when the config blob
291 is rendered with ConfigGroup.as_jsonish(). You still have full
292 read/write access to this blob otherwise though.
293 False: This will be printed as part of ConfigGroup.as_jsonish()
294 AutoHide: This will be printed as part of ConfigGroup.as_jsonish() only
295 if self._is_default() is false.
296 """
297 # work around subclasses which override __setattr__
298 object.__setattr__(self, '_hidden_mode', hidden)
299 object.__setattr__(self, '_inclusions', set())
300
301 def get_val(self):
302 """Gets the native value of this config object."""
303 return self
304
305 def set_val(self, val):
306 """Resets the value of this config object using data in val."""
307 raise NotImplementedError
308
309 def reset(self):
310 """Resets the value of this config object to it's initial state."""
311 raise NotImplementedError
312
313 def as_jsonish(self, include_hidden=False):
314 """Returns the value of this config object as simple types."""
315 raise NotImplementedError
316
317 def complete(self):
318 """Returns True iff this configuraton blob is fully viable."""
319 raise NotImplementedError
320
321 def _is_default(self):
322 """Returns True iff this configuraton blob is the default value."""
323 raise NotImplementedError
324
325 @property
326 def _hidden(self):
327 """Returns True iff this configuraton blob is hidden."""
328 if self._hidden_mode is AutoHide:
329 return self._is_default()
330 return self._hidden_mode
331
332
333 class ConfigGroup(ConfigBase):
334 """Allows you to provide hierarchy to a configuration schema.
335
336 Example usage:
337 config_blob = ConfigGroup(
338 some_item = SimpleConfig(str),
339 group = ConfigGroup(
340 numbahs = SetConfig(int),
341 ),
342 )
343 config_blob.some_item = "hello"
344 config_blob.group.numbahs.update(range(10))
345 """
346
347 def __init__(self, hidden=AutoHide, **type_map):
348 """Expects type_map to be {python_name -> ConfigBase} instance."""
349 super(ConfigGroup, self).__init__(hidden)
350 assert type_map, 'A ConfigGroup with no type_map is meaningless.'
351
352 object.__setattr__(self, '_type_map', type_map)
353 for name, typeval in self._type_map.iteritems():
354 typeAssert(typeval, ConfigBase)
355 object.__setattr__(self, name, typeval)
356
357 def __getattribute__(self, name):
358 obj = object.__getattribute__(self, name)
359 if isinstance(obj, ConfigBase):
360 return obj.get_val()
361 else:
362 return obj
363
364 def __setattr__(self, name, val):
365 obj = object.__getattribute__(self, name)
366 typeAssert(obj, ConfigBase)
367 obj.set_val(val)
368
369 def __delattr__(self, name):
370 obj = object.__getattribute__(self, name)
371 typeAssert(obj, ConfigBase)
372 obj.reset()
373
374 def set_val(self, val):
375 if isinstance(val, ConfigBase):
376 val = val.as_jsonish(include_hidden=True)
377 if isinstance(val, infra_types.FrozenDict):
378 val = infra_types.thaw(val)
379 typeAssert(val, dict)
380
381 val = dict(val) # because we pop later.
382 for name, config_obj in self._type_map.iteritems():
383 if name in val:
384 try:
385 config_obj.set_val(val.pop(name))
386 except Exception as e:
387 raise Exception('While assigning key %r: %s' % (name, e))
388
389 if val:
390 raise TypeError("Got extra keys while setting ConfigGroup: %s" % val)
391
392 def as_jsonish(self, include_hidden=False):
393 return dict(
394 (n, v.as_jsonish(include_hidden)) for n, v in self._type_map.iteritems()
395 if include_hidden or not v._hidden) # pylint: disable=W0212
396
397 def reset(self):
398 for v in self._type_map.values():
399 v.reset()
400
401 def complete(self):
402 return all(v.complete() for v in self._type_map.values())
403
404 def _is_default(self):
405 # pylint: disable=W0212
406 return all(v._is_default() for v in self._type_map.values())
407
408
409 class ConfigList(ConfigBase, collections.MutableSequence):
410 """Allows you to provide an ordered repetition to a configuration schema.
411
412 Example usage:
413 config_blob = ConfigGroup(
414 some_items = ConfigList(
415 lambda: ConfigGroup(
416 herp = SimpleConfig(int),
417 derp = SimpleConfig(str)
418 )
419 )
420 )
421 config_blob.some_items.append({'herp': 1})
422 config_blob.some_items[0].derp = 'bob'
423 """
424
425 def __init__(self, item_schema, hidden=AutoHide):
426 """
427 Args:
428 item_schema: The schema of each object. Should be a function which returns
429 an instance of ConfigGroup.
430 """
431 super(ConfigList, self).__init__(hidden=hidden)
432 typeAssert(item_schema, types.FunctionType)
433 typeAssert(item_schema(), ConfigGroup)
434 self.item_schema = item_schema
435 self.data = []
436
437 def __getitem__(self, index):
438 return self.data.__getitem__(index)
439
440 def __setitem__(self, index, value):
441 datum = self.item_schema()
442 datum.set_val(value)
443 return self.data.__setitem__(index, datum)
444
445 def __delitem__(self, index):
446 return self.data.__delitem__(index)
447
448 def __len__(self):
449 return len(self.data)
450
451 def insert(self, index, value):
452 datum = self.item_schema()
453 datum.set_val(value)
454 return self.data.insert(index, datum)
455
456 def add(self):
457 self.append({})
458 return self[-1]
459
460 def reset(self):
461 self.data = []
462
463 def complete(self):
464 return all(i.complete() for i in self.data)
465
466 def set_val(self, data):
467 if isinstance(data, ConfigList):
468 data = data.as_jsonish(include_hidden=True)
469
470 typeAssert(data, list)
471 self.reset()
472 for item in data:
473 self.append(item)
474
475 def as_jsonish(self, include_hidden=False):
476 return [i.as_jsonish(include_hidden) for i in self.data
477 if include_hidden or not i._hidden] # pylint: disable=W0212
478
479 def _is_default(self):
480 # pylint: disable=W0212
481 return all(v._is_default() for v in self.data)
482
483
484 class Dict(ConfigBase, collections.MutableMapping):
485 """Provides a semi-homogenous dict()-like configuration object."""
486
487 def __init__(self, item_fn=lambda i: i, jsonish_fn=dict, value_type=None,
488 hidden=AutoHide):
489 """
490 Args:
491 item_fn - A function which renders (k, v) pairs to input items for
492 jsonish_fn. Defaults to the identity function.
493 jsonish_fn - A function which renders a list of outputs of item_fn to a
494 JSON-compatiple python datatype. Defaults to dict().
495 value_type - A type object used for constraining/validating the values
496 assigned to this dictionary.
497 hidden - See ConfigBase.
498 """
499 super(Dict, self).__init__(hidden)
500 self.value_type = value_type
501 self.item_fn = item_fn
502 self.jsonish_fn = jsonish_fn
503 self.data = {}
504
505 def __getitem__(self, k):
506 return self.data.__getitem__(k)
507
508 def __setitem__(self, k, v):
509 if self.value_type:
510 typeAssert(v, self.value_type)
511 return self.data.__setitem__(k, v)
512
513 def __delitem__(self, k):
514 return self.data.__delitem__(k)
515
516 def __iter__(self):
517 return iter(self.data)
518
519 def __len__(self):
520 return len(self.data)
521
522 def set_val(self, val):
523 if isinstance(val, Dict):
524 val = val.data
525 if isinstance(val, infra_types.FrozenDict):
526 val = dict(val)
527
528 typeAssert(val, dict)
529 for v in val.itervalues():
530 typeAssert(v, self.value_type)
531 self.data = val
532
533 def as_jsonish(self, _include_hidden=None):
534 return self.jsonish_fn(map(
535 self.item_fn, sorted(self.data.iteritems(), key=lambda x: x[0])))
536
537 def reset(self):
538 self.data.clear()
539
540 def complete(self):
541 return True
542
543 def _is_default(self):
544 return not self.data
545
546
547 class List(ConfigBase, collections.MutableSequence):
548 """Provides a semi-homogenous list()-like configuration object."""
549
550 def __init__(self, inner_type, jsonish_fn=list, hidden=AutoHide):
551 """
552 Args:
553 inner_type - The type of data contained in this set, e.g. str, int, ...
554 Can also be a tuple of types to allow more than one type.
555 jsonish_fn - A function used to reduce the list() to a JSON-compatible
556 python datatype. Defaults to list().
557 hidden - See ConfigBase.
558 """
559 super(List, self).__init__(hidden)
560 self.inner_type = inner_type
561 self.jsonish_fn = jsonish_fn
562 self.data = []
563
564 def __getitem__(self, index):
565 return self.data[index]
566
567 def __setitem__(self, index, value):
568 typeAssert(value, self.inner_type)
569 self.data[index] = value
570
571 def __delitem__(self, index):
572 del self.data
573
574 def __len__(self):
575 return len(self.data)
576
577 def __radd__(self, other):
578 if not isinstance(other, list):
579 other = list(other)
580 return other + self.data
581
582 def insert(self, index, value):
583 typeAssert(value, self.inner_type)
584 self.data.insert(index, value)
585
586 def set_val(self, val):
587 for v in val:
588 typeAssert(v, self.inner_type)
589 self.data = list(val)
590
591 def as_jsonish(self, _include_hidden=None):
592 return self.jsonish_fn(self.data)
593
594 def reset(self):
595 self.data = []
596
597 def complete(self):
598 return True
599
600 def _is_default(self):
601 return not self.data
602
603
604 class Set(ConfigBase, collections.MutableSet):
605 """Provides a semi-homogenous set()-like configuration object."""
606
607 def __init__(self, inner_type, jsonish_fn=list, hidden=AutoHide):
608 """
609 Args:
610 inner_type - The type of data contained in this set, e.g. str, int, ...
611 Can also be a tuple of types to allow more than one type.
612 jsonish_fn - A function used to reduce the set() to a JSON-compatible
613 python datatype. Defaults to list().
614 hidden - See ConfigBase.
615 """
616 super(Set, self).__init__(hidden)
617 self.inner_type = inner_type
618 self.jsonish_fn = jsonish_fn
619 self.data = set()
620
621 def __contains__(self, val):
622 return val in self.data
623
624 def __iter__(self):
625 return iter(self.data)
626
627 def __len__(self):
628 return len(self.data)
629
630 def add(self, value):
631 typeAssert(value, self.inner_type)
632 self.data.add(value)
633
634 def update(self, values):
635 for value in values:
636 if value not in self:
637 self.add(value)
638
639 def discard(self, value):
640 self.data.discard(value)
641
642 def set_val(self, val):
643 for v in val:
644 typeAssert(v, self.inner_type)
645 self.data = set(val)
646
647 def as_jsonish(self, _include_hidden=None):
648 return self.jsonish_fn(sorted(self.data))
649
650 def reset(self):
651 self.data = set()
652
653 def complete(self):
654 return True
655
656 def _is_default(self):
657 return not self.data
658
659
660 class Single(ConfigBase):
661 """Provides a configuration object which holds a single 'simple' type."""
662
663 def __init__(self, inner_type, jsonish_fn=lambda x: x, empty_val=None,
664 required=True, hidden=AutoHide):
665 """
666 Args:
667 inner_type - The type of data contained in this object, e.g. str, int, ...
668 Can also be a tuple of types to allow more than one type.
669 jsonish_fn - A function used to reduce the data to a JSON-compatible
670 python datatype. Default is the identity function.
671 empty_val - The value to use when initializing this object or when calling
672 reset().
673 required(bool) - True iff this config item is required to have a
674 non-empty_val in order for it to be considered complete().
675 hidden - See ConfigBase.
676 """
677 super(Single, self).__init__(hidden)
678 self.inner_type = inner_type
679 self.jsonish_fn = jsonish_fn
680 self.empty_val = empty_val
681 self.data = empty_val
682 self.required = required
683
684 def get_val(self):
685 return self.data
686
687 def set_val(self, val):
688 if isinstance(val, Single):
689 val = val.data
690 if val is not self.empty_val:
691 typeAssert(val, self.inner_type)
692 self.data = val
693
694 def as_jsonish(self, _include_hidden=None):
695 return self.jsonish_fn(self.data)
696
697 def reset(self):
698 self.data = self.empty_val
699
700 def complete(self):
701 return not self.required or self.data is not self.empty_val
702
703 def _is_default(self):
704 return self.data is self.empty_val
705
706
707 class Static(ConfigBase):
708 """Holds a single, hidden, immutible data object.
709
710 This is very useful for holding the 'input' configuration values.
711 """
712
713 def __init__(self, value, hidden=AutoHide):
714 super(Static, self).__init__(hidden=hidden)
715 # Attempt to hash the value, which will ensure that it's immutable all the
716 # way down :).
717 hash(value)
718 self.data = value
719
720 def get_val(self):
721 return self.data
722
723 def set_val(self, val):
724 raise TypeError("Cannot assign to a Static config member")
725
726 def as_jsonish(self, _include_hidden=None):
727 return self.data
728
729 def reset(self):
730 assert False
731
732 def complete(self):
733 return True
734
735 def _is_default(self):
736 return True
OLDNEW
« 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