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