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 tiramisu.basetype import HiddenBaseType, DisabledBaseType
24 from tiramisu.error import (ConfigError, ConflictConfigError, NotFoundError,
25 RequiresError, RequirementRecursionError, MandatoryError)
26 requires_actions = [('hide', 'show'), ('enable', 'disable'), ('freeze', 'unfreeze')]
28 available_actions = []
30 for act1, act2 in requires_actions:
31 available_actions.extend([act1, act2])
32 reverse_actions[act1] = act2
33 reverse_actions[act2] = act1
35 # ____________________________________________________________
36 # OptionDescription authorized group_type values
37 group_types = ['default', 'family', 'group', 'master']
38 # ____________________________________________________________
41 "container that support items for the values of list (multi) options"
42 def __init__(self, lst, config, child):
45 super(Multi, self).__init__(lst)
47 def __setitem__(self, key, value):
48 return self.setoption(value, key)
50 def append(self, value):
53 def setoption(self, value, key=None):
54 owners = self.child.getowner(self.config)
55 # None is replaced by default_multi
57 defval = self.child.getdefault()
58 if key is not None and len(defval) > key:
61 value = self.child.default_multi
64 who = self.config._cfgimpl_owner
65 if not self.child._validate(value):
66 raise ConfigError("invalid value {0} "
67 "for option {1}".format(str(value), self.child._name))
69 oldowner = self.child.getowner(self.config)
71 ret = super(Multi, self).append(value)
75 ret = super(Multi, self).__setitem__(key, value)
77 self.config._cfgimpl_previous_values[self.child._name] = oldvalue
78 self.child.setowner(self.config, oldowner)
82 oldowner = self.child.getowner(self.config)
84 self.child.setowner(self.config, oldowner)
85 super(Multi, self).pop(key)
86 # ____________________________________________________________
88 class Option(HiddenBaseType, DisabledBaseType):
89 #reminder: an Option object is **not** a container for the value
91 _force_default_on_freeze = False
92 def __init__(self, name, doc, default=None, default_multi=None,
93 requires=None, mandatory=False, multi=False, callback=None,
94 callback_params=None):
97 self._requires = requires
98 self._mandatory = mandatory
100 if not self.multi and default_multi is not None:
101 raise ConfigError("a default_multi is set whereas multi is False"
102 " in option: {0}".format(name))
103 if default_multi is not None and not self._validate(default_multi):
104 raise ConfigError("invalid default_multi value {0} "
105 "for option {1}".format(str(default_multi), name))
106 self.default_multi = default_multi
107 #if self.multi and default_multi is None:
108 # _cfgimpl_warnings[name] = DefaultMultiWarning
109 if callback is not None and (default is not None or default_multi is not None):
110 raise ConfigError("defaut values not allowed if option: {0} "
111 "is calculated".format(name))
112 self.callback = callback
113 if self.callback is None and callback_params is not None:
114 raise ConfigError("params defined for a callback function but"
115 " no callback defined yet for option {0}".format(name))
116 self.callback_params = callback_params
117 if self.multi == True:
120 if not isinstance(default, list) or not self.validate(default):
121 raise ConfigError("invalid default value {0} "
122 "for option {1} : not list type".format(str(default), name))
124 if default != None and not self.validate(default):
125 raise ConfigError("invalid default value {0} "
126 "for option {1}".format(str(default), name))
127 self.default = default
128 self.properties = [] # 'hidden', 'disabled'...
130 def validate(self, value):
131 if self.multi == False:
132 # None allows the reset of the value
134 return self._validate(value)
136 if not isinstance(value, list):
137 raise ConfigError("invalid value {0} "
138 "for option {1} which must be a list".format(value,
142 # None allows the reset of the value
143 if not self._validate(val):
147 def getdefault(self):
150 def is_empty_by_default(self):
151 if ((not self.is_multi() and self.default == None) or
152 (self.is_multi() and self.default == []) or None in self.default):
156 def force_default(self):
157 self._force_default_on_freeze = True
159 def hascallback_and_isfrozen():
160 return self._frozen and self.has_callback()
162 def is_forced_on_freeze(self):
163 return self._frozen and self._force_default_on_freeze
168 def getcallback(self):
171 def has_callback(self):
172 if self.callback == None:
177 def getcallback_params(self):
178 return self.callback_params
180 def setowner(self, config, owner):
181 # config *must* be only the **parent** config (not the toplevel config)
182 # owner is a **real* owner, a list is actually allowable here
185 if not type(owner) == list:
186 raise ConfigError("invalid owner for multi "
187 "option: {0}".format(name))
188 config._cfgimpl_value_owners[name] = owner
190 def getowner(self, config):
191 # config *must* be only the **parent** config (not the toplevel config)
192 return config._cfgimpl_value_owners[self._name]
194 def setoption(self, config, value, who):
195 "who is **not necessarily** a owner because it cannot be a list"
197 if not self.validate(value):
198 raise ConfigError('invalid value %s for option %s' % (value, name))
199 if self.is_mandatory():
200 # value shall not be '' for a mandatory option
201 # so '' is considered as being None
202 if not self.is_multi() and value == '':
204 if self.is_multi() and '' in value:
205 value = Multi([{'': None}.get(i, i) for i in value], config, self)
206 if config.is_mandatory() and ((self.is_multi() and value == []) or \
207 (not self.is_multi() and value is None)):
208 raise MandatoryError('cannot change the value to %s for '
209 'option %s' % (value, name))
210 if name not in config._cfgimpl_values:
211 raise AttributeError('unknown option %s' % (name))
213 if config.is_frozen() and self.is_frozen():
214 raise TypeError('cannot change the value to %s for '
215 'option %s' % (str(value), name))
217 # changes the default value (and therefore resets the previous value)
218 if self._validate(value):
221 raise ConfigError("invalid value %s for option %s" % (value, name))
222 apply_requires(self, config)
223 # FIXME put the validation for the multi somewhere else
224 # # it is a multi **and** it has requires
225 # if self.multi == True:
226 # if type(value) != list:
227 # raise TypeError("value {0} must be a list".format(value))
228 # if self._requires is not None:
229 # for reqname in self._requires:
230 # # FIXME : verify that the slaves are all multi
231 # #option = getattr(config._cfgimpl_descr, reqname)
232 # # if not option.multi == True:
233 # # raise ConflictConfigError("an option with requires "
234 # # "has to be a list type : {0}".format(name))
235 # if len(config._cfgimpl_values[reqname]) != len(value):
236 # raise ConflictConfigError("an option with requires "
237 # "has not the same length of the others "
238 # "in the group : {0}".format(reqname))
239 if type(config._cfgimpl_values[name]) == Multi:
240 config._cfgimpl_previous_values[name] = list(config._cfgimpl_values[name])
242 config._cfgimpl_previous_values[name] = config._cfgimpl_values[name]
243 config._cfgimpl_values[name] = value
245 def getkey(self, value):
247 # ____________________________________________________________
257 # ____________________________________________________________
261 def is_mandatory(self):
262 return self._mandatory
264 class ChoiceOption(Option):
267 def __init__(self, name, doc, values, default=None,
268 requires=None, callback=None, callback_params=None,
269 multi=False, mandatory=False, open_values=False):
271 if open_values not in [True, False]:
272 raise ConfigError('Open_values must be a boolean for '
274 self.open_values = open_values
275 super(ChoiceOption, self).__init__(name, doc, default=default,
276 callback=callback, callback_params=callback_params,
277 requires=requires, multi=multi, mandatory=mandatory)
279 def _validate(self, value):
280 if not self.open_values:
281 return value is None or value in self.values
285 class BoolOption(Option):
288 def _validate(self, value):
289 return isinstance(value, bool)
291 # config level validator
292 # def setoption(self, config, value, who):
294 # if value and self._validator is not None:
295 # toplevel = config._cfgimpl_get_toplevel()
296 # self._validator(toplevel)
297 # super(BoolOption, self).setoption(config, value, who)
299 class IntOption(Option):
302 def _validate(self, value):
303 return isinstance(value, int)
305 class FloatOption(Option):
308 def _validate(self, value):
309 return isinstance(value, float)
311 class StrOption(Option):
314 def _validate(self, value):
315 return isinstance(value, str)
317 class SymLinkOption(object):
320 def __init__(self, name, path):
324 def setoption(self, config, value, who):
325 setattr(config, self.path, value) # .setoption(self.path, value, who)
327 class IPOption(Option):
330 def _validate(self, value):
331 # by now the validation is nothing but a string, use IPy instead
332 return isinstance(value, str)
334 class NetmaskOption(Option):
337 def _validate(self, value):
338 # by now the validation is nothing but a string, use IPy instead
339 return isinstance(value, str)
341 class ArbitraryOption(Option):
342 def __init__(self, name, doc, default=None, defaultfactory=None,
343 requires=None, multi=False, mandatory=False):
344 super(ArbitraryOption, self).__init__(name, doc, requires=requires,
345 multi=multi, mandatory=mandatory)
346 self.defaultfactory = defaultfactory
347 if defaultfactory is not None:
348 assert default is None
350 def _validate(self, value):
353 def getdefault(self):
354 if self.defaultfactory is not None:
355 return self.defaultfactory()
358 class OptionDescription(HiddenBaseType, DisabledBaseType):
359 group_type = 'default'
361 def __init__(self, name, doc, children, requires=None):
364 self._children = children
365 self._requires = requires
367 self.properties = [] # 'hidden', 'disabled'...
373 for child in self._children:
374 setattr(self, child._name, child)
376 def add_child(self, child):
377 "dynamically adds a configuration option"
378 #Nothing is static. Even the Mona Lisa is falling apart.
379 for ch in self._children:
380 if isinstance(ch, Option):
381 if child._name == ch._name:
382 raise ConflictConfigError("existing option : {0}".format(
384 self._children.append(child)
385 setattr(self, child._name, child)
387 def update_child(self, child):
388 "modification of an existing option"
389 # XXX : corresponds to the `redefine`, is it usefull
392 def getkey(self, config):
393 return tuple([child.getkey(getattr(config, child._name))
394 for child in self._children])
396 def getpaths(self, include_groups=False, currpath=None):
397 """returns a list of all paths in self, recursively
398 currpath should not be provided (helps with recursion)
403 for option in self._children:
405 if attr.startswith('_cfgimpl'):
407 value = getattr(self, attr)
408 if isinstance(value, OptionDescription):
410 paths.append('.'.join(currpath + [attr]))
411 currpath.append(attr)
412 paths += value.getpaths(include_groups=include_groups,
416 paths.append('.'.join(currpath + [attr]))
418 # ____________________________________________________________
420 def set_group_type(self, group_type):
421 if group_type in group_types:
422 self.group_type = group_type
424 raise ConfigError('not allowed value for group_type : {0}'.format(
427 def get_group_type(self):
428 return self.group_type
429 # ____________________________________________________________
431 super(OptionDescription, self).hide()
432 # FIXME : AND THE SUBCHILDREN ?
433 for child in self._children:
434 if isinstance(child, OptionDescription):
438 # FIXME : AND THE SUBCHILDREN ??
439 super(OptionDescription, self).show()
440 for child in self._children:
441 if isinstance(child, OptionDescription):
443 # ____________________________________________________________
445 super(OptionDescription, self).disable()
446 # FIXME : AND THE SUBCHILDREN ?
447 for child in self._children:
448 if isinstance(child, OptionDescription):
452 # FIXME : AND THE SUBCHILDREN ?
453 super(OptionDescription, self).enable()
454 for child in self._children:
455 if isinstance(child, OptionDescription):
457 # ____________________________________________________________
459 def validate_requires_arg(requires, name):
460 # malformed requirements
463 if not type(req) == tuple and len(req) != 3:
464 raise RequiresError("malformed requirements for option:"
467 if action not in available_actions:
468 raise RequiresError("malformed requirements for option: {0}"
469 "unknown action: {1}".format(name, action))
470 if reverse_actions[action] in config_action:
471 raise RequiresError("inconsistency in action types for option: {0}"
472 "action: {1} in contradiction with {2}\n"
473 " ({3})".format(name, action,
474 reverse_actions[action], requires))
475 config_action.append(action)
477 def build_actions(requires):
479 for require in requires:
481 trigger_actions.setdefault(action, []).append(require)
482 return trigger_actions
484 def apply_requires(opt, config):
485 if hasattr(opt, '_requires') and opt._requires is not None:
486 rootconfig = config._cfgimpl_get_toplevel()
487 validate_requires_arg(opt._requires, opt._name)
488 # filters the callbacks
489 trigger_actions = build_actions(opt._requires)
490 for requires in trigger_actions.values():
492 for require in requires:
493 name, expected, action = require
494 path = config._cfgimpl_get_path() + '.' + opt._name
495 if name.startswith(path):
496 raise RequirementRecursionError("malformed requirements "
497 "imbrication detected for option: '{0}' "
498 "with requirement on: '{1}'".format(path, name))
499 homeconfig, shortname = rootconfig._cfgimpl_get_home_by_path(name)
500 if shortname in homeconfig._cfgimpl_values:
501 value = homeconfig._cfgimpl_values[shortname]
502 if value == expected:
503 getattr(opt, action)() #.hide() or show() or...
504 # FIXME generic programming opt.property_launch(action, False)
506 else: # option doesn't exist ! should not happen...
507 raise NotFoundError("required option not found: "
509 # no callback has been triggered, then just reverse the action
511 getattr(opt, reverse_actions[action])()