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