1 # -*- coding: utf-8 -*-
2 "option types and option description for the configuration management"
3 # Copyright (C) 2012 Team tiramisu (see AUTHORS for all contributors)
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 2 of the License, or
8 # (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License
16 # along with this program; if not, write to the Free Software
17 # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19 # The original `Config` design model is unproudly borrowed from
20 # the rough pypy's guys: http://codespeak.net/svn/pypy/dist/pypy/config/
21 # the whole pypy projet is under MIT licence
22 # ____________________________________________________________
23 from types import FunctionType
24 from tiramisu.basetype import HiddenBaseType, DisabledBaseType
25 from tiramisu.error import (ConfigError, ConflictConfigError, NotFoundError,
26 RequiresError, RequirementRecursionError, MandatoryError,
27 PropertiesOptionError)
28 from tiramisu.autolib import carry_out_calculation
29 from tiramisu.setting import groups, owners
31 requires_actions = [('hide', 'show'), ('enable', 'disable'), ('freeze', 'unfreeze')]
33 available_actions = []
35 for act1, act2 in requires_actions:
36 available_actions.extend([act1, act2])
37 reverse_actions[act1] = act2
38 reverse_actions[act2] = act1
39 # ____________________________________________________________
43 """multi options values container
44 that support item notation for the values of multi options"""
45 def __init__(self, lst, config, opt):
47 :param lst: the Multi wraps a list value
48 :param config: the parent config
49 :param opt: the option object that have this Multi value
53 super(Multi, self).__init__(lst)
55 def __setitem__(self, key, value):
56 self._setvalue(value, key,
57 who=self.config._cfgimpl_context._cfgimpl_settings.get_owner())
59 def append(self, value):
60 """the list value can be updated (appened)
61 only if the option is a master
64 who=self.config._cfgimpl_context._cfgimpl_settings.get_owner())
66 def _setvalue(self, value, key=None, who=None):
68 if not self.opt._validate(value):
69 raise ConfigError("invalid value {0} "
70 "for option {1}".format(str(value), self.opt._name))
73 super(Multi, self).append(value)
75 super(Multi, self).__setitem__(key, value)
77 if not isinstance(who, owners.Owner):
78 raise TypeError("invalid owner {0} for the value {1}".format(
79 str(who), str(value)))
80 self.opt.setowner(self.config, getattr(owners, who))
81 self.config._cfgimpl_context._cfgimpl_values.previous_values[self.opt] = oldvalue
84 """the list value can be updated (poped)
85 only if the option is a master
87 :param key: index of the element to pop
88 :return: the requested element
91 self.opt.setowner(self.config,
92 self.config._cfgimpl_context._cfgimpl_settings.get_owner())
93 self.config._cfgimpl_context._cfgimpl_values.previous_values[self.opt] = list(self)
94 return super(Multi, self).pop(key)
95 # ____________________________________________________________
97 class Option(HiddenBaseType, DisabledBaseType):
99 Abstract base class for configuration option's.
101 Reminder: an Option object is **not** a container for the value
103 #freeze means: cannot modify the value of an Option once set
105 #if an Option has been frozen, shall return the default value
106 _force_default_on_freeze = False
107 def __init__(self, name, doc, default=None, default_multi=None,
108 requires=None, mandatory=False, multi=False, callback=None,
109 callback_params=None, validator=None, validator_args={}):
111 :param name: the option's name
112 :param doc: the option's description
113 :param default: specifies the default value of the option,
114 for a multi : ['bla', 'bla', 'bla']
115 :param default_multi: 'bla' (used in case of a reset to default only at
117 :param requires: is a list of names of options located anywhere
118 in the configuration.
119 :param multi: if true, the option's value is a list
120 :param callback: the name of a function. If set, the function's output
121 is responsible of the option's value
122 :param callback_params: the callback's parameter
123 :param validator: the name of a function wich stands for a custom
124 validation of the value
125 :param validator_args: the validator's parameters
129 self._requires = requires
130 self._mandatory = mandatory
132 self._validator = None
133 self._validator_args = None
134 if validator is not None:
135 if type(validator) != FunctionType:
136 raise TypeError("validator must be a function")
137 self._validator = validator
138 if validator_args is not None:
139 self._validator_args = validator_args
140 if not self.multi and default_multi is not None:
141 raise ConfigError("a default_multi is set whereas multi is False"
142 " in option: {0}".format(name))
143 if default_multi is not None and not self._validate(default_multi):
144 raise ConfigError("invalid default_multi value {0} "
145 "for option {1}".format(str(default_multi), name))
146 self.default_multi = default_multi
147 #if self.multi and default_multi is None:
148 # _cfgimpl_warnings[name] = DefaultMultiWarning
149 if callback is not None and (default is not None or default_multi is not None):
150 raise ConfigError("defaut values not allowed if option: {0} "
151 "is calculated".format(name))
152 self.callback = callback
153 if self.callback is None and callback_params is not None:
154 raise ConfigError("params defined for a callback function but"
155 " no callback defined yet for option {0}".format(name))
156 self.callback_params = callback_params
157 if self.multi == True:
160 if not isinstance(default, list):
161 raise ConfigError("invalid default value {0} "
162 "for option {1} : not list type".format(str(default), name))
163 if not self.validate(default, False):
164 raise ConfigError("invalid default value {0} "
165 "for option {1}".format(str(default), name))
167 if default != None and not self.validate(default, False):
168 raise ConfigError("invalid default value {0} "
169 "for option {1}".format(str(default), name))
170 self.default = default
171 self.properties = [] # 'hidden', 'disabled'...
173 def validate(self, value, validate=True):
175 :param value: the option's value
176 :param validate: if true enables ``self._validator`` validation
178 # generic calculation
179 if self.multi == False:
180 # None allows the reset of the value
182 # customizing the validator
183 if validate and self._validator is not None and \
184 not self._validator(value, **self._validator_args):
186 return self._validate(value)
188 if not isinstance(value, list):
189 raise ConfigError("invalid value {0} "
190 "for option {1} which must be a list".format(value,
193 # None allows the reset of the value
195 # customizing the validator
196 if validate and self._validator is not None and \
197 not self._validator(val, **self._validator_args):
199 if not self._validate(val):
203 def getdefault(self, default_multi=False):
204 "accessing the default value"
205 if default_multi == False or not self.is_multi():
208 return self.getdefault_multi()
210 def getdefault_multi(self):
211 "accessing the default value for a multi"
212 return self.default_multi
214 def is_empty_by_default(self):
215 "no default value has been set yet"
216 if ((not self.is_multi() and self.default == None) or
217 (self.is_multi() and (self.default == [] or None in self.default))):
221 def force_default(self):
222 "if an Option has been frozen, shall return the default value"
223 self._force_default_on_freeze = True
225 def hascallback_and_isfrozen():
226 return self._frozen and self.has_callback()
228 def is_forced_on_freeze(self):
229 "if an Option has been frozen, shall return the default value"
230 return self._frozen and self._force_default_on_freeze
233 "accesses the Option's doc"
236 def getcallback(self):
237 "a callback is only a link, the name of an external hook"
240 def has_callback(self):
241 "to know if a callback has been defined or not"
242 if self.callback == None:
247 def getcallback_value(self, config):
248 return carry_out_calculation(self._name,
249 option=self, config=config)
251 def getcallback_params(self):
252 "if a callback has been defined, returns his arity"
253 return self.callback_params
255 def setowner(self, config, owner):
257 :param config: *must* be only the **parent** config
258 (not the toplevel config)
259 :param owner: is a **real** owner, that is an object
260 that lives in setting.owners
263 if not isinstance(owner, owners.Owner):
264 raise ConfigError("invalid type owner for option: {0}".format(
266 config._cfgimpl_context._cfgimpl_values.owners[self] = owner
268 def getowner(self, config):
269 "config *must* be only the **parent** config (not the toplevel config)"
270 return config._cfgimpl_context._cfgimpl_values.owners[self]
272 def reset(self, config):
273 """resets the default value and owner
275 config.setoption(self._name, self.getdefault(), owners.default)
277 def is_default_owner(self, config):
279 :param config: *must* be only the **parent** config
280 (not the toplevel config)
283 return self.getowner(config) == owners.default
285 def setoption(self, config, value):
286 """changes the option's value with the value_owner's who
287 :param config: the parent config is necessary here to store the value
290 rootconfig = config._cfgimpl_get_toplevel()
291 if not self.validate(value,
292 config._cfgimpl_context._cfgimpl_settings.validator):
293 raise ConfigError('invalid value %s for option %s' % (value, name))
294 if self.is_mandatory():
295 # value shall not be '' for a mandatory option
296 # so '' is considered as being None
297 if not self.is_multi() and value == '':
299 if self.is_multi() and '' in value:
300 value = Multi([{'': None}.get(i, i) for i in value], config, self)
301 if config._cfgimpl_context._cfgimpl_settings.is_mandatory() \
302 and ((self.is_multi() and value == []) or \
303 (not self.is_multi() and value is None)):
304 raise MandatoryError('cannot change the value to %s for '
305 'option %s' % (value, name))
306 if self not in config._cfgimpl_context._cfgimpl_values:
307 raise AttributeError('unknown option %s' % (name))
309 if config._cfgimpl_context._cfgimpl_settings.is_frozen_for_everything():
310 raise TypeError("cannot set a value to the option {} if the whole "
311 "config has been frozen".format(name))
313 if config._cfgimpl_context._cfgimpl_settings.is_frozen() \
314 and self.is_frozen():
315 raise TypeError('cannot change the value to %s for '
316 'option %s this option is frozen' % (str(value), name))
317 apply_requires(self, config)
318 if type(config._cfgimpl_context._cfgimpl_values[self]) == Multi:
319 config._cfgimpl_context._cfgimpl_values.previous_values[self] = list(config._cfgimpl_context._cfgimpl_values[self])
321 config._cfgimpl_context._cfgimpl_values.previous_values[self] = config._cfgimpl_context._cfgimpl_values[self]
322 config._cfgimpl_context._cfgimpl_values[self] = value
324 def getkey(self, value):
326 # ____________________________________________________________
335 # ____________________________________________________________
338 def is_mandatory(self):
339 return self._mandatory
341 class ChoiceOption(Option):
344 def __init__(self, name, doc, values, default=None, default_multi=None,
345 requires=None, mandatory=False, multi=False, callback=None,
346 callback_params=None, open_values=False, validator=None,
349 if open_values not in [True, False]:
350 raise ConfigError('Open_values must be a boolean for '
352 self.open_values = open_values
353 super(ChoiceOption, self).__init__(name, doc, default=default,
354 default_multi=default_multi, callback=callback,
355 callback_params=callback_params, requires=requires,
356 multi=multi, mandatory=mandatory, validator=validator,
357 validator_args=validator_args)
359 def _validate(self, value):
360 if not self.open_values:
361 return value is None or value in self.values
365 class BoolOption(Option):
368 def _validate(self, value):
369 return isinstance(value, bool)
371 class IntOption(Option):
374 def _validate(self, value):
375 return isinstance(value, int)
377 class FloatOption(Option):
380 def _validate(self, value):
381 return isinstance(value, float)
383 class StrOption(Option):
386 def _validate(self, value):
387 return isinstance(value, str)
389 class SymLinkOption(object):
392 def __init__(self, name, path, opt):
397 def setoption(self, config, value):
398 setattr(config, self.path, value)
400 def __getattr__(self, name):
401 if name in ('_name', 'path', 'opt', 'setoption'):
402 return self.__dict__[name]
404 return getattr(self.opt, name)
406 class IPOption(Option):
409 def _validate(self, value):
410 # by now the validation is nothing but a string, use IPy instead
411 return isinstance(value, str)
413 class NetmaskOption(Option):
416 def _validate(self, value):
417 # by now the validation is nothing but a string, use IPy instead
418 return isinstance(value, str)
420 class OptionDescription(HiddenBaseType, DisabledBaseType):
421 """Config's schema (organisation, group) and container of Options"""
422 # the group_type is useful for filtering OptionDescriptions in a config
423 group_type = groups.default
424 def __init__(self, name, doc, children, requires=None):
426 :param children: is a list of option descriptions (including
427 ``OptionDescription`` instances for nested namespaces).
431 self._children = children
432 self._requires = requires
434 self.properties = [] # 'hidden', 'disabled'...
440 for child in self._children:
441 setattr(self, child._name, child)
443 def add_child(self, child):
444 "dynamically adds a configuration option"
445 #Nothing is static. Even the Mona Lisa is falling apart.
446 for ch in self._children:
447 if isinstance(ch, Option):
448 if child._name == ch._name:
449 raise ConflictConfigError("existing option : {0}".format(
451 self._children.append(child)
452 setattr(self, child._name, child)
454 def update_child(self, child):
455 "modification of an existing option"
456 # XXX : corresponds to the `redefine`, is it usefull
459 def getkey(self, config):
460 return tuple([child.getkey(getattr(config, child._name))
461 for child in self._children])
463 def getpaths(self, include_groups=False, currpath=None):
464 """returns a list of all paths in self, recursively
465 currpath should not be provided (helps with recursion)
470 for option in self._children:
472 if attr.startswith('_cfgimpl'):
474 if isinstance(option, OptionDescription):
476 paths.append('.'.join(currpath + [attr]))
477 currpath.append(attr)
478 paths += option.getpaths(include_groups=include_groups,
482 paths.append('.'.join(currpath + [attr]))
484 # ____________________________________________________________
485 def set_group_type(self, group_type):
486 """sets a given group object to an OptionDescription
488 :param group_type: an instance of `GroupType` or `MasterGroupType`
489 that lives in `setting.groups`
491 if isinstance(group_type, groups.GroupType):
492 self.group_type = group_type
493 if isinstance(group_type, groups.MasterGroupType):
494 identical_master_child_name = False
495 for child in self._children:
496 if isinstance(child, OptionDescription):
497 raise ConfigError("master group {} shall not have "
498 "a subgroup".format(self._name))
500 raise ConfigError("not allowed option {0} in group {1}"
501 ": this option is not a multi".format(child._name,
503 if child._name == self._name:
504 identical_master_child_name = True
505 if not identical_master_child_name:
506 raise ConfigError("the master group: {} has not any "
507 "master child".format(self._name))
509 raise ConfigError('not allowed group_type : {0}'.format(group_type))
511 def get_group_type(self):
512 return self.group_type
513 # ____________________________________________________________
516 super(OptionDescription, self).hide()
517 for child in self._children:
518 if isinstance(child, OptionDescription):
521 super(OptionDescription, self).show()
522 for child in self._children:
523 if isinstance(child, OptionDescription):
527 super(OptionDescription, self).disable()
528 for child in self._children:
529 if isinstance(child, OptionDescription):
532 super(OptionDescription, self).enable()
533 for child in self._children:
534 if isinstance(child, OptionDescription):
536 # ____________________________________________________________
538 def validate_requires_arg(requires, name):
539 "malformed requirements"
542 if not type(req) == tuple and len(req) != 3:
543 raise RequiresError("malformed requirements for option:"
546 if action not in available_actions:
547 raise RequiresError("malformed requirements for option: {0}"
548 " unknown action: {1}".format(name, action))
549 if reverse_actions[action] in config_action:
550 raise RequiresError("inconsistency in action types for option: {0}"
551 " action: {1} in contradiction with {2}\n"
552 " ({3})".format(name, action,
553 reverse_actions[action], requires))
554 config_action.append(action)
556 def build_actions(requires):
557 "action are hide, show, enable, disable..."
559 for require in requires:
561 trigger_actions.setdefault(action, []).append(require)
562 return trigger_actions
564 def apply_requires(opt, config, permissive=False):
565 "carries out the jit (just in time requirements between options"
566 if hasattr(opt, '_requires') and opt._requires is not None:
567 rootconfig = config._cfgimpl_get_toplevel()
568 validate_requires_arg(opt._requires, opt._name)
569 # filters the callbacks
570 trigger_actions = build_actions(opt._requires)
571 for requires in trigger_actions.values():
573 for require in requires:
574 name, expected, action = require
575 path = config._cfgimpl_get_path() + '.' + opt._name
576 if name.startswith(path):
577 raise RequirementRecursionError("malformed requirements "
578 "imbrication detected for option: '{0}' "
579 "with requirement on: '{1}'".format(path, name))
580 homeconfig, shortname = rootconfig._cfgimpl_get_home_by_path(name)
582 value = homeconfig._getattr(shortname, permissive=True)
583 except PropertiesOptionError, err:
584 properties = err.proptype
587 config._cfgimpl_context._cfgimpl_settings.permissive:
588 if perm in properties:
589 properties.remove(perm)
591 raise NotFoundError("option '{0}' has requirement's property error: "
592 "{1} {2}".format(opt._name, name, properties))
593 except Exception, err:
594 raise NotFoundError("required option not found: "
596 if value == expected:
597 getattr(opt, action)() #.hide() or show() or...
598 # FIXME generic programming opt.property_launch(action, False)
600 # no callback has been triggered, then just reverse the action
602 getattr(opt, reverse_actions[action])()