opt.hidden and opt.disabled is replaced by opt.properties
[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.autolib import special_owners
24 from tiramisu.basetype import HiddenBaseType, DisabledBaseType
25 from tiramisu.error import (ConfigError, ConflictConfigError, NotFoundError, 
26     SpecialOwnersError, RequiresError, RequirementRecursionError)
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
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     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 getdoc(self):
145         return self.doc
146
147     def getcallback(self):
148         return self.callback
149     
150     def has_callback(self):
151         if self.callback == None:
152             return False
153         else:
154             return True
155
156     def getcallback_params(self):
157         return self.callback_params
158
159     def setowner(self, config, owner):
160         # config *must* be only the **parent** config (not the toplevel config) 
161         # owner is a **real* owner, a list is actually allowable here
162         name = self._name
163         if self._frozen:
164             raise TypeError("trying to change a frozen option's owner: %s" % name)
165         if owner in special_owners:
166             if self.callback == None:
167                 raise SpecialOwnersError("no callback specified for" 
168                                                       "option {0}".format(name))
169         if self.is_multi():
170             if not type(owner) == list:
171                 raise ConfigError("invalid owner for multi "
172                     "option: {0}".format(self._name))
173         config._cfgimpl_value_owners[name] = owner
174
175     def getowner(self, config):
176         # config *must* be only the **parent** config (not the toplevel config) 
177         return config._cfgimpl_value_owners[self._name]
178
179     def setoption(self, config, value, who):
180         "who is **not necessarily** a owner because it cannot be a list"
181         name = self._name
182         if self._frozen:
183             raise TypeError('trying to change a frozen option object: %s' % name)
184         # we want the possibility to reset everything
185         if who == "default" and value is None:
186             self.default = None
187             return
188         if not self.validate(value):
189             raise ConfigError('invalid value %s for option %s' % (value, name))
190         if who == "default":
191             # changes the default value (and therefore resets the previous value)
192             if self._validate(value):
193                 self.default = value
194             else:
195                 raise ConfigError("invalid value %s for option %s" % (value, name))
196         apply_requires(self, config)
197         # FIXME put the validation for the multi somewhere else
198 #            # it is a multi **and** it has requires
199 #            if self.multi == True:
200 #                if type(value) != list:
201 #                    raise TypeError("value {0} must be a list".format(value))
202 #                if self._requires is not None:
203 #                    for reqname in self._requires:
204 #                        # FIXME : verify that the slaves are all multi
205 #                        #option = getattr(config._cfgimpl_descr, reqname)
206 #    #                    if not option.multi == True:
207 #    #                        raise ConflictConfigError("an option with requires "
208 #    #                         "has to be a list type : {0}".format(name)) 
209 #                        if len(config._cfgimpl_values[reqname]) != len(value):
210 #                            raise ConflictConfigError("an option with requires "
211 #                             "has not the same length of the others " 
212 #                             "in the group : {0}".format(reqname)) 
213         if type(config._cfgimpl_values[name]) == Multi:
214             config._cfgimpl_previous_values[name] = list(config._cfgimpl_values[name])
215         else:
216             config._cfgimpl_previous_values[name] = config._cfgimpl_values[name] 
217         config._cfgimpl_values[name] = value
218
219     def getkey(self, value):
220         return value
221
222     def freeze(self):
223         self._frozen = True
224         return True
225
226     def unfreeze(self):
227         self._frozen = False
228     # ____________________________________________________________
229     def is_multi(self):
230         return self.multi
231
232     def is_mandatory(self):
233         return self._mandatory
234         
235 class ChoiceOption(Option):
236     opt_type = 'string'
237     
238     def __init__(self, name, doc, values, default=None,
239                  requires=None, callback=None, callback_params=None,
240                  multi=False, mandatory=False, open_values=False):
241         self.values = values
242         if open_values not in [True, False]:
243             raise ConfigError('Open_values must be a boolean for '
244                               '{0}'.format(name))
245         self.open_values = open_values
246         super(ChoiceOption, self).__init__(name, doc, default=default,
247                            callback=callback, callback_params=callback_params, 
248                            requires=requires, multi=multi, mandatory=mandatory)
249
250     def setoption(self, config, value, who):
251         name = self._name
252         super(ChoiceOption, self).setoption(config, value, who)
253
254     def _validate(self, value):
255         if not self.open_values:
256             return value is None or value in self.values
257         else:
258             return True
259
260 class BoolOption(Option):
261     opt_type = 'bool'
262     
263     def _validate(self, value):
264         return isinstance(value, bool)
265
266 # config level validator             
267 #    def setoption(self, config, value, who):
268 #        name = self._name
269 #        if value and self._validator is not None:
270 #            toplevel = config._cfgimpl_get_toplevel()
271 #            self._validator(toplevel)
272 #        super(BoolOption, self).setoption(config, value, who)
273
274 class IntOption(Option):
275     opt_type = 'int'
276     
277     def _validate(self, value):
278         try:
279             int(value)
280         except TypeError:
281             return False
282         return True
283                             
284     def setoption(self, config, value, who):
285         try:
286             super(IntOption, self).setoption(config, value, who)
287         except TypeError, e:
288             raise ConfigError(*e.args)
289
290 class FloatOption(Option):
291     opt_type = 'float'
292
293     def _validate(self, value):
294         try:
295             float(value)
296         except TypeError:
297             return False
298         return True
299
300     def setoption(self, config, value, who):
301         try:
302             super(FloatOption, self).setoption(config, float(value), who)
303         except TypeError, e:
304             raise ConfigError(*e.args)
305
306 class StrOption(Option):
307     opt_type = 'string'
308     
309     def _validate(self, value):
310         return isinstance(value, str)
311                                      
312     def setoption(self, config, value, who):
313         try:
314             super(StrOption, self).setoption(config, value, who)
315         except TypeError, e:
316             raise ConfigError(*e.args)
317
318 class SymLinkOption(object):
319     opt_type = 'symlink'
320     
321     def __init__(self, name, path):
322         self._name = name
323         self.path = path 
324     
325     def setoption(self, config, value, who):
326         try:
327             setattr(config, self.path, value) # .setoption(self.path, value, who)
328         except TypeError, e:
329             raise ConfigError(*e.args)
330
331 class IPOption(Option):
332     opt_type = 'ip'
333     
334     def _validate(self, value):
335         # by now the validation is nothing but a string, use IPy instead
336         return isinstance(value, str)
337                                      
338     def setoption(self, config, value, who):
339         try:
340             super(IPOption, self).setoption(config, value, who)
341         except TypeError, e:
342             raise ConfigError(*e.args)
343
344 class NetmaskOption(Option):
345     opt_type = 'netmask'
346     
347     def _validate(self, value):
348         # by now the validation is nothing but a string, use IPy instead
349         return isinstance(value, str)
350                                      
351     def setoption(self, config, value, who):
352         try:
353             super(NetmaskOption, self).setoption(config, value, who)
354         except TypeError, e:
355             raise ConfigError(*e.args)
356
357 class ArbitraryOption(Option):
358     def __init__(self, name, doc, default=None, defaultfactory=None, 
359                                    requires=None, multi=False, mandatory=False):
360         super(ArbitraryOption, self).__init__(name, doc, requires=requires,
361                                                multi=multi, mandatory=mandatory)
362         self.defaultfactory = defaultfactory
363         if defaultfactory is not None:
364             assert default is None
365
366     def _validate(self, value):
367         return True
368
369     def getdefault(self):
370         if self.defaultfactory is not None:
371             return self.defaultfactory()
372         return self.default
373
374 class OptionDescription(HiddenBaseType, DisabledBaseType):
375     
376     def __init__(self, name, doc, children, requires=None):
377         self._name = name
378         self.doc = doc
379         self._children = children
380         self._requires = requires
381         self._build()
382         self.properties = [] # 'hidden', 'disabled'...
383         self.group_type = 'default'
384
385     def getdoc(self):
386         return self.doc
387
388     def _build(self):
389         for child in self._children:
390             setattr(self, child._name, child)
391
392     def add_child(self, child):
393         "dynamically adds a configuration option"
394         #Nothing is static. Even the Mona Lisa is falling apart.
395         for ch in self._children:
396             if isinstance(ch, Option):
397                 if child._name == ch._name:
398                     raise ConflictConfigError("existing option : {0}".format(
399                                                                    child._name))
400         self._children.append(child)
401         setattr(self, child._name, child)
402     
403     def update_child(self, child):
404         "modification of an existing option"
405         # XXX : corresponds to the `redefine`, is it usefull 
406         pass
407         
408     def getkey(self, config):
409         return tuple([child.getkey(getattr(config, child._name))
410                       for child in self._children])
411
412     def getpaths(self, include_groups=False, currpath=None):
413         """returns a list of all paths in self, recursively
414            currpath should not be provided (helps with recursion)
415         """
416         if currpath is None:
417             currpath = []
418         paths = []
419         for option in self._children:
420             attr = option._name
421             if attr.startswith('_cfgimpl'):
422                 continue
423             value = getattr(self, attr)
424             if isinstance(value, OptionDescription):
425                 if include_groups:
426                     paths.append('.'.join(currpath + [attr]))
427                 currpath.append(attr)
428                 paths += value.getpaths(include_groups=include_groups,
429                                         currpath=currpath)
430                 currpath.pop()
431             else:
432                 paths.append('.'.join(currpath + [attr]))
433         return paths
434     # ____________________________________________________________
435
436     def set_group_type(self, group_type):
437         if group_type in group_types:
438             self.group_type = group_type
439         else:
440             raise ConfigError('not allowed value for group_type : {0}'.format(
441                               group_type))
442     
443     def get_group_type(self):
444         return self.group_type
445     # ____________________________________________________________
446     def hide(self):
447         super(OptionDescription, self).hide()
448         # FIXME : AND THE SUBCHILDREN ? 
449         for child in self._children:
450             if isinstance(child, OptionDescription):
451                 child.hide()
452     
453     def show(self):
454         # FIXME : AND THE SUBCHILDREN ?? 
455         super(OptionDescription, self).show()
456         for child in self._children:
457             if isinstance(child, OptionDescription):
458                 child.show()
459     # ____________________________________________________________
460     def disable(self):
461         super(OptionDescription, self).disable()
462         # FIXME : AND THE SUBCHILDREN ? 
463         for child in self._children:
464             if isinstance(child, OptionDescription):
465                 child.disable()
466     
467     def enable(self):
468         # FIXME : AND THE SUBCHILDREN ? 
469         super(OptionDescription, self).enable()
470         for child in self._children:
471             if isinstance(child, OptionDescription):
472                 child.enable()
473 # ____________________________________________________________
474 def apply_requires(opt, config):
475     if hasattr(opt, '_requires'):
476         if opt._requires is not None:
477             # malformed requirements
478             rootconfig = config._cfgimpl_get_toplevel()
479             for req in opt._requires:
480                 if not type(req) == tuple and len(req) in (3, 4):
481                     raise RequiresError("malformed requirements for option:"
482                                                    " {0}".format(opt._name))
483             # all actions **must** be identical
484             actions = [req[2] for req in opt._requires]
485             action = actions[0]
486             for act in actions:
487                 if act != action:
488                     raise RequiresError("malformed requirements for option:"
489                                                    " {0}".format(opt._name))
490             # filters the callbacks
491             matches = False
492             for req in opt._requires:
493                 if len(req) == 3:
494                     name, expected, action = req
495                     inverted = False
496                 if len(req) == 4:
497                     name, expected, action, inverted = req
498                     if inverted == 'inverted':
499                         inverted = True
500                 path = config._cfgimpl_get_path() + '.' + opt._name
501                 if name.startswith(path):
502                     raise RequirementRecursionError("malformed requirements imbrication "
503                           "detected for option: '{0}' with requirement on: '{1}'".format(path, name))
504                 homeconfig, shortname = \
505                                       rootconfig._cfgimpl_get_home_by_path(name)
506                 # FIXME: doesn't work with 'auto' or 'fill' yet 
507                 # (copy the code from the __getattr__
508                 if shortname in homeconfig._cfgimpl_values:
509                     value = homeconfig._cfgimpl_values[shortname]
510                     if (not inverted and value == expected) or \
511                             (inverted and value != expected):
512                         if action not in available_actions:
513                             raise RequiresError("malformed requirements"
514                                            " for option: {0}".format(opt._name))
515                         getattr(opt, action)() #.hide() or show() or...
516                         matches = True
517                 else: # option doesn't exist ! should not happen...
518                     raise NotFoundError("required option not found: "
519                                                              "{0}".format(name))
520             # no callback has been triggered, then just reverse the action
521             if not matches:
522                 getattr(opt, reverse_actions[action])()
523