851ac770f96ca4b00476664df4179c375ca35e08
[tiramisu.git] / src / config.py
1 # -*- coding: utf-8 -*-
2 "pretty small and local configuration management tool"
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 error import (HiddenOptionError, ConfigError, NotFoundError, 
24                 AmbigousOptionError, ConflictConfigError, NoMatchingOptionFound, 
25                             SpecialOwnersError, MandatoryError, MethodCallError, 
26                                            DisabledOptionError, ModeOptionError)
27 from option import (OptionDescription, Option, SymLinkOption, group_types, 
28                     Multi, apply_requires, modes)
29 from autolib import special_owners, special_owner_factory
30 from copy import copy
31 # ______________________________________________________________________
32 # generic owner. 'default' is the general config owner after init time
33 default_owner = 'user'
34 # ____________________________________________________________
35 class Config(object):
36     _cfgimpl_hidden = True
37     _cfgimpl_disabled = True
38     _cfgimpl_mandatory = True
39     _cfgimpl_frozen = False
40     _cfgimpl_owner = default_owner
41     _cfgimpl_toplevel = None
42     _cfgimpl_mode = 'normal'
43     
44     def __init__(self, descr, parent=None, **overrides):
45         self._cfgimpl_descr = descr
46         self._cfgimpl_value_owners = {}
47         self._cfgimpl_parent = parent
48         # `Config()` indeed takes care of the `Option()`'s values
49         self._cfgimpl_values = {}
50         self._cfgimpl_previous_values = {}
51         # XXX warnings are a great idea, let's make up a better use of it 
52         self._cfgimpl_warnings = []
53         self._cfgimpl_toplevel = self._cfgimpl_get_toplevel()
54         # `freeze()` allows us to carry out this calculation again if necessary 
55         self._cfgimpl_frozen = self._cfgimpl_toplevel._cfgimpl_frozen 
56         self._cfgimpl_build(overrides)
57
58     def _validate_duplicates(self, children):
59         duplicates = []
60         for dup in children:
61             if dup._name not in duplicates:
62                 duplicates.append(dup._name)
63             else:
64                 raise ConflictConfigError('duplicate option name: ' 
65                     '{0}'.format(dup._name))
66
67     def _cfgimpl_build(self, overrides):
68         self._validate_duplicates(self._cfgimpl_descr._children)
69         for child in self._cfgimpl_descr._children:
70             if isinstance(child, Option):
71                 if child.is_multi():
72                     childdef = Multi(copy(child.getdefault()), config=self, 
73                                      child=child)
74                     self._cfgimpl_values[child._name] = childdef
75                     self._cfgimpl_previous_values[child._name] = list(childdef)
76                 else:
77                     childdef = child.getdefault()
78                     self._cfgimpl_values[child._name] = childdef
79                     self._cfgimpl_previous_values[child._name] = childdef 
80                 if child.getcallback() is not None:
81                     if child._is_hidden():
82                         self._cfgimpl_value_owners[child._name] = 'auto'
83                     else:
84                         self._cfgimpl_value_owners[child._name] = 'fill'
85                 else:
86                     if child.is_multi():
87                         self._cfgimpl_value_owners[child._name] = ['default' \
88                             for i in range(len(child.getdefault() ))]
89                     else:
90                         self._cfgimpl_value_owners[child._name] = 'default'
91             elif isinstance(child, OptionDescription):
92                 self._validate_duplicates(child._children)
93                 self._cfgimpl_values[child._name] = Config(child, parent=self)
94         self.override(overrides)
95
96     def cfgimpl_update(self):
97         "dynamically adds `Option()` or `OptionDescription()`"
98         # Nothing is static. Everything evolve.
99         # FIXME this is an update for new options in the schema only 
100         # see the update_child() method of the descr object 
101         for child in self._cfgimpl_descr._children:
102             if isinstance(child, Option):
103                 if child._name not in self._cfgimpl_values:
104                     if child.is_multi():
105                         self._cfgimpl_values[child._name] = Multi(
106                                 copy(child.getdefault()), config=self, child=child)
107                         self._cfgimpl_value_owners[child._name] = ['default' \
108                                 for i in range(len(child.getdefault() ))]
109                     else:
110                         self._cfgimpl_values[child._name] = copy(child.getdefault())
111                         self._cfgimpl_value_owners[child._name] = 'default'
112             elif isinstance(child, OptionDescription):
113                 if child._name not in self._cfgimpl_values:
114                     self._cfgimpl_values[child._name] = Config(child, parent=self)
115
116     def override(self, overrides):
117         for name, value in overrides.iteritems():
118             homeconfig, name = self._cfgimpl_get_home_by_path(name)
119             #  if there are special_owners, impossible to override
120             if homeconfig._cfgimpl_value_owners[name] in special_owners:
121                 raise SpecialOwnersError("cannot override option: {0} because "
122                                             "of its special owner".format(name))
123             homeconfig.setoption(name, value, 'default')
124
125     def cfgimpl_set_owner(self, owner):
126         self._cfgimpl_owner = owner
127         for child in self._cfgimpl_descr._children:
128             if isinstance(child, OptionDescription):
129                 self._cfgimpl_values[child._name].cfgimpl_set_owner(owner)
130     # ____________________________________________________________
131     def cfgimpl_hide(self):
132         if self._cfgimpl_parent != None:
133             raise MethodCallError("this method root_hide() shall not be"
134                                            "used with non-root Config() object") 
135         rootconfig = self._cfgimpl_get_toplevel()
136         rootconfig._cfgimpl_hidden = True
137
138     def cfgimpl_show(self):
139         if self._cfgimpl_parent != None:
140             raise MethodCallError("this method root_hide() shall not be"
141                                            "used with non-root Config() object") 
142         rootconfig = self._cfgimpl_get_toplevel()
143         rootconfig._cfgimpl_hidden = False
144     # ____________________________________________________________
145     def cfgimpl_disable(self):
146         if self._cfgimpl_parent != None:
147             raise MethodCallError("this method root_hide() shall not be"
148                                            "used with non-root Confit() object") 
149         rootconfig = self._cfgimpl_get_toplevel()
150         rootconfig._cfgimpl_disabled = True
151
152     def cfgimpl_enable(self):
153         if self._cfgimpl_parent != None:
154             raise MethodCallError("this method root_hide() shall not be"
155                                            "used with non-root Confit() object") 
156         rootconfig = self._cfgimpl_get_toplevel()
157         rootconfig._cfgimpl_disabled = False
158     # ____________________________________________________________
159     def __setattr__(self, name, value):
160         if '.' in name:
161             homeconfig, name = self._cfgimpl_get_home_by_path(name)
162             return setattr(homeconfig, name, value)
163
164         if name.startswith('_cfgimpl_'):
165             self.__dict__[name] = value
166             return
167         if self._cfgimpl_frozen and getattr(self, name) != value:
168             raise TypeError("trying to change a value in a frozen config"
169                                                 ": {0} {1}".format(name, value))
170         if type(getattr(self._cfgimpl_descr, name)) != SymLinkOption:
171             self._validate(name, getattr(self._cfgimpl_descr, name))
172         self.setoption(name, value, self._cfgimpl_owner)
173         
174     def _validate(self, name, opt_or_descr):
175         apply_requires(opt_or_descr, self) 
176         if not type(opt_or_descr) == OptionDescription:
177             # hidden options
178             if self._cfgimpl_toplevel._cfgimpl_hidden and \
179                 (opt_or_descr._is_hidden() or self._cfgimpl_descr._is_hidden()):
180                 raise HiddenOptionError("trying to access to a hidden option:"
181                                                            " {0}".format(name))
182             # disabled options
183             if self._cfgimpl_toplevel._cfgimpl_disabled and \
184             (opt_or_descr._is_disabled() or self._cfgimpl_descr._is_disabled()):
185                 raise DisabledOptionError("this option is disabled:"
186                                                             " {0}".format(name))
187             # expert options 
188             # XXX currently doesn't look at the group, is it really necessary ?
189             if self._cfgimpl_toplevel._cfgimpl_mode != 'normal':
190                 if opt_or_descr.get_mode() != 'normal':
191                     raise ModeOptionError("this option's mode is not normal:"
192                                                             " {0}".format(name))
193
194     def __getattr__(self, name):
195         # attribute access by passing a path, 
196         # for instance getattr(self, "creole.general.family.adresse_ip_eth0") 
197         if '.' in name:
198             homeconfig, name = self._cfgimpl_get_home_by_path(name)
199             return getattr(homeconfig, name)
200         opt_or_descr = getattr(self._cfgimpl_descr, name)
201         # symlink options 
202         if type(opt_or_descr) == SymLinkOption:
203             return getattr(self, opt_or_descr.path)
204         self._validate(name, opt_or_descr)
205         # special attributes
206         if name.startswith('_cfgimpl_'):
207             # if it were in __dict__ it would have been found already
208             return self.__dict__[name]
209             raise AttributeError("%s object has no attribute %s" %
210                                  (self.__class__, name))
211         if name not in self._cfgimpl_values:
212             raise AttributeError("%s object has no attribute %s" %
213                                  (self.__class__, name))
214         if name in self._cfgimpl_value_owners:
215             owner = self._cfgimpl_value_owners[name]
216             if owner in special_owners:
217                 value = self._cfgimpl_values[name]
218                 if value != None:
219                     if opt_or_descr.is_multi():
220                         if owner == 'fill' and None not in value:
221                             return value
222                     else:
223                         if owner == 'fill' and value != None:
224                             return value
225                 result = special_owner_factory(name, owner, 
226                             value=value,
227                             callback=opt_or_descr.getcallback(),
228                             callback_params=opt_or_descr.getcallback_params(),
229                             config=self._cfgimpl_get_toplevel())
230                 # this result **shall not** be a list 
231                 # for example, [1, 2, 3, None] -> [1, 2, 3, result]
232                 if isinstance(result, list):
233                     raise ConfigError('invalid calculated value returned'
234                         ' for option {0} : shall not be a list'.format(name))
235                 if result != None and not opt_or_descr._validate(result):
236                     raise ConfigError('invalid calculated value returned'
237                         ' for option {0}'.format(name))
238                 if opt_or_descr.is_multi():
239                     if value == []:
240                         _result = Multi([result], value.config, value.child)
241                     else:
242                         _result = Multi([], value.config, value.child)
243                         for val in value:
244                             if val == None:
245                                 val = result
246                             _result.append(val)
247                 else:
248                     _result = result
249                 return _result
250         # mandatory options
251         if not isinstance(opt_or_descr, OptionDescription):
252             homeconfig = self._cfgimpl_get_toplevel()
253             mandatory = homeconfig._cfgimpl_mandatory
254             if opt_or_descr.is_mandatory() and mandatory:
255                 if self._cfgimpl_values[name] == None\
256                   and opt_or_descr.getdefault() == None:
257                     raise MandatoryError("option: {0} is mandatory " 
258                                           "and shall have a value".format(name))
259         return self._cfgimpl_values[name]
260                 
261     def __dir__(self):
262         #from_type = dir(type(self))
263         from_dict = list(self.__dict__)
264         extras = list(self._cfgimpl_values)
265         return sorted(set(extras + from_dict))
266
267     def unwrap_from_name(self, name):
268         # didn't have to stoop so low: `self.get()` must be the proper method
269         # **and it is slow**: it recursively searches into the namespaces
270         paths = self.getpaths(allpaths=True)
271         opts = dict([(path, self.unwrap_from_path(path)) for path in paths])
272         all_paths = [p.split(".") for p in self.getpaths()]
273         for pth in all_paths:
274             if name in pth:
275                 return opts[".".join(pth)]
276         raise NotFoundError("name: {0} not found".format(name))
277         
278     def unwrap_from_path(self, path):
279         # didn't have to stoop so low, `geattr(self, path)` is much better
280         # **fast**: finds the option directly in the appropriate namespace
281         if '.' in path:
282             homeconfig, path = self._cfgimpl_get_home_by_path(path)
283             return getattr(homeconfig._cfgimpl_descr, path)
284         return getattr(self._cfgimpl_descr, path)
285                                             
286     def __delattr__(self, name):
287         # if you use delattr you are responsible for all bad things happening
288         if name.startswith('_cfgimpl_'):
289             del self.__dict__[name]
290             return
291         self._cfgimpl_value_owners[name] = 'default'
292         opt = getattr(self._cfgimpl_descr, name)
293         if isinstance(opt, OptionDescription):
294             raise AttributeError("can't option subgroup")
295         self._cfgimpl_values[name] = getattr(opt, 'default', None)
296
297     def setoption(self, name, value, who=None):
298         #who is **not necessarily** a owner, because it cannot be a list
299         #FIXME : sortir le setoption pour les multi, ca ne devrait pas être la
300         child = getattr(self._cfgimpl_descr, name)
301         if who == None:
302             if child.is_multi():
303                 newowner = [self._cfgimpl_owner for i in range(len(value))] 
304             else:
305                 newowner = self._cfgimpl_owner
306         else:
307             if type(child) != SymLinkOption:
308                 if child.is_multi():
309                     if type(value) != Multi:
310                         if type(value) == list:
311                             value = Multi(value, self, child)
312                         else:
313                             raise ConfigError("invalid value for option:"
314                                        " {0} that is set to multi".format(name))
315                     newowner = [who for i in range(len(value))]
316                 else:
317                     newowner = who 
318         if type(child) != SymLinkOption:
319             if name not in self._cfgimpl_values:
320                 raise AttributeError('unknown option %s' % (name,))
321             # special owners, a value with a owner *auto* cannot be changed
322             oldowner = self._cfgimpl_value_owners[child._name]
323             if oldowner == 'auto':
324                 if who == 'auto':
325                     raise ConflictConfigError('cannot override value to %s for '
326                                           'option %s' % (value, name))
327             if oldowner == who:
328                 oldvalue = getattr(self, name)
329                 if oldvalue == value: #or who in ("default",):
330                     return
331             child.setoption(self, value, who)
332             # if the value owner is 'auto', set the option to hidden 
333             if who == 'auto':
334                 if not child._is_hidden():
335                     child.hide()
336             if (value is None and who != 'default' and not child.is_multi()):
337                 child.setowner(self, 'default')
338                 self._cfgimpl_values[name] = copy(child.getdefault())
339             elif (value == [] and who != 'default' and child.is_multi()):
340                 child.setowner(self, ['default' for i in range(len(child.getdefault()))])
341                 self._cfgimpl_values[name] = Multi(copy(child.getdefault()),
342                             config=self, child=child)
343             else:         
344                 child.setowner(self, newowner)
345         else:
346             homeconfig = self._cfgimpl_get_toplevel()
347             child.setoption(homeconfig, value, who)
348
349     def set(self, **kwargs):
350         all_paths = [p.split(".") for p in self.getpaths(allpaths=True)]
351         for key, value in kwargs.iteritems():
352             key_p = key.split('.')
353             candidates = [p for p in all_paths if p[-len(key_p):] == key_p]
354             if len(candidates) == 1:
355                 name = '.'.join(candidates[0])
356                 homeconfig, name = self._cfgimpl_get_home_by_path(name)
357                 try:
358                     getattr(homeconfig, name)
359                 except MandatoryError:
360                     pass
361                 except Exception, e:
362                     raise e # HiddenOptionError or DisabledOptionError
363                 homeconfig.setoption(name, value, self._cfgimpl_owner)
364             elif len(candidates) > 1:
365                 raise AmbigousOptionError(
366                     'more than one option that ends with %s' % (key, ))
367             else:
368                 raise NoMatchingOptionFound(
369                     'there is no option that matches %s' 
370                     ' or the option is hidden or disabled'% (key, ))
371
372     def get(self, name):
373         paths = self.getpaths(allpaths=True)
374         pathsvalues = []
375         for path in paths:
376             pathname = path.split('.')[-1]
377             if pathname == name:
378                 try:
379                     value = getattr(self, path)            
380                     return value 
381                 except Exception, e:
382                     raise e
383         raise NotFoundError("option {0} not found in config".format(name))                    
384
385     def _cfgimpl_get_home_by_path(self, path):
386         """returns tuple (config, name)"""
387         path = path.split('.')
388         
389         for step in path[:-1]:
390             self = getattr(self, step)
391         return self, path[-1]
392
393     def _cfgimpl_get_toplevel(self):
394         while self._cfgimpl_parent is not None:
395             self = self._cfgimpl_parent
396         return self
397
398     def cfgimpl_previous_value(self, path):
399         home, name = self._cfgimpl_get_home_by_path(path)
400         return home._cfgimpl_previous_values[name]
401     
402     def get_previous_value(self, name):
403         return self._cfgimpl_previous_values[name]
404              
405     def add_warning(self, warning):
406         self._cfgimpl_get_toplevel()._cfgimpl_warnings.append(warning)
407
408     def get_warnings(self):
409         return self._cfgimpl_get_toplevel()._cfgimpl_warnings
410     # ____________________________________________________________
411     # freeze and read-write statuses
412     def cfgimpl_freeze(self):
413         rootconfig = self._cfgimpl_get_toplevel()
414         rootconfig._cfgimpl_frozen = True
415         self._cfgimpl_frozen = True
416
417     def cfgimpl_unfreeze(self):
418         rootconfig = self._cfgimpl_get_toplevel()
419         rootconfig._cfgimpl_frozen = False
420         self._cfgimpl_frozen = False
421
422     def is_frozen(self):
423         # it should be the same value as self._cfgimpl_frozen...
424         rootconfig = self._cfgimpl_get_toplevel()
425         return rootconfig.__dict__['_cfgimpl_frozen']
426
427     def cfgimpl_read_only(self):
428         # hung up on freeze, hidden and disabled concepts 
429         self.cfgimpl_freeze()
430         rootconfig = self._cfgimpl_get_toplevel()
431         rootconfig._cfgimpl_hidden = False
432         rootconfig._cfgimpl_disabled = True
433         rootconfig._cfgimpl_mandatory = True
434
435     def cfgimpl_set_mode(self, mode):
436         # normal or expert mode
437         rootconfig = self._cfgimpl_get_toplevel()
438         if mode not in modes:
439             raise ConfigError("mode {0} not available".format(mode))
440         rootconfig._cfgimpl_mode = mode
441     
442     def cfgimpl_read_write(self):
443         # hung up on freeze, hidden and disabled concepts
444         self.cfgimpl_unfreeze()
445         rootconfig = self._cfgimpl_get_toplevel()
446         rootconfig._cfgimpl_hidden = True
447         rootconfig._cfgimpl_disabled = True
448         rootconfig._cfgimpl_mandatory = False
449     # ____________________________________________________________
450     def getkey(self):
451         return self._cfgimpl_descr.getkey(self)
452
453     def __hash__(self):
454         return hash(self.getkey())
455
456     def __eq__(self, other):
457         return self.getkey() == other.getkey()
458
459     def __ne__(self, other):
460         return not self == other
461
462     def __iter__(self):
463         # iteration only on Options (not OptionDescriptions)
464         for child in self._cfgimpl_descr._children:
465             if isinstance(child, Option):
466                 try:
467                     yield child._name, getattr(self, child._name)
468                 except:
469                     pass # hidden, disabled option group
470
471     def iter_groups(self, group_type=None):
472         "iteration on OptionDescriptions"
473         if group_type == None:
474             groups = group_types
475         else:
476             if group_type not in group_types:
477                 raise TypeError("Unknown group_type: {0}".format(group_type))
478             groups = [group_type]
479         for child in self._cfgimpl_descr._children:
480             if isinstance(child, OptionDescription):
481                     try:
482                         if child.get_group_type() in groups: 
483                             yield child._name, getattr(self, child._name)
484                     except:
485                         pass # hidden, disabled option
486                     
487     def __str__(self, indent=""):
488         lines = []
489         children = [(child._name, child)
490                     for child in self._cfgimpl_descr._children]
491         children.sort()
492         for name, child in children:
493             if self._cfgimpl_value_owners.get(name, None) == 'default':
494                 continue
495             value = getattr(self, name)
496             if isinstance(value, Config):
497                 substr = value.__str__(indent + "    ")
498             else:
499                 substr = "%s    %s = %s" % (indent, name, value)
500             if substr:
501                 lines.append(substr)
502         if indent and not lines:
503             return ''   # hide subgroups with all default values
504         lines.insert(0, "%s[%s]" % (indent, self._cfgimpl_descr._name,))
505         return '\n'.join(lines)
506
507     def getpaths(self, include_groups=False, allpaths=False):
508         """returns a list of all paths in self, recursively, taking care of 
509         the context (hidden/disabled)
510         """
511         paths = []
512         for path in self._cfgimpl_descr.getpaths(include_groups=include_groups):
513             try: 
514                 value = getattr(self, path)
515             except Exception, e:
516                 if not allpaths:
517                     pass # hidden or disabled option
518                 else:
519                     paths.append(path) # hidden or disabled option added
520             else:
521                 paths.append(path)
522         return paths 
523         
524 def make_dict(config, flatten=False):
525     paths = config.getpaths()
526     pathsvalues = []
527     for path in paths:
528         if flatten:
529             pathname = path.split('.')[-1]
530         else:
531             pathname = path
532         try:
533             value = getattr(config, path)            
534             pathsvalues.append((pathname, value))      
535         except:
536             pass # this just a hidden or disabled option  
537     options = dict(pathsvalues)
538     return options
539 # ____________________________________________________________
540