add docstring and some docs
[tiramisu.git] / tiramisu / option.py
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)
4 #
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.
9 #
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.
14 #
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
18 #
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')]
27
28 available_actions = []
29 reverse_actions = {}
30 for act1, act2 in requires_actions:
31     available_actions.extend([act1, act2])
32     reverse_actions[act1] = act2
33     reverse_actions[act2] = act1
34 # ____________________________________________________________
35 # OptionDescription authorized group_type values
36 """
37 Three available group_types : `default`, `family`, `group` and
38 `master` (for master~slave group type). Notice that for a
39 master~slave group, the name of the group and the name of the
40 master option are identical.
41 """
42 group_types = ['default', 'family', 'group', 'master']
43 # ____________________________________________________________
44 # multi types
45 class Multi(list):
46     "container that support items for the values of list (multi) options"
47     def __init__(self, lst, config, child):
48         self.config = config
49         self.child = child
50         super(Multi, self).__init__(lst)
51
52     def __setitem__(self, key, value):
53         return self.setoption(value, key)
54
55     def append(self, value):
56         self.setoption(value)
57
58     def setoption(self, value, key=None):
59         owners = self.child.getowner(self.config)
60         # None is replaced by default_multi
61         if value == None:
62             defval = self.child.getdefault()
63             if key is not None and len(defval) > key:
64                 value = defval[key]
65             else:
66                 value = self.child.default_multi
67             who = 'default'
68         else:
69             who = self.config._cfgimpl_owner
70             if not self.child._validate(value):
71                 raise ConfigError("invalid value {0} "
72                     "for option {1}".format(str(value), self.child._name))
73         oldvalue = list(self)
74         oldowner = self.child.getowner(self.config)
75         if key is None:
76             ret = super(Multi, self).append(value)
77             oldvalue.append(None)
78             oldowner.append(who)
79         else:
80             ret = super(Multi, self).__setitem__(key, value)
81             oldowner[key] = who
82         self.config._cfgimpl_previous_values[self.child._name] = oldvalue
83         self.child.setowner(self.config, oldowner)
84         return ret
85
86     def pop(self, key):
87         oldowner = self.child.getowner(self.config)
88         oldowner.pop(key)
89         self.child.setowner(self.config, oldowner)
90         super(Multi, self).pop(key)
91 # ____________________________________________________________
92 #
93 class Option(HiddenBaseType, DisabledBaseType):
94     """
95     Abstract base class for configuration option's
96     reminder: an Option object is **not** a container for the value
97     """
98     "freeze means: cannot modify the value of an Option once set"
99     _frozen = False
100     "if an Option has been frozen, shall return the default value"
101     _force_default_on_freeze = False
102     def __init__(self, name, doc, default=None, default_multi=None,
103                  requires=None, mandatory=False, multi=False, callback=None,
104                  callback_params=None):
105         self._name = name
106         self.doc = doc
107         self._requires = requires
108         self._mandatory = mandatory
109         self.multi = multi
110         if not self.multi and default_multi is not None:
111             raise ConfigError("a default_multi is set whereas multi is False"
112                   " in option: {0}".format(name))
113         if default_multi is not None and not self._validate(default_multi):
114             raise ConfigError("invalid default_multi value {0} "
115                 "for option {1}".format(str(default_multi), name))
116         self.default_multi = default_multi
117         #if self.multi and default_multi is None:
118         #    _cfgimpl_warnings[name] = DefaultMultiWarning
119         if callback is not None and (default is not None or default_multi is not None):
120             raise ConfigError("defaut values not allowed if option: {0} "
121                 "is calculated".format(name))
122         self.callback = callback
123         if self.callback is None and callback_params is not None:
124             raise ConfigError("params defined for a callback function but"
125             " no callback defined yet for option {0}".format(name))
126         self.callback_params = callback_params
127         if self.multi == True:
128             if default == None:
129                 default = []
130             if not isinstance(default, list) or not self.validate(default):
131                 raise ConfigError("invalid default value {0} "
132                 "for option {1} : not list type".format(str(default), name))
133         else:
134             if default != None and not self.validate(default):
135                 raise ConfigError("invalid default value {0} "
136                                          "for option {1}".format(str(default), name))
137         self.default = default
138         self.properties = [] # 'hidden', 'disabled'...
139
140     def validate(self, value):
141         if self.multi == False:
142             # None allows the reset of the value
143             if value != None:
144                 return self._validate(value)
145         else:
146             if not isinstance(value, list):
147                 raise ConfigError("invalid value {0} "
148                         "for option {1} which must be a list".format(value,
149                         self._name))
150             for val in value:
151                 if val != None:
152                     # None allows the reset of the value
153                     if not self._validate(val):
154                         return False
155         return True
156
157     def getdefault(self):
158         "accessing the default value"
159         return self.default
160
161     def is_empty_by_default(self):
162         "no default value has been set yet"
163         if ((not self.is_multi() and self.default == None) or
164             (self.is_multi() and self.default == []) or None in self.default):
165             return True
166         return False
167
168     def force_default(self):
169         "if an Option has been frozen, shall return the default value"
170         self._force_default_on_freeze = True
171
172     def hascallback_and_isfrozen():
173         return self._frozen and self.has_callback()
174
175     def is_forced_on_freeze(self):
176         "if an Option has been frozen, shall return the default value"
177         return self._frozen and self._force_default_on_freeze
178
179     def getdoc(self):
180         "accesses the Option's doc"
181         return self.doc
182
183     def getcallback(self):
184         "a callback is only a link, the name of an external hook"
185         return self.callback
186
187     def has_callback(self):
188         "to know if a callback has been defined or not"
189         if self.callback == None:
190             return False
191         else:
192             return True
193
194     def getcallback_params(self):
195         "if a callback has been defined, returns his arity"
196         return self.callback_params
197
198     def setowner(self, config, owner):
199         """
200         :param config: *must* be only the **parent** config
201         (not the toplevel config)
202         :param owner: is a **real* owner, that is a name or a list
203         which is allowable here
204         """
205         name = self._name
206         if self.is_multi():
207             if not type(owner) == list:
208                 raise ConfigError("invalid owner for multi "
209                     "option: {0}".format(name))
210         config._cfgimpl_value_owners[name] = owner
211
212     def getowner(self, config):
213         "config *must* be only the **parent** config (not the toplevel config)"
214         return config._cfgimpl_value_owners[self._name]
215
216     def setoption(self, config, value, who):
217         """changes the option's value with the value_owner's who
218         :param config: the parent config is necessary here to store the value
219         :param who : is **not necessarily** a owner because it cannot be a list
220         :type who: string """
221         name = self._name
222         if not self.validate(value):
223             raise ConfigError('invalid value %s for option %s' % (value, name))
224         if self.is_mandatory():
225             # value shall not be '' for a mandatory option
226             # so '' is considered as being None
227             if not self.is_multi() and value == '':
228                 value = None
229             if self.is_multi() and '' in value:
230                 value = Multi([{'': None}.get(i, i) for i in value], config, self)
231             if config.is_mandatory() and ((self.is_multi() and value == []) or \
232                 (not self.is_multi() and value is None)):
233                 raise MandatoryError('cannot change the value to %s for '
234               'option %s' % (value, name))
235         if name not in config._cfgimpl_values:
236             raise AttributeError('unknown option %s' % (name))
237
238         if config.is_frozen() and self.is_frozen():
239             raise TypeError('cannot change the value to %s for '
240                'option %s' % (str(value), name))
241         if who == "default":
242             # changes the default value (and therefore resets the previous value)
243             if self._validate(value):
244                 self.default = value
245             else:
246                 raise ConfigError("invalid value %s for option %s" % (value, name))
247         apply_requires(self, config)
248         # FIXME put the validation for the multi somewhere else
249 #            # it is a multi **and** it has requires
250 #            if self.multi == True:
251 #                if type(value) != list:
252 #                    raise TypeError("value {0} must be a list".format(value))
253 #                if self._requires is not None:
254 #                    for reqname in self._requires:
255 #                        # FIXME : verify that the slaves are all multi
256 #                        #option = getattr(config._cfgimpl_descr, reqname)
257 #    #                    if not option.multi == True:
258 #    #                        raise ConflictConfigError("an option with requires "
259 #    #                         "has to be a list type : {0}".format(name))
260 #                        if len(config._cfgimpl_values[reqname]) != len(value):
261 #                            raise ConflictConfigError("an option with requires "
262 #                             "has not the same length of the others "
263 #                             "in the group : {0}".format(reqname))
264         if type(config._cfgimpl_values[name]) == Multi:
265             config._cfgimpl_previous_values[name] = list(config._cfgimpl_values[name])
266         else:
267             config._cfgimpl_previous_values[name] = config._cfgimpl_values[name]
268         config._cfgimpl_values[name] = value
269
270     def getkey(self, value):
271         return value
272     # ____________________________________________________________
273     "freeze utility"
274     def freeze(self):
275         self._frozen = True
276         return True
277     def unfreeze(self):
278         self._frozen = False
279     def is_frozen(self):
280         return self._frozen
281     # ____________________________________________________________
282     def is_multi(self):
283         return self.multi
284     def is_mandatory(self):
285         return self._mandatory
286
287 class ChoiceOption(Option):
288     opt_type = 'string'
289
290     def __init__(self, name, doc, values, default=None,
291                  requires=None, callback=None, callback_params=None,
292                  multi=False, mandatory=False, open_values=False):
293         self.values = values
294         if open_values not in [True, False]:
295             raise ConfigError('Open_values must be a boolean for '
296                               '{0}'.format(name))
297         self.open_values = open_values
298         super(ChoiceOption, self).__init__(name, doc, default=default,
299                            callback=callback, callback_params=callback_params,
300                            requires=requires, multi=multi, mandatory=mandatory)
301
302     def _validate(self, value):
303         if not self.open_values:
304             return value is None or value in self.values
305         else:
306             return True
307
308 class BoolOption(Option):
309     opt_type = 'bool'
310
311     def _validate(self, value):
312         return isinstance(value, bool)
313
314 # config level validator
315 #    def setoption(self, config, value, who):
316 #        name = self._name
317 #        if value and self._validator is not None:
318 #            toplevel = config._cfgimpl_get_toplevel()
319 #            self._validator(toplevel)
320 #        super(BoolOption, self).setoption(config, value, who)
321
322 class IntOption(Option):
323     opt_type = 'int'
324
325     def _validate(self, value):
326         return isinstance(value, int)
327
328 class FloatOption(Option):
329     opt_type = 'float'
330
331     def _validate(self, value):
332         return isinstance(value, float)
333
334 class StrOption(Option):
335     opt_type = 'string'
336
337     def _validate(self, value):
338         return isinstance(value, str)
339
340 class SymLinkOption(object):
341     opt_type = 'symlink'
342
343     def __init__(self, name, path):
344         self._name = name
345         self.path = path
346
347     def setoption(self, config, value, who):
348         setattr(config, self.path, value) # .setoption(self.path, value, who)
349
350 class IPOption(Option):
351     opt_type = 'ip'
352
353     def _validate(self, value):
354         # by now the validation is nothing but a string, use IPy instead
355         return isinstance(value, str)
356
357 class NetmaskOption(Option):
358     opt_type = 'netmask'
359
360     def _validate(self, value):
361         # by now the validation is nothing but a string, use IPy instead
362         return isinstance(value, str)
363
364 class ArbitraryOption(Option):
365     def __init__(self, name, doc, default=None, defaultfactory=None,
366                                    requires=None, multi=False, mandatory=False):
367         super(ArbitraryOption, self).__init__(name, doc, requires=requires,
368                                                multi=multi, mandatory=mandatory)
369         self.defaultfactory = defaultfactory
370         if defaultfactory is not None:
371             assert default is None
372
373     def _validate(self, value):
374         return True
375
376     def getdefault(self):
377         if self.defaultfactory is not None:
378             return self.defaultfactory()
379         return self.default
380
381 class OptionDescription(HiddenBaseType, DisabledBaseType):
382     "Config's schema (organisation) and container of Options"
383     "the group_type is an attribute useful for iteration on groups in a config"
384     group_type = 'default'
385     def __init__(self, name, doc, children, requires=None):
386         """
387         :param children: is a list of option descriptions (including
388         ``OptionDescription`` instances for nested namespaces).
389         """
390         self._name = name
391         self.doc = doc
392         self._children = children
393         self._requires = requires
394         self._build()
395         self.properties = [] # 'hidden', 'disabled'...
396
397     def getdoc(self):
398         return self.doc
399
400     def _build(self):
401         for child in self._children:
402             setattr(self, child._name, child)
403
404     def add_child(self, child):
405         "dynamically adds a configuration option"
406         #Nothing is static. Even the Mona Lisa is falling apart.
407         for ch in self._children:
408             if isinstance(ch, Option):
409                 if child._name == ch._name:
410                     raise ConflictConfigError("existing option : {0}".format(
411                                                                    child._name))
412         self._children.append(child)
413         setattr(self, child._name, child)
414
415     def update_child(self, child):
416         "modification of an existing option"
417         # XXX : corresponds to the `redefine`, is it usefull
418         pass
419
420     def getkey(self, config):
421         return tuple([child.getkey(getattr(config, child._name))
422                       for child in self._children])
423
424     def getpaths(self, include_groups=False, currpath=None):
425         """returns a list of all paths in self, recursively
426            currpath should not be provided (helps with recursion)
427         """
428         if currpath is None:
429             currpath = []
430         paths = []
431         for option in self._children:
432             attr = option._name
433             if attr.startswith('_cfgimpl'):
434                 continue
435             if isinstance(option, OptionDescription):
436                 if include_groups:
437                     paths.append('.'.join(currpath + [attr]))
438                 currpath.append(attr)
439                 paths += option.getpaths(include_groups=include_groups,
440                                         currpath=currpath)
441                 currpath.pop()
442             else:
443                 paths.append('.'.join(currpath + [attr]))
444         return paths
445     # ____________________________________________________________
446     def set_group_type(self, group_type):
447         ":param group_type: string in group_types"
448         if group_type in group_types:
449             self.group_type = group_type
450         else:
451             raise ConfigError('not allowed value for group_type : {0}'.format(
452                               group_type))
453
454     def get_group_type(self):
455         return self.group_type
456     # ____________________________________________________________
457     "actions API"
458     def hide(self):
459         super(OptionDescription, self).hide()
460         for child in self._children:
461             if isinstance(child, OptionDescription):
462                 child.hide()
463     def show(self):
464         super(OptionDescription, self).show()
465         for child in self._children:
466             if isinstance(child, OptionDescription):
467                 child.show()
468
469     def disable(self):
470         super(OptionDescription, self).disable()
471         for child in self._children:
472             if isinstance(child, OptionDescription):
473                 child.disable()
474     def enable(self):
475         super(OptionDescription, self).enable()
476         for child in self._children:
477             if isinstance(child, OptionDescription):
478                 child.enable()
479 # ____________________________________________________________
480
481 def validate_requires_arg(requires, name):
482     "malformed requirements"
483     config_action = []
484     for req in requires:
485         if not type(req) == tuple and len(req) != 3:
486             raise RequiresError("malformed requirements for option:"
487                                            " {0}".format(name))
488         action = req[2]
489         if action not in available_actions:
490             raise RequiresError("malformed requirements for option: {0}"
491                                 "unknown action: {1}".format(name, action))
492         if reverse_actions[action] in config_action:
493             raise RequiresError("inconsistency in action types for option: {0}"
494                                 "action: {1} in contradiction with {2}\n"
495                                 " ({3})".format(name, action,
496                                     reverse_actions[action], requires))
497         config_action.append(action)
498
499 def build_actions(requires):
500     "action are hide, show, enable, disable..."
501     trigger_actions = {}
502     for require in requires:
503         action = require[2]
504         trigger_actions.setdefault(action, []).append(require)
505     return trigger_actions
506
507 def apply_requires(opt, config):
508     "carries out the jit (just in time requirements between options"
509     if hasattr(opt, '_requires') and opt._requires is not None:
510         rootconfig = config._cfgimpl_get_toplevel()
511         validate_requires_arg(opt._requires, opt._name)
512         # filters the callbacks
513         trigger_actions = build_actions(opt._requires)
514         for requires in trigger_actions.values():
515             matches = False
516             for require in requires:
517                 name, expected, action = require
518                 path = config._cfgimpl_get_path() + '.' + opt._name
519                 if name.startswith(path):
520                     raise RequirementRecursionError("malformed requirements "
521                           "imbrication detected for option: '{0}' "
522                           "with requirement on: '{1}'".format(path, name))
523                 homeconfig, shortname = rootconfig._cfgimpl_get_home_by_path(name)
524                 if shortname in homeconfig._cfgimpl_values:
525                     value = homeconfig._cfgimpl_values[shortname]
526                     if value == expected:
527                         getattr(opt, action)() #.hide() or show() or...
528                         # FIXME generic programming opt.property_launch(action, False)
529                         matches = True
530                 else: # option doesn't exist ! should not happen...
531                     raise NotFoundError("required option not found: "
532                                                              "{0}".format(name))
533             # no callback has been triggered, then just reverse the action
534             if not matches:
535                 getattr(opt, reverse_actions[action])()