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