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