OLD | NEW |
| (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 | |
OLD | NEW |