668821807dee8e4a0861def570d877a5e1815147
[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 supports the configuration `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         #
54         self._cfgimpl_build(overrides)
55
56     def _validate_duplicates(self, children):
57         duplicates = []
58         for dup in children:
59             if dup._name not in duplicates:
60                 duplicates.append(dup._name)
61             else:
62                 raise ConflictConfigError('duplicate option name: <%s>' % \
63                                                                       dup._name)
64
65     def _cfgimpl_build(self, overrides):
66         self._validate_duplicates(self._cfgimpl_descr._children)
67         for child in self._cfgimpl_descr._children:
68             if isinstance(child, Option):
69                 self._cfgimpl_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 _validate(self, name, opt_or_descr):
159         if not type(opt_or_descr) == OptionDescription:
160             apply_requires(opt_or_descr, self) 
161             # hidden options
162             if self._cfgimpl_toplevel._cfgimpl_hidden and \
163                 (opt_or_descr._is_hidden() or self._cfgimpl_descr._is_hidden()):
164                 raise HiddenOptionError("trying to access to a hidden option:"
165                                                            " {0}".format(name))
166             # disabled options
167             if self._cfgimpl_toplevel._cfgimpl_disabled and \
168             (opt_or_descr._is_disabled() or self._cfgimpl_descr._is_disabled()):
169                 raise DisabledOptionError("this option is disabled:"
170                                                             " {0}".format(name))
171             # expert options 
172             # XXX currently doesn't look at the group, is it really necessary ?
173             if self._cfgimpl_toplevel._cfgimpl_mode != 'normal':
174                 if opt_or_descr.get_mode() != 'normal':
175                     raise ModeOptionError("this option's mode is not normal:"
176                                                             " {0}".format(name))
177         if type(opt_or_descr) == OptionDescription:
178             apply_requires(opt_or_descr, self) 
179
180     def __getattr__(self, name):
181         # attribute access by passing a path, 
182         # for instance getattr(self, "creole.general.family.adresse_ip_eth0") 
183         if '.' in name:
184             homeconfig, name = self._cfgimpl_get_home_by_path(name)
185             return getattr(homeconfig, name)
186         opt_or_descr = getattr(self._cfgimpl_descr, name)
187         # symlink options 
188         if type(opt_or_descr) == SymLinkOption:
189             return getattr(self, opt_or_descr.path)
190         self._validate(name, opt_or_descr)
191         # special attributes
192         if name.startswith('_cfgimpl_'):
193             # if it were in __dict__ it would have been found already
194             return self.__dict__[name]
195             raise AttributeError("%s object has no attribute %s" %
196                                  (self.__class__, name))
197         if name not in self._cfgimpl_values:
198             raise AttributeError("%s object has no attribute %s" %
199                                  (self.__class__, name))
200         if name in self._cfgimpl_value_owners:
201             owner = self._cfgimpl_value_owners[name]
202             # special owners
203             if owner in special_owners:
204                 value = self._cfgimpl_values[name]
205                 if value != None:
206                     if opt_or_descr.is_multi():
207                         if owner == 'fill' and None not in value:
208                             return value
209                     else:
210                         if owner == 'fill' and value != None:
211                             return value
212                 result = special_owner_factory(name, owner, 
213                             value=value,
214                             callback=opt_or_descr.getcallback(),
215                             callback_params=opt_or_descr.getcallback_params(),
216                             config=self._cfgimpl_get_toplevel())
217                 # this result **shall not** be a list 
218                 # for example, [1, 2, 3, None] -> [1, 2, 3, result]
219                 #
220                 if type(result) == list:
221                     raise ConfigError('invalid calculated value returned'
222                         ' for option {0} : shall not be a list'.format(name))
223                 if result != None and not opt_or_descr._validate(result):
224                     raise ConfigError('invalid calculated value returned'
225                         ' for option {0}'.format(name))
226                 
227                 if opt_or_descr.is_multi():
228                     if value == None:
229                         _result = [result]
230                     else:
231                         _result = []
232                         for val in value:
233                             if val == None:
234                                 val = result
235                             _result.append(val)
236                 else:
237                     _result = result
238                 return _result
239         # mandatory options
240         if not isinstance(opt_or_descr, OptionDescription):
241             homeconfig = self._cfgimpl_get_toplevel()
242             mandatory = homeconfig._cfgimpl_mandatory
243             if opt_or_descr.is_mandatory() and mandatory:
244                 if self._cfgimpl_values[name] == None\
245                   and opt_or_descr.getdefault() == None:
246                     raise MandatoryError("option: {0} is mandatory " 
247                                           "and shall have a value".format(name))
248         return self._cfgimpl_values[name]
249
250     def __dir__(self):
251         #from_type = dir(type(self))
252         from_dict = list(self.__dict__)
253         extras = list(self._cfgimpl_values)
254         return sorted(set(extras + from_dict))
255
256     def unwrap_from_name(self, name):
257         # didn't have to stoop so low: `self.get()` must be the proper method
258         # **and it is slow**: it recursively searches into the namespaces
259         paths = self.getpaths(allpaths=True)
260         opts = dict([(path, self.unwrap_from_path(path)) for path in paths])
261         all_paths = [p.split(".") for p in self.getpaths()]
262         for pth in all_paths:
263             if name in pth:
264                 return opts[".".join(pth)]
265         raise NotFoundError("name: {0} not found".format(name))
266         
267     def unwrap_from_path(self, path):
268         # didn't have to stoop so low, `geattr(self, path)` is much better
269         # **fast**: finds the option directly in the appropriate namespace
270         if '.' in path:
271             homeconfig, path = self._cfgimpl_get_home_by_path(path)
272             return getattr(homeconfig._cfgimpl_descr, path)
273         return getattr(self._cfgimpl_descr, path)
274                                             
275     def __delattr__(self, name):
276         # if you use delattr you are responsible for all bad things happening
277         if name.startswith('_cfgimpl_'):
278             del self.__dict__[name]
279             return
280         self._cfgimpl_value_owners[name] = 'default'
281         opt = getattr(self._cfgimpl_descr, name)
282         if isinstance(opt, OptionDescription):
283             raise AttributeError("can't option subgroup")
284         self._cfgimpl_values[name] = getattr(opt, 'default', None)
285
286     def setoption(self, name, value, who=None):
287         "who is **not necessarily** a owner, because it cannot be a list"
288         child = getattr(self._cfgimpl_descr, name)
289         if who == None:
290             if child.is_multi():
291                 newowner = [self._cfgimpl_owner for i in range(len(value))] 
292             else:
293                 newowner = self._cfgimpl_owner
294         else:
295             if type(child) != SymLinkOption:
296                 if child.is_multi():
297                     if type(value) != list:
298                         raise ConfigError("invalid value for option:"
299                                        " {0} that is set to multi".format(name))
300                     newowner = [who for i in range(len(value))]
301                 else:
302                     newowner = who 
303         if type(child) != SymLinkOption:
304             if name not in self._cfgimpl_values:
305                 raise AttributeError('unknown option %s' % (name,))
306             # special owners, a value with a owner *auto* cannot be changed
307             oldowner = self._cfgimpl_value_owners[child._name]
308             if oldowner == 'auto':
309                 if who == 'auto':
310                     raise ConflictConfigError('cannot override value to %s for '
311                                           'option %s' % (value, name))
312             if oldowner == who:
313                 oldvalue = getattr(self, name)
314                 if oldvalue == value: #or who in ("default",):
315                     return
316             child.setoption(self, value, who)
317             # if the value owner is 'auto', set the option to hidden 
318             if who == 'auto':
319                 if not child._is_hidden():
320                     child.hide()
321             self._cfgimpl_value_owners[name] = newowner
322         else:
323             homeconfig = self._cfgimpl_get_toplevel()
324             child.setoption(homeconfig, value, who)
325
326     def set(self, **kwargs):
327         all_paths = [p.split(".") for p in self.getpaths(allpaths=True)]
328         for key, value in kwargs.iteritems():
329             key_p = key.split('.')
330             candidates = [p for p in all_paths if p[-len(key_p):] == key_p]
331             if len(candidates) == 1:
332                 name = '.'.join(candidates[0])
333                 homeconfig, name = self._cfgimpl_get_home_by_path(name)
334                 try:
335                     getattr(homeconfig, name)
336                 except MandatoryError:
337                     pass
338                 except Exception, e:
339                     raise e # HiddenOptionError or DisabledOptionError
340                 homeconfig.setoption(name, value, self._cfgimpl_owner)
341             elif len(candidates) > 1:
342                 raise AmbigousOptionError(
343                     'more than one option that ends with %s' % (key, ))
344             else:
345                 raise NoMatchingOptionFound(
346                     'there is no option that matches %s' 
347                     ' or the option is hidden or disabled'% (key, ))
348
349     def get(self, name):
350         paths = self.getpaths(allpaths=True)
351         pathsvalues = []
352         for path in paths:
353             pathname = path.split('.')[-1]
354             if pathname == name:
355                 try:
356                     value = getattr(self, path)            
357                     return value 
358                 except Exception, e:
359                     raise e
360         raise NotFoundError("option {0} not found in config".format(name))                    
361
362     def _cfgimpl_get_home_by_path(self, path):
363         """returns tuple (config, name)"""
364         path = path.split('.')
365         
366         for step in path[:-1]:
367             self = getattr(self, step)
368         return self, path[-1]
369
370     def _cfgimpl_get_toplevel(self):
371         while self._cfgimpl_parent is not None:
372             self = self._cfgimpl_parent
373         return self
374
375     def add_warning(self, warning):
376         self._cfgimpl_get_toplevel()._cfgimpl_warnings.append(warning)
377
378     def get_warnings(self):
379         return self._cfgimpl_get_toplevel()._cfgimpl_warnings
380     # ____________________________________________________________
381     # freeze and read-write statuses
382     def cfgimpl_freeze(self):
383         rootconfig = self._cfgimpl_get_toplevel()
384         rootconfig._cfgimpl_frozen = True
385         self._cfgimpl_frozen = True
386
387     def cfgimpl_unfreeze(self):
388         rootconfig = self._cfgimpl_get_toplevel()
389         rootconfig._cfgimpl_frozen = False
390         self._cfgimpl_frozen = False
391
392     def is_frozen(self):
393         # it should be the same value as self._cfgimpl_frozen...
394         rootconfig = self._cfgimpl_get_toplevel()
395         return rootconfig.__dict__['_cfgimpl_frozen']
396
397     def cfgimpl_read_only(self):
398         # hung up on freeze, hidden and disabled concepts 
399         self.cfgimpl_freeze()
400         rootconfig = self._cfgimpl_get_toplevel()
401         rootconfig._cfgimpl_hidden = False
402         rootconfig._cfgimpl_disabled = True
403         rootconfig._cfgimpl_mandatory = True
404
405     def cfgimpl_set_mode(self, mode):
406         # normal or expert mode
407         rootconfig = self._cfgimpl_get_toplevel()
408         if mode not in modes:
409             raise ConfigError("mode {0} not available".format(mode))
410         rootconfig._cfgimpl_mode = mode
411     
412     def cfgimpl_read_write(self):
413         # hung up on freeze, hidden and disabled concepts
414         self.cfgimpl_unfreeze()
415         rootconfig = self._cfgimpl_get_toplevel()
416         rootconfig._cfgimpl_hidden = True
417         rootconfig._cfgimpl_disabled = False
418         rootconfig._cfgimpl_mandatory = False
419     # ____________________________________________________________
420     def getkey(self):
421         return self._cfgimpl_descr.getkey(self)
422
423     def __hash__(self):
424         return hash(self.getkey())
425
426     def __eq__(self, other):
427         return self.getkey() == other.getkey()
428
429     def __ne__(self, other):
430         return not self == other
431
432     def __iter__(self):
433         # iteration only on Options (not OptionDescriptions)
434         for child in self._cfgimpl_descr._children:
435             if isinstance(child, Option):
436                 try:
437                     yield child._name, getattr(self, child._name)
438                 except:
439                     pass # hidden, disabled option group
440
441     def iter_groups(self, group_type=None):
442         "iteration on OptionDescriptions"
443         if group_type == None:
444             groups = group_types
445         else:
446             if group_type not in group_types:
447                 raise TypeError("Unknown group_type: {0}".format(group_type))
448             groups = [group_type]
449         for child in self._cfgimpl_descr._children:
450             if isinstance(child, OptionDescription):
451                     try:
452                         if child.get_group_type() in groups: 
453                             yield child._name, getattr(self, child._name)
454                     except:
455                         pass # hidden, disabled option
456                     
457     def __str__(self, indent=""):
458         lines = []
459         children = [(child._name, child)
460                     for child in self._cfgimpl_descr._children]
461         children.sort()
462         for name, child in children:
463             if self._cfgimpl_value_owners.get(name, None) == 'default':
464                 continue
465             value = getattr(self, name)
466             if isinstance(value, Config):
467                 substr = value.__str__(indent + "    ")
468             else:
469                 substr = "%s    %s = %s" % (indent, name, value)
470             if substr:
471                 lines.append(substr)
472         if indent and not lines:
473             return ''   # hide subgroups with all default values
474         lines.insert(0, "%s[%s]" % (indent, self._cfgimpl_descr._name,))
475         return '\n'.join(lines)
476
477     def getpaths(self, include_groups=False, allpaths=False):
478         """returns a list of all paths in self, recursively, taking care of 
479         the context (hidden/disabled)
480         """
481         paths = []
482         for path in self._cfgimpl_descr.getpaths(include_groups=include_groups):
483             try: 
484                 value = getattr(self, path)
485             except Exception, e:
486                 if not allpaths:
487                     pass # hidden or disabled option
488                 else:
489                     paths.append(path) # hidden or disabled option added
490             else:
491                 paths.append(path)
492         return paths 
493         
494 def make_dict(config, flatten=False):
495     paths = config.getpaths()
496     pathsvalues = []
497     for path in paths:
498         if flatten:
499             pathname = path.split('.')[-1]
500         else:
501             pathname = path
502         try:
503             value = getattr(config, path)            
504             pathsvalues.append((pathname, value))      
505         except:
506             pass # this just a hidden or disabled option  
507     options = dict(pathsvalues)
508     return options
509 # ____________________________________________________________
510