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