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