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