None and [] are both possible
[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 pypy's guys: 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 takes care of the `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         self._cfgimpl_build(overrides)
54
55     def _validate_duplicates(self, children):
56         duplicates = []
57         for dup in children:
58             if dup._name not in duplicates:
59                 duplicates.append(dup._name)
60             else:
61                 raise ConflictConfigError('duplicate option name: ' 
62                     '{0}'.format(dup._name))
63
64     def _cfgimpl_build(self, overrides):
65         self._validate_duplicates(self._cfgimpl_descr._children)
66         for child in self._cfgimpl_descr._children:
67             if isinstance(child, Option):
68                 self._cfgimpl_values[child._name] = child.getdefault()
69                 self._cfgimpl_previous_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             if (value is None and who != 'default' and not child.is_multi()):
322                 child.setowner(self, 'default')
323                 self._cfgimpl_values[name] = child.getdefault()
324             elif (value == [] and who != 'default' and child.is_multi()):
325                 child.setowner(self, ['default' for i in range(len(child.getdefault()))])
326                 self._cfgimpl_values[name] = child.getdefault()
327             else:         
328                 child.setowner(self, newowner)
329         else:
330             homeconfig = self._cfgimpl_get_toplevel()
331             child.setoption(homeconfig, value, who)
332
333     def set(self, **kwargs):
334         all_paths = [p.split(".") for p in self.getpaths(allpaths=True)]
335         for key, value in kwargs.iteritems():
336             key_p = key.split('.')
337             candidates = [p for p in all_paths if p[-len(key_p):] == key_p]
338             if len(candidates) == 1:
339                 name = '.'.join(candidates[0])
340                 homeconfig, name = self._cfgimpl_get_home_by_path(name)
341                 try:
342                     getattr(homeconfig, name)
343                 except MandatoryError:
344                     pass
345                 except Exception, e:
346                     raise e # HiddenOptionError or DisabledOptionError
347                 homeconfig.setoption(name, value, self._cfgimpl_owner)
348             elif len(candidates) > 1:
349                 raise AmbigousOptionError(
350                     'more than one option that ends with %s' % (key, ))
351             else:
352                 raise NoMatchingOptionFound(
353                     'there is no option that matches %s' 
354                     ' or the option is hidden or disabled'% (key, ))
355
356     def get(self, name):
357         paths = self.getpaths(allpaths=True)
358         pathsvalues = []
359         for path in paths:
360             pathname = path.split('.')[-1]
361             if pathname == name:
362                 try:
363                     value = getattr(self, path)            
364                     return value 
365                 except Exception, e:
366                     raise e
367         raise NotFoundError("option {0} not found in config".format(name))                    
368
369     def _cfgimpl_get_home_by_path(self, path):
370         """returns tuple (config, name)"""
371         path = path.split('.')
372         
373         for step in path[:-1]:
374             self = getattr(self, step)
375         return self, path[-1]
376
377     def _cfgimpl_get_toplevel(self):
378         while self._cfgimpl_parent is not None:
379             self = self._cfgimpl_parent
380         return self
381
382     def cfgimpl_previous_value(self, path):
383         home, name = self._cfgimpl_get_home_by_path(path)
384         return home._cfgimpl_previous_values[name]
385     
386     def get_previous_value(self, name):
387         return self._cfgimpl_previous_values[name]
388              
389     def add_warning(self, warning):
390         self._cfgimpl_get_toplevel()._cfgimpl_warnings.append(warning)
391
392     def get_warnings(self):
393         return self._cfgimpl_get_toplevel()._cfgimpl_warnings
394     # ____________________________________________________________
395     # freeze and read-write statuses
396     def cfgimpl_freeze(self):
397         rootconfig = self._cfgimpl_get_toplevel()
398         rootconfig._cfgimpl_frozen = True
399         self._cfgimpl_frozen = True
400
401     def cfgimpl_unfreeze(self):
402         rootconfig = self._cfgimpl_get_toplevel()
403         rootconfig._cfgimpl_frozen = False
404         self._cfgimpl_frozen = False
405
406     def is_frozen(self):
407         # it should be the same value as self._cfgimpl_frozen...
408         rootconfig = self._cfgimpl_get_toplevel()
409         return rootconfig.__dict__['_cfgimpl_frozen']
410
411     def cfgimpl_read_only(self):
412         # hung up on freeze, hidden and disabled concepts 
413         self.cfgimpl_freeze()
414         rootconfig = self._cfgimpl_get_toplevel()
415         rootconfig._cfgimpl_hidden = False
416         rootconfig._cfgimpl_disabled = True
417         rootconfig._cfgimpl_mandatory = True
418
419     def cfgimpl_set_mode(self, mode):
420         # normal or expert mode
421         rootconfig = self._cfgimpl_get_toplevel()
422         if mode not in modes:
423             raise ConfigError("mode {0} not available".format(mode))
424         rootconfig._cfgimpl_mode = mode
425     
426     def cfgimpl_read_write(self):
427         # hung up on freeze, hidden and disabled concepts
428         self.cfgimpl_unfreeze()
429         rootconfig = self._cfgimpl_get_toplevel()
430         rootconfig._cfgimpl_hidden = True
431         rootconfig._cfgimpl_disabled = False
432         rootconfig._cfgimpl_mandatory = False
433     # ____________________________________________________________
434     def getkey(self):
435         return self._cfgimpl_descr.getkey(self)
436
437     def __hash__(self):
438         return hash(self.getkey())
439
440     def __eq__(self, other):
441         return self.getkey() == other.getkey()
442
443     def __ne__(self, other):
444         return not self == other
445
446     def __iter__(self):
447         # iteration only on Options (not OptionDescriptions)
448         for child in self._cfgimpl_descr._children:
449             if isinstance(child, Option):
450                 try:
451                     yield child._name, getattr(self, child._name)
452                 except:
453                     pass # hidden, disabled option group
454
455     def iter_groups(self, group_type=None):
456         "iteration on OptionDescriptions"
457         if group_type == None:
458             groups = group_types
459         else:
460             if group_type not in group_types:
461                 raise TypeError("Unknown group_type: {0}".format(group_type))
462             groups = [group_type]
463         for child in self._cfgimpl_descr._children:
464             if isinstance(child, OptionDescription):
465                     try:
466                         if child.get_group_type() in groups: 
467                             yield child._name, getattr(self, child._name)
468                     except:
469                         pass # hidden, disabled option
470                     
471     def __str__(self, indent=""):
472         lines = []
473         children = [(child._name, child)
474                     for child in self._cfgimpl_descr._children]
475         children.sort()
476         for name, child in children:
477             if self._cfgimpl_value_owners.get(name, None) == 'default':
478                 continue
479             value = getattr(self, name)
480             if isinstance(value, Config):
481                 substr = value.__str__(indent + "    ")
482             else:
483                 substr = "%s    %s = %s" % (indent, name, value)
484             if substr:
485                 lines.append(substr)
486         if indent and not lines:
487             return ''   # hide subgroups with all default values
488         lines.insert(0, "%s[%s]" % (indent, self._cfgimpl_descr._name,))
489         return '\n'.join(lines)
490
491     def getpaths(self, include_groups=False, allpaths=False):
492         """returns a list of all paths in self, recursively, taking care of 
493         the context (hidden/disabled)
494         """
495         paths = []
496         for path in self._cfgimpl_descr.getpaths(include_groups=include_groups):
497             try: 
498                 value = getattr(self, path)
499             except Exception, e:
500                 if not allpaths:
501                     pass # hidden or disabled option
502                 else:
503                     paths.append(path) # hidden or disabled option added
504             else:
505                 paths.append(path)
506         return paths 
507         
508 def make_dict(config, flatten=False):
509     paths = config.getpaths()
510     pathsvalues = []
511     for path in paths:
512         if flatten:
513             pathname = path.split('.')[-1]
514         else:
515             pathname = path
516         try:
517             value = getattr(config, path)            
518             pathsvalues.append((pathname, value))      
519         except:
520             pass # this just a hidden or disabled option  
521     options = dict(pathsvalues)
522     return options
523 # ____________________________________________________________
524