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