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