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