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