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 available_actions = ['hide', 'show', 'enable', 'disable']
27 reverse_actions = {'hide': 'show', 'show': 'hide',
28 'disable':'enable', 'enable': 'disable'}
29 # ____________________________________________________________
30 # OptionDescription authorized group_type values
31 group_types = ['default', 'family', 'group', 'master']
32 # ____________________________________________________________
35 "container that support items for the values of list (multi) options"
36 def __init__(self, lst, config, child):
39 super(Multi, self).__init__(lst)
41 def __setitem__(self, key, value):
42 return self.setoption(value, key)
44 def append(self, value):
47 def setoption(self, value, key=None):
48 owners = self.child.getowner(self.config)
49 # None is replaced by default_multi
51 defval = self.child.getdefault()
52 if key is not None and len(defval) > key:
55 value = self.child.default_multi
58 who = self.config._cfgimpl_owner
59 if not self.child._validate(value):
60 raise ConfigError("invalid value {0} "
61 "for option {1}".format(str(value), self.child._name))
63 oldowner = self.child.getowner(self.config)
65 ret = super(Multi, self).append(value)
69 ret = super(Multi, self).__setitem__(key, value)
71 self.config._cfgimpl_previous_values[self.child._name] = oldvalue
72 self.child.setowner(self.config, oldowner)
76 oldowner = self.child.getowner(self.config)
78 self.child.setowner(self.config, oldowner)
79 super(Multi, self).pop(key)
80 # ____________________________________________________________
82 class Option(HiddenBaseType, DisabledBaseType):
83 #reminder: an Option object is **not** a container for the value
85 _force_default_on_freeze = False
86 def __init__(self, name, doc, default=None, default_multi=None,
87 requires=None, mandatory=False, multi=False, callback=None,
88 callback_params=None):
91 self._requires = requires
92 self._mandatory = mandatory
94 if not self.multi and default_multi is not None:
95 raise ConfigError("a default_multi is set whereas multi is False"
96 " in option: {0}".format(name))
97 if default_multi is not None and not self._validate(default_multi):
98 raise ConfigError("invalid default_multi value {0} "
99 "for option {1}".format(str(default_multi), name))
100 self.default_multi = default_multi
101 #if self.multi and default_multi is None:
102 # _cfgimpl_warnings[name] = DefaultMultiWarning
103 if callback is not None and (default is not None or default_multi is not None):
104 raise ConfigError("defaut values not allowed if option: {0}"
105 "is calculated".format(name))
106 self.callback = callback
107 if self.callback is None and callback_params is not None:
108 raise ConfigError("params defined for a callback function but"
109 " no callback defined yet for option {0}".format(name))
110 self.callback_params = callback_params
111 if self.multi == True:
114 if not isinstance(default, list) or not self.validate(default):
115 raise ConfigError("invalid default value {0} "
116 "for option {1} : not list type".format(str(default), name))
118 if default != None and not self.validate(default):
119 raise ConfigError("invalid default value {0} "
120 "for option {1}".format(str(default), name))
121 self.default = default
122 self.properties = [] # 'hidden', 'disabled'...
124 def validate(self, value):
125 if self.multi == False:
126 # None allows the reset of the value
128 return self._validate(value)
130 if not isinstance(value, list):
131 raise ConfigError("invalid value {0} "
132 "for option {1} which must be a list".format(value,
136 # None allows the reset of the value
137 if not self._validate(val):
141 def getdefault(self):
144 def is_empty_by_default(self):
145 if ((not self.is_multi() and self.default == None) or
146 (self.is_multi() and self.default == []) or None in self.default):
150 def force_default(self):
151 self._force_default_on_freeze = True
153 def hascallback_and_isfrozen():
154 return self._frozen and self.has_callback()
156 def is_forced_on_freeze(self):
157 return self._frozen and self._force_default_on_freeze
162 def getcallback(self):
165 def has_callback(self):
166 if self.callback == None:
171 def getcallback_params(self):
172 return self.callback_params
174 def setowner(self, config, owner):
175 # config *must* be only the **parent** config (not the toplevel config)
176 # owner is a **real* owner, a list is actually allowable here
179 raise TypeError("trying to change a frozen option's owner: %s" % name)
181 if not type(owner) == list:
182 raise ConfigError("invalid owner for multi "
183 "option: {0}".format(self._name))
184 config._cfgimpl_value_owners[name] = owner
186 def getowner(self, config):
187 # config *must* be only the **parent** config (not the toplevel config)
188 return config._cfgimpl_value_owners[self._name]
190 def setoption(self, config, value, who):
191 "who is **not necessarily** a owner because it cannot be a list"
193 # the value cannot be changed if a callback is defined
194 if self.has_callback():
195 raise TypeError("trying to change an option with callback: %s" % name)
196 # we want the possibility to reset everything
197 if not (who == "default" and value is None) and 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 override value to %s for '
209 'option %s' % (value, name))
210 if who == "default" and value is None:
213 if name not in config._cfgimpl_values:
214 raise AttributeError('unknown option %s' % (name))
215 if config.is_frozen() and (self.has_callback() or self.isfrozen()):
216 raise ConflictConfigError('cannot override value to %s for '
217 'option %s' % (value, name))
219 # changes the default value (and therefore resets the previous value)
220 if self._validate(value):
223 raise ConfigError("invalid value %s for option %s" % (value, name))
224 apply_requires(self, config)
225 # FIXME put the validation for the multi somewhere else
226 # # it is a multi **and** it has requires
227 # if self.multi == True:
228 # if type(value) != list:
229 # raise TypeError("value {0} must be a list".format(value))
230 # if self._requires is not None:
231 # for reqname in self._requires:
232 # # FIXME : verify that the slaves are all multi
233 # #option = getattr(config._cfgimpl_descr, reqname)
234 # # if not option.multi == True:
235 # # raise ConflictConfigError("an option with requires "
236 # # "has to be a list type : {0}".format(name))
237 # if len(config._cfgimpl_values[reqname]) != len(value):
238 # raise ConflictConfigError("an option with requires "
239 # "has not the same length of the others "
240 # "in the group : {0}".format(reqname))
241 if type(config._cfgimpl_values[name]) == Multi:
242 config._cfgimpl_previous_values[name] = list(config._cfgimpl_values[name])
244 config._cfgimpl_previous_values[name] = config._cfgimpl_values[name]
245 config._cfgimpl_values[name] = value
247 def getkey(self, value):
249 # ____________________________________________________________
259 # ____________________________________________________________
263 def is_mandatory(self):
264 return self._mandatory
266 class ChoiceOption(Option):
269 def __init__(self, name, doc, values, default=None,
270 requires=None, callback=None, callback_params=None,
271 multi=False, mandatory=False, open_values=False):
273 if open_values not in [True, False]:
274 raise ConfigError('Open_values must be a boolean for '
276 self.open_values = open_values
277 super(ChoiceOption, self).__init__(name, doc, default=default,
278 callback=callback, callback_params=callback_params,
279 requires=requires, multi=multi, mandatory=mandatory)
281 def setoption(self, config, value, who):
283 super(ChoiceOption, self).setoption(config, value, who)
285 def _validate(self, value):
286 if not self.open_values:
287 return value is None or value in self.values
291 class BoolOption(Option):
294 def _validate(self, value):
295 return isinstance(value, bool)
297 # config level validator
298 # def setoption(self, config, value, who):
300 # if value and self._validator is not None:
301 # toplevel = config._cfgimpl_get_toplevel()
302 # self._validator(toplevel)
303 # super(BoolOption, self).setoption(config, value, who)
305 class IntOption(Option):
308 def _validate(self, value):
315 def setoption(self, config, value, who):
317 super(IntOption, self).setoption(config, value, who)
319 raise ConfigError(*e.args)
321 class FloatOption(Option):
324 def _validate(self, value):
331 def setoption(self, config, value, who):
333 super(FloatOption, self).setoption(config, float(value), who)
335 raise ConfigError(*e.args)
337 class StrOption(Option):
340 def _validate(self, value):
341 return isinstance(value, str)
343 def setoption(self, config, value, who):
345 super(StrOption, self).setoption(config, value, who)
347 raise ConfigError(*e.args)
349 class SymLinkOption(object):
352 def __init__(self, name, path):
356 def setoption(self, config, value, who):
358 setattr(config, self.path, value) # .setoption(self.path, value, who)
360 raise ConfigError(*e.args)
362 class IPOption(Option):
365 def _validate(self, value):
366 # by now the validation is nothing but a string, use IPy instead
367 return isinstance(value, str)
369 def setoption(self, config, value, who):
371 super(IPOption, self).setoption(config, value, who)
373 raise ConfigError(*e.args)
375 class NetmaskOption(Option):
378 def _validate(self, value):
379 # by now the validation is nothing but a string, use IPy instead
380 return isinstance(value, str)
382 def setoption(self, config, value, who):
384 super(NetmaskOption, self).setoption(config, value, who)
386 raise ConfigError(*e.args)
388 class ArbitraryOption(Option):
389 def __init__(self, name, doc, default=None, defaultfactory=None,
390 requires=None, multi=False, mandatory=False):
391 super(ArbitraryOption, self).__init__(name, doc, requires=requires,
392 multi=multi, mandatory=mandatory)
393 self.defaultfactory = defaultfactory
394 if defaultfactory is not None:
395 assert default is None
397 def _validate(self, value):
400 def getdefault(self):
401 if self.defaultfactory is not None:
402 return self.defaultfactory()
405 class OptionDescription(HiddenBaseType, DisabledBaseType):
406 group_type = 'default'
408 def __init__(self, name, doc, children, requires=None):
411 self._children = children
412 self._requires = requires
414 self.properties = [] # 'hidden', 'disabled'...
420 for child in self._children:
421 setattr(self, child._name, child)
423 def add_child(self, child):
424 "dynamically adds a configuration option"
425 #Nothing is static. Even the Mona Lisa is falling apart.
426 for ch in self._children:
427 if isinstance(ch, Option):
428 if child._name == ch._name:
429 raise ConflictConfigError("existing option : {0}".format(
431 self._children.append(child)
432 setattr(self, child._name, child)
434 def update_child(self, child):
435 "modification of an existing option"
436 # XXX : corresponds to the `redefine`, is it usefull
439 def getkey(self, config):
440 return tuple([child.getkey(getattr(config, child._name))
441 for child in self._children])
443 def getpaths(self, include_groups=False, currpath=None):
444 """returns a list of all paths in self, recursively
445 currpath should not be provided (helps with recursion)
450 for option in self._children:
452 if attr.startswith('_cfgimpl'):
454 value = getattr(self, attr)
455 if isinstance(value, OptionDescription):
457 paths.append('.'.join(currpath + [attr]))
458 currpath.append(attr)
459 paths += value.getpaths(include_groups=include_groups,
463 paths.append('.'.join(currpath + [attr]))
465 # ____________________________________________________________
467 def set_group_type(self, group_type):
468 if group_type in group_types:
469 self.group_type = group_type
471 raise ConfigError('not allowed value for group_type : {0}'.format(
474 def get_group_type(self):
475 return self.group_type
476 # ____________________________________________________________
478 super(OptionDescription, self).hide()
479 # FIXME : AND THE SUBCHILDREN ?
480 for child in self._children:
481 if isinstance(child, OptionDescription):
485 # FIXME : AND THE SUBCHILDREN ??
486 super(OptionDescription, self).show()
487 for child in self._children:
488 if isinstance(child, OptionDescription):
490 # ____________________________________________________________
492 super(OptionDescription, self).disable()
493 # FIXME : AND THE SUBCHILDREN ?
494 for child in self._children:
495 if isinstance(child, OptionDescription):
499 # FIXME : AND THE SUBCHILDREN ?
500 super(OptionDescription, self).enable()
501 for child in self._children:
502 if isinstance(child, OptionDescription):
504 # ____________________________________________________________
505 def apply_requires(opt, config):
506 if hasattr(opt, '_requires'):
507 if opt._requires is not None:
508 # malformed requirements
509 rootconfig = config._cfgimpl_get_toplevel()
510 for req in opt._requires:
511 if not type(req) == tuple and len(req) in (3, 4):
512 raise RequiresError("malformed requirements for option:"
513 " {0}".format(opt._name))
514 # all actions **must** be identical
515 actions = [req[2] for req in opt._requires]
519 raise RequiresError("malformed requirements for option:"
520 " {0}".format(opt._name))
521 # filters the callbacks
523 for req in opt._requires:
525 name, expected, action = req
528 name, expected, action, inverted = req
529 if inverted == 'inverted':
531 path = config._cfgimpl_get_path() + '.' + opt._name
532 if name.startswith(path):
533 raise RequirementRecursionError("malformed requirements imbrication "
534 "detected for option: '{0}' with requirement on: '{1}'".format(path, name))
535 homeconfig, shortname = \
536 rootconfig._cfgimpl_get_home_by_path(name)
537 if shortname in homeconfig._cfgimpl_values:
538 value = homeconfig._cfgimpl_values[shortname]
539 if (not inverted and value == expected) or \
540 (inverted and value != expected):
541 if action not in available_actions:
542 raise RequiresError("malformed requirements"
543 " for option: {0}".format(opt._name))
544 getattr(opt, action)() #.hide() or show() or...
545 # FIXME generic programming opt.property_launch(action, False)
547 else: # option doesn't exist ! should not happen...
548 raise NotFoundError("required option not found: "
550 # no callback has been triggered, then just reverse the action
552 getattr(opt, reverse_actions[action])()