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