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, config_vars=None): | |
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 config_vars(dict) - A dictionary mapping of { CONFIG_VAR: <value> }. This | |
181 sets the input contidions for the CONFIG_SCHEMA. | |
182 | |
183 Returns a new decorated version of this function (see inner()). | |
184 """ | |
185 def decorator(f): | |
186 name = f.__name__ | |
187 @functools.wraps(f) | |
188 def inner(config=None, final=True, optional=False, **kwargs): | |
189 """This is the function which is returned from the config_ctx | |
190 decorator. | |
191 | |
192 It applies all of the logic mentioned in the config_ctx docstring | |
193 above, and alters the function signature slightly. | |
194 | |
195 Args: | |
196 config - The config blob that we intend to manipulate. This is passed | |
197 through to the function after checking deps and including includes. | |
198 After the function manipulates it, it is automatically returned. | |
199 | |
200 final(bool) - Set to True by default, this will record the application | |
201 of this config_ctx to `config', which will prevent the config_ctx | |
202 from being applied to `config' again. It also is used to see if the | |
203 config blob satisfies deps for subsequent config_ctx applications | |
204 (i.e. in order for a config_ctx to satisfy a dependency, it must | |
205 be applied with final=True). | |
206 | |
207 This is useful to apply default values while allowing the config to | |
208 later override those values. | |
209 | |
210 However, it's best if each config_ctx is final, because then you | |
211 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 | |
213 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. | |
215 | |
216 **kwargs - Passed through to the decorated function without harm. | |
217 | |
218 Returns config and ignores the return value of the decorated function. | |
219 """ | |
220 if config is None: | |
221 config = config_ctx.CONFIG_SCHEMA() | |
222 assert isinstance(config, ConfigGroup) | |
223 inclusions = config._inclusions # pylint: disable=W0212 | |
224 | |
225 # 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 | |
227 config_ctx.ROOT_CONFIG_ITEM.__name__ not in inclusions): | |
228 config_ctx.ROOT_CONFIG_ITEM(config) | |
229 | |
230 if name in inclusions: | |
231 if optional: | |
232 return config | |
233 raise BadConf('config_ctx "%s" is already in this config "%s"' % | |
234 (name, config.as_jsonish(include_hidden=True))) | |
235 if final: | |
236 inclusions.add(name) | |
237 | |
238 for include in (includes or []): | |
239 if include in inclusions: | |
240 continue | |
241 try: | |
242 config_ctx.CONFIG_ITEMS[include](config) | |
243 except BadConf, e: | |
244 raise BadConf('config "%s" includes "%s", but [%s]' % | |
245 (name, include, e)) | |
246 | |
247 # deps are a list of group names. All groups must be represented | |
248 # in config already. | |
249 for dep_group in (deps or []): | |
250 if not (inclusions & config_ctx.MUTEX_GROUPS[dep_group]): | |
251 raise BadConf('dep group "%s" is unfulfilled for "%s"' % | |
252 (dep_group, name)) | |
253 | |
254 if group: | |
255 overlap = inclusions & config_ctx.MUTEX_GROUPS[group] | |
256 overlap.discard(name) | |
257 if overlap: | |
258 raise BadConf('"%s" is a member of group "%s", but %s already ran' % | |
259 (name, group, tuple(overlap))) | |
260 | |
261 ret = f(config, **kwargs) | |
262 assert ret is None, 'Got return value (%s) from "%s"?' % (ret, name) | |
263 | |
264 return config | |
265 | |
266 def default_config_vars(): | |
267 ret = {} | |
268 for include in (includes or []): | |
269 item = config_ctx.CONFIG_ITEMS[include] | |
270 ret.update(item.DEFAULT_CONFIG_VARS()) | |
271 if config_vars: | |
272 ret.update(config_vars) | |
273 return ret | |
274 inner.DEFAULT_CONFIG_VARS = default_config_vars | |
275 | |
276 assert name not in config_ctx.CONFIG_ITEMS | |
277 config_ctx.CONFIG_ITEMS[name] = inner | |
278 if group: | |
279 config_ctx.MUTEX_GROUPS.setdefault(group, set()).add(name) | |
280 inner.IS_ROOT = is_root | |
281 if is_root: | |
282 assert not config_ctx.ROOT_CONFIG_ITEM, ( | |
283 'may only have one root config_ctx!') | |
284 config_ctx.ROOT_CONFIG_ITEM = inner | |
285 inner.IS_ROOT = True | |
286 inner.NO_TEST = no_test or bool(deps) | |
287 return inner | |
288 return decorator | |
289 | |
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 | |
298 def formatter(obj, ext=None): | |
299 '''Converts format obj to a function taking var assignments. | |
300 | |
301 Args: | |
302 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). | |
304 Otherwise obj will be invoked with assignments, and expected to return | |
305 a fully-rendered string. | |
306 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 | |
308 the string doesn't end with ext, it will be appended. | |
309 ''' | |
310 def inner(var_assignments): | |
311 ret = '' | |
312 if isinstance(obj, basestring): | |
313 ret = obj % var_assignments | |
314 else: | |
315 ret = obj(var_assignments) | |
316 if ext and not ret.endswith(ext): | |
317 ret += ext | |
318 return ret | |
319 return inner | |
320 config_ctx.TEST_NAME_FORMAT = formatter(TEST_NAME_FORMAT) | |
321 config_ctx.TEST_FILE_FORMAT = formatter( | |
322 (TEST_FILE_FORMAT or TEST_NAME_FORMAT), ext='.json') | |
323 return config_ctx | |
324 | |
325 | |
326 class AutoHide(object): | |
327 pass | |
328 AutoHide = AutoHide() | |
329 | |
330 class ConfigBase(object): | |
331 """This is the root interface for all config schema types.""" | |
332 | |
333 def __init__(self, hidden=AutoHide): | |
334 """ | |
335 Args: | |
336 hidden - | |
337 True: This object will be excluded from printing when the config blob | |
338 is rendered with ConfigGroup.as_jsonish(). You still have full | |
339 read/write access to this blob otherwise though. | |
340 False: This will be printed as part of ConfigGroup.as_jsonish() | |
341 AutoHide: This will be printed as part of ConfigGroup.as_jsonish() only | |
342 if self._is_default() is false. | |
343 """ | |
344 # work around subclasses which override __setattr__ | |
345 object.__setattr__(self, '_hidden_mode', hidden) | |
346 object.__setattr__(self, '_inclusions', set()) | |
347 | |
348 def get_val(self): | |
349 """Gets the native value of this config object.""" | |
350 return self | |
351 | |
352 def set_val(self, val): | |
353 """Resets the value of this config object using data in val.""" | |
354 raise NotImplementedError | |
355 | |
356 def reset(self): | |
357 """Resets the value of this config object to it's initial state.""" | |
358 raise NotImplementedError | |
359 | |
360 def as_jsonish(self, include_hidden=False): | |
361 """Returns the value of this config object as simple types.""" | |
362 raise NotImplementedError | |
363 | |
364 def complete(self): | |
365 """Returns True iff this configuraton blob is fully viable.""" | |
366 raise NotImplementedError | |
367 | |
368 def _is_default(self): | |
369 """Returns True iff this configuraton blob is the default value.""" | |
370 raise NotImplementedError | |
371 | |
372 @property | |
373 def _hidden(self): | |
374 """Returns True iff this configuraton blob is hidden.""" | |
375 if self._hidden_mode is AutoHide: | |
376 return self._is_default() | |
377 return self._hidden_mode | |
378 | |
379 | |
380 class ConfigGroup(ConfigBase): | |
381 """Allows you to provide hierarchy to a configuration schema. | |
382 | |
383 Example usage: | |
384 config_blob = ConfigGroup( | |
385 some_item = SimpleConfig(str), | |
386 group = ConfigGroup( | |
387 numbahs = SetConfig(int), | |
388 ), | |
389 ) | |
390 config_blob.some_item = "hello" | |
391 config_blob.group.numbahs.update(range(10)) | |
392 """ | |
393 | |
394 def __init__(self, hidden=AutoHide, **type_map): | |
395 """Expects type_map to be {python_name -> ConfigBase} instance.""" | |
396 super(ConfigGroup, self).__init__(hidden) | |
397 assert type_map, 'A ConfigGroup with no type_map is meaningless.' | |
398 | |
399 object.__setattr__(self, '_type_map', type_map) | |
400 for name, typeval in self._type_map.iteritems(): | |
401 assert isinstance(typeval, ConfigBase) | |
402 object.__setattr__(self, name, typeval) | |
403 | |
404 def __getattribute__(self, name): | |
405 obj = object.__getattribute__(self, name) | |
406 if isinstance(obj, ConfigBase): | |
407 return obj.get_val() | |
408 else: | |
409 return obj | |
410 | |
411 def __setattr__(self, name, val): | |
412 obj = object.__getattribute__(self, name) | |
413 assert isinstance(obj, ConfigBase) | |
414 obj.set_val(val) | |
415 | |
416 def __delattr__(self, name): | |
417 obj = object.__getattribute__(self, name) | |
418 assert isinstance(obj, ConfigBase) | |
419 obj.reset() | |
420 | |
421 def set_val(self, val): | |
422 if isinstance(val, ConfigBase): | |
423 val = val.as_jsonish(include_hidden=True) | |
424 assert isinstance(val, dict) | |
425 for name, config_obj in self._type_map.iteritems(): | |
426 if name in val: | |
427 config_obj.set_val(val.pop()) | |
428 assert not val, "Got extra keys while setting ConfigGroup: %s" % val | |
429 | |
430 def as_jsonish(self, include_hidden=False): | |
431 return dict( | |
432 (n, v.as_jsonish(include_hidden)) for n, v in self._type_map.iteritems() | |
433 if (include_hidden or not v._hidden)) # pylint: disable=W0212 | |
434 | |
435 def reset(self): | |
436 for v in self._type_map.values(): | |
437 v.reset() | |
438 | |
439 def complete(self): | |
440 return all(v.complete() for v in self._type_map.values()) | |
441 | |
442 def _is_default(self): | |
443 # pylint: disable=W0212 | |
444 return all(v._is_default() for v in self._type_map.values()) | |
445 | |
446 | |
447 class ConfigList(ConfigBase, collections.MutableSequence): | |
448 """Allows you to provide an ordered repetition to a configuration schema. | |
449 | |
450 Example usage: | |
451 config_blob = ConfigGroup( | |
452 some_items = ConfigList( | |
453 ConfigGroup( | |
454 herp = SimpleConfig(int), | |
455 derp = SimpleConfig(str) | |
456 ) | |
457 ) | |
458 ) | |
459 config_blob.some_items.append({'herp': 1}) | |
460 config_blob.some_items[0].derp = 'bob' | |
461 """ | |
462 | |
463 def __init__(self, item_schema, hidden=AutoHide): | |
464 """ | |
465 Args: | |
466 item_schema: The schema of each object. Should be a function which returns | |
467 an instance of ConfigGroup. | |
468 """ | |
469 super(ConfigList, self).__init__(hidden=hidden) | |
470 assert isinstance(item_schema, types.FunctionType) | |
471 assert isinstance(item_schema(), ConfigGroup) | |
472 self.item_schema = item_schema | |
473 self.data = [] | |
474 | |
475 def __getitem__(self, index): | |
476 return self.data.__getitem__(index) | |
477 | |
478 def __setitem__(self, index, value): | |
479 datum = self.item_schema() | |
480 datum.set_val(value) | |
481 return self.data.__setitem__(index, datum) | |
482 | |
483 def __delitem__(self, index): | |
484 return self.data.__delitem__(index) | |
485 | |
486 def __len__(self): | |
487 return len(self.data) | |
488 | |
489 def insert(self, index, value): | |
490 datum = self.item_schema() | |
491 datum.set_val(value) | |
492 return self.data.insert(index, datum) | |
493 | |
494 def add(self): | |
495 self.append({}) | |
496 return self[-1] | |
497 | |
498 def reset(self): | |
499 self.data = [] | |
500 | |
501 def complete(self): | |
502 return all(i.complete() for i in self.data) | |
503 | |
504 def set_val(self, data): | |
505 if isinstance(data, ConfigList): | |
506 data = data.as_jsonish(include_hidden=True) | |
507 assert isinstance(data, list) | |
508 self.reset() | |
509 for item in data: | |
510 self.append(item) | |
511 | |
512 def as_jsonish(self, include_hidden=False): | |
513 return [i.as_jsonish(include_hidden) for i in self.data | |
514 if (include_hidden or not i._hidden)] # pylint: disable=W0212 | |
515 | |
516 def _is_default(self): | |
517 # pylint: disable=W0212 | |
518 return all(v._is_default() for v in self.data) | |
519 | |
520 | |
521 class Dict(ConfigBase, collections.MutableMapping): | |
522 """Provides a semi-homogenous dict()-like configuration object.""" | |
523 | |
524 def __init__(self, item_fn=lambda i: i, jsonish_fn=dict, value_type=None, | |
525 hidden=AutoHide): | |
526 """ | |
527 Args: | |
528 item_fn - A function which renders (k, v) pairs to input items for | |
529 jsonish_fn. Defaults to the identity function. | |
530 jsonish_fn - A function which renders a list of outputs of item_fn to a | |
531 JSON-compatiple python datatype. Defaults to dict(). | |
532 value_type - A type object used for constraining/validating the values | |
533 assigned to this dictionary. | |
534 hidden - See ConfigBase. | |
535 """ | |
536 super(Dict, self).__init__(hidden) | |
537 self.value_type = value_type | |
538 self.item_fn = item_fn | |
539 self.jsonish_fn = jsonish_fn | |
540 self.data = {} | |
541 | |
542 def __getitem__(self, k): | |
543 return self.data.__getitem__(k) | |
544 | |
545 def __setitem__(self, k, v): | |
546 if self.value_type: | |
547 assert isinstance(v, self.value_type) | |
548 return self.data.__setitem__(k, v) | |
549 | |
550 def __delitem__(self, k): | |
551 return self.data.__delitem__(k) | |
552 | |
553 def __iter__(self): | |
554 return iter(self.data) | |
555 | |
556 def __len__(self): | |
557 return len(self.data) | |
558 | |
559 def set_val(self, val): | |
560 if isinstance(val, Dict): | |
561 val = val.data | |
562 assert isinstance(val, dict) | |
563 assert all(isinstance(v, self.value_type) for v in val.itervalues()) | |
564 self.data = val | |
565 | |
566 def as_jsonish(self, _include_hidden=None): | |
567 return self.jsonish_fn(map( | |
568 self.item_fn, sorted(self.data.iteritems(), key=lambda x: x[0]))) | |
569 | |
570 def reset(self): | |
571 self.data.clear() | |
572 | |
573 def complete(self): | |
574 return True | |
575 | |
576 def _is_default(self): | |
577 return not self.data | |
578 | |
579 | |
580 class List(ConfigBase, collections.MutableSequence): | |
581 """Provides a semi-homogenous list()-like configuration object.""" | |
582 | |
583 def __init__(self, inner_type, jsonish_fn=list, hidden=AutoHide): | |
584 """ | |
585 Args: | |
586 inner_type - The type of data contained in this set, e.g. str, int, ... | |
587 Can also be a tuple of types to allow more than one type. | |
588 jsonish_fn - A function used to reduce the list() to a JSON-compatible | |
589 python datatype. Defaults to list(). | |
590 hidden - See ConfigBase. | |
591 """ | |
592 super(List, self).__init__(hidden) | |
593 self.inner_type = inner_type | |
594 self.jsonish_fn = jsonish_fn | |
595 self.data = [] | |
596 | |
597 def __getitem__(self, index): | |
598 return self.data[index] | |
599 | |
600 def __setitem__(self, index, value): | |
601 assert isinstance(value, self.inner_type) | |
602 self.data[index] = value | |
603 | |
604 def __delitem__(self, index): | |
605 del self.data | |
606 | |
607 def __len__(self): | |
608 return len(self.data) | |
609 | |
610 def __radd__(self, other): | |
611 if not isinstance(other, list): | |
612 other = list(other) | |
613 return other + self.data | |
614 | |
615 def insert(self, index, value): | |
616 assert isinstance(value, self.inner_type) | |
617 self.data.insert(index, value) | |
618 | |
619 def set_val(self, val): | |
620 assert all(isinstance(v, self.inner_type) for v in val) | |
621 self.data = list(val) | |
622 | |
623 def as_jsonish(self, _include_hidden=None): | |
624 return self.jsonish_fn(self.data) | |
625 | |
626 def reset(self): | |
627 self.data = [] | |
628 | |
629 def complete(self): | |
630 return True | |
631 | |
632 def _is_default(self): | |
633 return not self.data | |
634 | |
635 | |
636 class Set(ConfigBase, collections.MutableSet): | |
637 """Provides a semi-homogenous set()-like configuration object.""" | |
638 | |
639 def __init__(self, inner_type, jsonish_fn=list, hidden=AutoHide): | |
640 """ | |
641 Args: | |
642 inner_type - The type of data contained in this set, e.g. str, int, ... | |
643 Can also be a tuple of types to allow more than one type. | |
644 jsonish_fn - A function used to reduce the set() to a JSON-compatible | |
645 python datatype. Defaults to list(). | |
646 hidden - See ConfigBase. | |
647 """ | |
648 super(Set, self).__init__(hidden) | |
649 self.inner_type = inner_type | |
650 self.jsonish_fn = jsonish_fn | |
651 self.data = set() | |
652 | |
653 def __contains__(self, val): | |
654 return val in self.data | |
655 | |
656 def __iter__(self): | |
657 return iter(self.data) | |
658 | |
659 def __len__(self): | |
660 return len(self.data) | |
661 | |
662 def add(self, value): | |
663 assert isinstance(value, self.inner_type) | |
664 self.data.add(value) | |
665 | |
666 def discard(self, value): | |
667 self.data.discard(value) | |
668 | |
669 def set_val(self, val): | |
670 assert all(isinstance(v, self.inner_type) for v in val) | |
671 self.data = set(val) | |
672 | |
673 def as_jsonish(self, _include_hidden=None): | |
674 return self.jsonish_fn(sorted(self.data)) | |
675 | |
676 def reset(self): | |
677 self.data = set() | |
678 | |
679 def complete(self): | |
680 return True | |
681 | |
682 def _is_default(self): | |
683 return not self.data | |
684 | |
685 | |
686 class Single(ConfigBase): | |
687 """Provides a configuration object which holds a single 'simple' type.""" | |
688 | |
689 def __init__(self, inner_type, jsonish_fn=lambda x: x, empty_val=None, | |
690 required=True, hidden=AutoHide): | |
691 """ | |
692 Args: | |
693 inner_type - The type of data contained in this object, e.g. str, int, ... | |
694 Can also be a tuple of types to allow more than one type. | |
695 jsonish_fn - A function used to reduce the data to a JSON-compatible | |
696 python datatype. Default is the identity function. | |
697 emtpy_val - The value to use when initializing this object or when calling | |
698 reset(). | |
699 required(bool) - True iff this config item is required to have a | |
700 non-empty_val in order for it to be considered complete(). | |
701 hidden - See ConfigBase. | |
702 """ | |
703 super(Single, self).__init__(hidden) | |
704 self.inner_type = inner_type | |
705 self.jsonish_fn = jsonish_fn | |
706 self.empty_val = empty_val | |
707 self.data = empty_val | |
708 self.required = required | |
709 | |
710 def get_val(self): | |
711 return self.data | |
712 | |
713 def set_val(self, val): | |
714 if isinstance(val, Single): | |
715 val = val.data | |
716 assert val is self.empty_val or isinstance(val, self.inner_type) | |
717 self.data = val | |
718 | |
719 def as_jsonish(self, _include_hidden=None): | |
720 return self.jsonish_fn(self.data) | |
721 | |
722 def reset(self): | |
723 self.data = self.empty_val | |
724 | |
725 def complete(self): | |
726 return not self.required or self.data is not self.empty_val | |
727 | |
728 def _is_default(self): | |
729 return self.data is self.empty_val | |
730 | |
731 | |
732 class Static(ConfigBase): | |
733 """Holds a single, hidden, immutible data object. | |
734 | |
735 This is very useful for holding the 'input' configuration values (i.e. those | |
736 which are in your VAR_TEST_MAP). | |
737 """ | |
738 | |
739 def __init__(self, value, hidden=AutoHide): | |
740 super(Static, self).__init__(hidden=hidden) | |
741 # Attempt to hash the value, which will ensure that it's immutable all the | |
742 # way down :). | |
743 hash(value) | |
744 self.data = value | |
745 | |
746 def get_val(self): | |
747 return self.data | |
748 | |
749 def set_val(self, val): | |
750 assert False | |
751 | |
752 def as_jsonish(self, _include_hidden=None): | |
753 return self.data | |
754 | |
755 def reset(self): | |
756 assert False | |
757 | |
758 def complete(self): | |
759 return True | |
760 | |
761 def _is_default(self): | |
762 return True | |
OLD | NEW |