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