d4d694f12734b37e3efcf9a5f7f8fe0b1ba13ac3
[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 # ____________________________________________________________
36 # OptionDescription authorized group_type values
37 group_types = ['default', 'family', 'group', 'master']
38 # ____________________________________________________________
39 # multi types 
40 class Multi(list):
41     "container that support items for the values of list (multi) options" 
42     def __init__(self, lst, config, child):
43         self.config = config
44         self.child = child
45         super(Multi, self).__init__(lst)
46         
47     def __setitem__(self, key, value):
48         return self.setoption(value, key)
49
50     def append(self, value):
51         self.setoption(value)
52    
53     def setoption(self, value, key=None):
54         owners = self.child.getowner(self.config)
55         # None is replaced by default_multi
56         if value == None:
57             defval = self.child.getdefault()
58             if key is not None and len(defval) > key:
59                 value = defval[key]
60             else:
61                 value = self.child.default_multi
62             who = 'default'
63         else:
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))
68         oldvalue = list(self)
69         oldowner = self.child.getowner(self.config)
70         if key is None:
71             ret = super(Multi, self).append(value)
72             oldvalue.append(None)
73             oldowner.append(who)
74         else:
75             ret = super(Multi, self).__setitem__(key, value)
76             oldowner[key] = who
77         self.config._cfgimpl_previous_values[self.child._name] = oldvalue
78         self.child.setowner(self.config, oldowner)
79         return ret
80         
81     def pop(self, key):
82         oldowner = self.child.getowner(self.config)
83         oldowner.pop(key)
84         self.child.setowner(self.config, oldowner)
85         super(Multi, self).pop(key)
86 # ____________________________________________________________
87 #
88 class Option(HiddenBaseType, DisabledBaseType):
89     #reminder: an Option object is **not** a container for the value
90     _frozen = False
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):
95         self._name = name
96         self.doc = doc
97         self._requires = requires
98         self._mandatory = mandatory
99         self.multi = multi
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:
118             if default == None:
119                 default = []
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))
123         else:
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'...
129       
130     def validate(self, value):
131         if self.multi == False:
132             # None allows the reset of the value
133             if value != None:
134                 return self._validate(value)
135         else:
136             if not isinstance(value, list): 
137                 raise ConfigError("invalid value {0} " 
138                         "for option {1} which must be a list".format(value,
139                         self._name))
140             for val in value:
141                 if val != None:
142                     # None allows the reset of the value
143                     if not self._validate(val):
144                         return False
145         return True
146
147     def getdefault(self):
148         return self.default
149
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):
153             return True
154         return False
155
156     def force_default(self):
157         self._force_default_on_freeze = True
158
159     def hascallback_and_isfrozen():
160         return self._frozen and self.has_callback()
161
162     def is_forced_on_freeze(self):
163         return self._frozen and self._force_default_on_freeze
164
165     def getdoc(self):
166         return self.doc
167
168     def getcallback(self):
169         return self.callback
170     
171     def has_callback(self):
172         if self.callback == None:
173             return False
174         else:
175             return True
176
177     def getcallback_params(self):
178         return self.callback_params
179
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
183         name = self._name
184         if self.is_multi():
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
189
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]
193
194     def setoption(self, config, value, who):
195         "who is **not necessarily** a owner because it cannot be a list"
196         name = self._name
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 == '':
203                 value = None
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))
212
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))
216         if who == "default":
217             # changes the default value (and therefore resets the previous value)
218             if self._validate(value):
219                 self.default = value
220             else:
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])
241         else:
242             config._cfgimpl_previous_values[name] = config._cfgimpl_values[name] 
243         config._cfgimpl_values[name] = value
244
245     def getkey(self, value):
246         return value
247     # ____________________________________________________________
248     def freeze(self):
249         self._frozen = True
250         return True
251
252     def unfreeze(self):
253         self._frozen = False
254
255     def is_frozen(self):
256         return self._frozen
257     # ____________________________________________________________
258     def is_multi(self):
259         return self.multi
260
261     def is_mandatory(self):
262         return self._mandatory
263         
264 class ChoiceOption(Option):
265     opt_type = 'string'
266     
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):
270         self.values = values
271         if open_values not in [True, False]:
272             raise ConfigError('Open_values must be a boolean for '
273                               '{0}'.format(name))
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)
278
279     def _validate(self, value):
280         if not self.open_values:
281             return value is None or value in self.values
282         else:
283             return True
284
285 class BoolOption(Option):
286     opt_type = 'bool'
287     
288     def _validate(self, value):
289         return isinstance(value, bool)
290
291 # config level validator             
292 #    def setoption(self, config, value, who):
293 #        name = self._name
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)
298
299 class IntOption(Option):
300     opt_type = 'int'
301     
302     def _validate(self, value):
303         return isinstance(value, int)
304
305 class FloatOption(Option):
306     opt_type = 'float'
307
308     def _validate(self, value):
309         return isinstance(value, float)
310
311 class StrOption(Option):
312     opt_type = 'string'
313     
314     def _validate(self, value):
315         return isinstance(value, str)
316                                      
317 class SymLinkOption(object):
318     opt_type = 'symlink'
319     
320     def __init__(self, name, path):
321         self._name = name
322         self.path = path 
323     
324     def setoption(self, config, value, who):
325         setattr(config, self.path, value) # .setoption(self.path, value, who)
326
327 class IPOption(Option):
328     opt_type = 'ip'
329     
330     def _validate(self, value):
331         # by now the validation is nothing but a string, use IPy instead
332         return isinstance(value, str)
333                                      
334 class NetmaskOption(Option):
335     opt_type = 'netmask'
336     
337     def _validate(self, value):
338         # by now the validation is nothing but a string, use IPy instead
339         return isinstance(value, str)
340                                      
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
349
350     def _validate(self, value):
351         return True
352
353     def getdefault(self):
354         if self.defaultfactory is not None:
355             return self.defaultfactory()
356         return self.default
357
358 class OptionDescription(HiddenBaseType, DisabledBaseType):
359     group_type = 'default'
360
361     def __init__(self, name, doc, children, requires=None):
362         self._name = name
363         self.doc = doc
364         self._children = children
365         self._requires = requires
366         self._build()
367         self.properties = [] # 'hidden', 'disabled'...
368         
369     def getdoc(self):
370         return self.doc
371
372     def _build(self):
373         for child in self._children:
374             setattr(self, child._name, child)
375
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(
383                                                                    child._name))
384         self._children.append(child)
385         setattr(self, child._name, child)
386     
387     def update_child(self, child):
388         "modification of an existing option"
389         # XXX : corresponds to the `redefine`, is it usefull 
390         pass
391         
392     def getkey(self, config):
393         return tuple([child.getkey(getattr(config, child._name))
394                       for child in self._children])
395
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)
399         """
400         if currpath is None:
401             currpath = []
402         paths = []
403         for option in self._children:
404             attr = option._name
405             if attr.startswith('_cfgimpl'):
406                 continue
407             if isinstance(option, OptionDescription):
408                 if include_groups:
409                     paths.append('.'.join(currpath + [attr]))
410                 currpath.append(attr)
411                 paths += option.getpaths(include_groups=include_groups,
412                                         currpath=currpath)
413                 currpath.pop()
414             else:
415                 paths.append('.'.join(currpath + [attr]))
416         return paths
417     # ____________________________________________________________
418
419     def set_group_type(self, group_type):
420         if group_type in group_types:
421             self.group_type = group_type
422         else:
423             raise ConfigError('not allowed value for group_type : {0}'.format(
424                               group_type))
425     
426     def get_group_type(self):
427         return self.group_type
428     # ____________________________________________________________
429     def hide(self):
430         super(OptionDescription, self).hide()
431         # FIXME : AND THE SUBCHILDREN ? 
432         for child in self._children:
433             if isinstance(child, OptionDescription):
434                 child.hide()
435     
436     def show(self):
437         # FIXME : AND THE SUBCHILDREN ?? 
438         super(OptionDescription, self).show()
439         for child in self._children:
440             if isinstance(child, OptionDescription):
441                 child.show()
442     # ____________________________________________________________
443     def disable(self):
444         super(OptionDescription, self).disable()
445         # FIXME : AND THE SUBCHILDREN ? 
446         for child in self._children:
447             if isinstance(child, OptionDescription):
448                 child.disable()
449     
450     def enable(self):
451         # FIXME : AND THE SUBCHILDREN ? 
452         super(OptionDescription, self).enable()
453         for child in self._children:
454             if isinstance(child, OptionDescription):
455                 child.enable()
456 # ____________________________________________________________
457
458 def validate_requires_arg(requires, name):
459     # malformed requirements
460     config_action = []
461     for req in requires:
462         if not type(req) == tuple and len(req) != 3:
463             raise RequiresError("malformed requirements for option:"
464                                            " {0}".format(name))
465         action = req[2]
466         if action not in available_actions:
467             raise RequiresError("malformed requirements for option: {0}"
468                                 "unknown action: {1}".format(name, action))
469         if reverse_actions[action] in config_action:
470             raise RequiresError("inconsistency in action types for option: {0}"
471                                 "action: {1} in contradiction with {2}\n"
472                                 " ({3})".format(name, action,
473                                     reverse_actions[action], requires))
474         config_action.append(action)
475
476 def build_actions(requires):
477     trigger_actions = {}
478     for require in requires:
479         action = require[2]
480         trigger_actions.setdefault(action, []).append(require)
481     return trigger_actions
482
483 def apply_requires(opt, config):
484     if hasattr(opt, '_requires') and opt._requires is not None:
485         rootconfig = config._cfgimpl_get_toplevel()
486         validate_requires_arg(opt._requires, opt._name)
487         # filters the callbacks
488         trigger_actions = build_actions(opt._requires)
489         for requires in trigger_actions.values():
490             matches = False
491             for require in requires:
492                 name, expected, action = require
493                 path = config._cfgimpl_get_path() + '.' + opt._name
494                 if name.startswith(path):
495                     raise RequirementRecursionError("malformed requirements "
496                           "imbrication detected for option: '{0}' "
497                           "with requirement on: '{1}'".format(path, name))
498                 homeconfig, shortname = rootconfig._cfgimpl_get_home_by_path(name)
499                 if shortname in homeconfig._cfgimpl_values:
500                     value = homeconfig._cfgimpl_values[shortname]
501                     if value == expected:
502                         getattr(opt, action)() #.hide() or show() or...
503                         # FIXME generic programming opt.property_launch(action, False)
504                         matches = True
505                 else: # option doesn't exist ! should not happen...
506                     raise NotFoundError("required option not found: "
507                                                              "{0}".format(name))
508             # no callback has been triggered, then just reverse the action
509             if not matches:
510                 getattr(opt, reverse_actions[action])()
511