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