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