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