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