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