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