add docstring and some docs
[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 # ______________________________________________________________________
32 # generic owner. 'default' is the general config owner after init time
33 default_owner = 'user'
34 # ____________________________________________________________
35 class Config(object):
36     "properties attribute: the name of a property enables this property"
37     _cfgimpl_properties = ['hidden', 'disabled']
38     "mandatory means: a mandatory option has to have a value that is not None"
39     _cfgimpl_mandatory = True
40     _cfgimpl_frozen = True
41     _cfgimpl_owner = default_owner
42     _cfgimpl_toplevel = None
43
44     def __init__(self, descr, parent=None, **overrides):
45         """ Configuration option management master class
46         :param descr: describes the configuration schema
47         :type descr: an instance of ``option.OptionDescription``
48         :param overrides: can be used to set different default values
49         (see method ``override``)
50         :param parent: is None if the ``Config`` is root parent Config otherwise
51         :type parent: ``Config``
52         """
53         self._cfgimpl_descr = descr
54         self._cfgimpl_value_owners = {}
55         self._cfgimpl_parent = parent
56         "`Config()` indeed is in charge of the `Option()`'s values"
57         self._cfgimpl_values = {}
58         self._cfgimpl_previous_values = {}
59         "warnings are a great idea, let's make up a better use of it"
60         self._cfgimpl_warnings = []
61         self._cfgimpl_toplevel = self._cfgimpl_get_toplevel()
62         '`freeze()` allows us to carry out this calculation again if necessary'
63         self._cfgimpl_frozen = self._cfgimpl_toplevel._cfgimpl_frozen
64         self._cfgimpl_build(overrides)
65
66     def _validate_duplicates(self, children):
67         """duplicates Option names in the schema
68         :type children: list of `Option` or `OptionDescription`
69         """
70         duplicates = []
71         for dup in children:
72             if dup._name not in duplicates:
73                 duplicates.append(dup._name)
74             else:
75                 raise ConflictConfigError('duplicate option name: '
76                     '{0}'.format(dup._name))
77
78     def _cfgimpl_build(self, overrides):
79         """
80         - builds the config object from the schema
81         - settles various default values for options
82         :param overrides: dict of options name:default values
83         """
84         self._validate_duplicates(self._cfgimpl_descr._children)
85         for child in self._cfgimpl_descr._children:
86             if isinstance(child, Option):
87                 if child.is_multi():
88                     childdef = Multi(copy(child.getdefault()), config=self,
89                                      child=child)
90                     self._cfgimpl_values[child._name] = childdef
91                     self._cfgimpl_previous_values[child._name] = list(childdef)
92                     self._cfgimpl_value_owners[child._name] = ['default' \
93                         for i in range(len(child.getdefault() ))]
94                 else:
95                     childdef = child.getdefault()
96                     self._cfgimpl_values[child._name] = childdef
97                     self._cfgimpl_previous_values[child._name] = childdef
98                     self._cfgimpl_value_owners[child._name] = 'default'
99             elif isinstance(child, OptionDescription):
100                 self._validate_duplicates(child._children)
101                 self._cfgimpl_values[child._name] = Config(child, parent=self)
102         self.override(overrides)
103
104     def cfgimpl_update(self):
105         """dynamically adds `Option()` or `OptionDescription()`
106         """
107         # FIXME this is an update for new options in the schema only
108         #┬ásee the update_child() method of the descr object
109         for child in self._cfgimpl_descr._children:
110             if isinstance(child, Option):
111                 if child._name not in self._cfgimpl_values:
112                     if child.is_multi():
113                         self._cfgimpl_values[child._name] = Multi(
114                                 copy(child.getdefault()), config=self, child=child)
115                         self._cfgimpl_value_owners[child._name] = ['default' \
116                                 for i in range(len(child.getdefault() ))]
117                     else:
118                         self._cfgimpl_values[child._name] = copy(child.getdefault())
119                         self._cfgimpl_value_owners[child._name] = 'default'
120             elif isinstance(child, OptionDescription):
121                 if child._name not in self._cfgimpl_values:
122                     self._cfgimpl_values[child._name] = Config(child, parent=self)
123
124     def override(self, overrides):
125         """
126         overrides default values. This marks the overridden values as defaults.
127         :param overrides: is a dictionary of path strings to values.
128         """
129         for name, value in overrides.iteritems():
130             homeconfig, name = self._cfgimpl_get_home_by_path(name)
131             homeconfig.setoption(name, value, 'default')
132
133     def cfgimpl_set_owner(self, owner):
134         ":param owner: sets the default value for owner at the Config level"
135         self._cfgimpl_owner = owner
136         for child in self._cfgimpl_descr._children:
137             if isinstance(child, OptionDescription):
138                 self._cfgimpl_values[child._name].cfgimpl_set_owner(owner)
139     # ____________________________________________________________
140     # properties methods
141     def _cfgimpl_has_properties(self):
142         "has properties means the Config's properties attribute is not empty"
143         return bool(len(self._cfgimpl_properties))
144
145     def _cfgimpl_has_property(self, propname):
146         """has property propname in the Config's properties attribute
147         :param property: string wich is the name of the property"""
148         return propname in self._cfgimpl_properties
149
150     def cfgimpl_enable_property(self, propname):
151         "puts property propname in the Config's properties attribute"
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 propname not in self._cfgimpl_properties:
156             self._cfgimpl_properties.append(propname)
157
158     def cfgimpl_disable_property(self, propname):
159         "deletes property propname in the Config's properties attribute"
160         if self._cfgimpl_parent != None:
161             raise MethodCallError("this method root_hide() shall not be"
162                                   "used with non-root Config() object")
163         if self._cfgimpl_has_property(propname):
164             self._cfgimpl_properties.remove(propname)
165     # ____________________________________________________________
166     # attribute methods
167     def __setattr__(self, name, value):
168         "attribute notation mechanism for the setting of the value of an option"
169         if name.startswith('_cfgimpl_'):
170             self.__dict__[name] = value
171             return
172         if '.' in name:
173             homeconfig, name = self._cfgimpl_get_home_by_path(name)
174             return setattr(homeconfig, name, value)
175         if type(getattr(self._cfgimpl_descr, name)) != SymLinkOption:
176             self._validate(name, getattr(self._cfgimpl_descr, name))
177         self.setoption(name, value, self._cfgimpl_owner)
178
179     def _validate(self, name, opt_or_descr):
180         "validation for the setattr and the getattr"
181         apply_requires(opt_or_descr, self)
182         if not isinstance(opt_or_descr, Option) and \
183                 not isinstance(opt_or_descr, OptionDescription):
184             raise TypeError('Unexpected object: {0}'.format(repr(opt_or_descr)))
185         properties = copy(opt_or_descr.properties)
186         for proper in properties:
187             if not self._cfgimpl_toplevel._cfgimpl_has_property(proper):
188                 properties.remove(proper)
189         if properties != []:
190             raise PropertiesOptionError("trying to access"
191                     " to an option named: {0} with properties"
192                     " {1}".format(name, str(properties)),
193                     properties)
194
195     def _is_empty(self, opt):
196         "convenience method to know if an option is empty"
197         if (not opt.is_multi() and self._cfgimpl_values[opt._name] == None) or \
198             (opt.is_multi() and (self._cfgimpl_values[opt._name] == [] or \
199                 None in self._cfgimpl_values[opt._name])):
200             return True
201         return False
202
203     def __getattr__(self, name):
204         "attribute notation mechanism for accessing the value of an option"
205         # attribute access by passing a path,
206         # for instance getattr(self, "creole.general.family.adresse_ip_eth0")
207         if '.' in name:
208             homeconfig, name = self._cfgimpl_get_home_by_path(name)
209             return getattr(homeconfig, name)
210         opt_or_descr = getattr(self._cfgimpl_descr, name)
211         # symlink options
212         if type(opt_or_descr) == SymLinkOption:
213             return getattr(self, opt_or_descr.path)
214         if name not in self._cfgimpl_values:
215             raise AttributeError("%s object has no attribute %s" %
216                                  (self.__class__, name))
217         self._validate(name, opt_or_descr)
218         # special attributes
219         if name.startswith('_cfgimpl_'):
220             # if it were in __dict__ it would have been found already
221             return self.__dict__[name]
222             raise AttributeError("%s object has no attribute %s" %
223                                  (self.__class__, name))
224         if not isinstance(opt_or_descr, OptionDescription):
225             # options with callbacks (fill or auto)
226             if opt_or_descr.has_callback():
227                 value = self._cfgimpl_values[name]
228                 if (not opt_or_descr.is_frozen() or \
229                         not opt_or_descr.is_forced_on_freeze()) and value != None:
230                     if opt_or_descr.is_multi():
231                         if None not in value:
232                             return value
233                     else:
234                         return value
235                 result = carry_out_calculation(name,
236                             callback=opt_or_descr.getcallback(),
237                             callback_params=opt_or_descr.getcallback_params(),
238                             config=self._cfgimpl_get_toplevel())
239                 # this result **shall not** be a list
240                 # for example, [1, 2, 3, None] -> [1, 2, 3, result]
241                 if isinstance(result, list):
242                     raise ConfigError('invalid calculated value returned'
243                         ' for option {0} : shall not be a list'.format(name))
244                 if result != None and not opt_or_descr._validate(result):
245                     raise ConfigError('invalid calculated value returned'
246                         ' for option {0}'.format(name))
247                 if opt_or_descr.is_multi():
248                     if value == []:
249                         _result = Multi([result], value.config, value.child)
250                     else:
251                         _result = Multi([], value.config, value.child)
252                         for val in value:
253                             if val == None:
254                                 val = result
255                             _result.append(val)
256                 else:
257                     _result = result
258                 return _result
259
260             # mandatory options
261             homeconfig = self._cfgimpl_get_toplevel()
262             mandatory = homeconfig._cfgimpl_mandatory
263             if opt_or_descr.is_mandatory() and mandatory:
264                 if self._is_empty(opt_or_descr) and \
265                         opt_or_descr.is_empty_by_default():
266                     raise MandatoryError("option: {0} is mandatory "
267                                           "and shall have a value".format(name))
268             # frozen and force default
269             if opt_or_descr.is_forced_on_freeze():
270                 return opt_or_descr.getdefault()
271
272         return self._cfgimpl_values[name]
273
274     def unwrap_from_name(self, name):
275         """convenience method to extract and Option() object from the Config()
276         **and it is slow**: it recursively searches into the namespaces
277
278         :returns: Option()
279         """
280         paths = self.getpaths(allpaths=True)
281         opts = dict([(path, self.unwrap_from_path(path)) for path in paths])
282         all_paths = [p.split(".") for p in self.getpaths()]
283         for pth in all_paths:
284             if name in pth:
285                 return opts[".".join(pth)]
286         raise NotFoundError("name: {0} not found".format(name))
287
288     def unwrap_from_path(self, path):
289         """convenience method to extract and Option() object from the Config()
290         and it is **fast**: finds the option directly in the appropriate
291         namespace
292
293         :returns: Option()
294         """
295         if '.' in path:
296             homeconfig, path = self._cfgimpl_get_home_by_path(path)
297             return getattr(homeconfig._cfgimpl_descr, path)
298         return getattr(self._cfgimpl_descr, path)
299
300     def __delattr__(self, name):
301         "if you use delattr you are responsible for all bad things happening"
302         if name.startswith('_cfgimpl_'):
303             del self.__dict__[name]
304             return
305         self._cfgimpl_value_owners[name] = 'default'
306         opt = getattr(self._cfgimpl_descr, name)
307         if isinstance(opt, OptionDescription):
308             raise AttributeError("can't option subgroup")
309         self._cfgimpl_values[name] = getattr(opt, 'default', None)
310
311     def setoption(self, name, value, who=None):
312         """effectively modifies the value of an Option()
313         (typically called by the __setattr__)
314
315         :param who: is an owner's name
316         who is **not necessarily** a owner, because it cannot be a list
317         :type who: string
318         """
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 child.has_callback() and who=='default':
339                 raise TypeError("trying to set a value to an option "
340                     "wich has a callback: {0}".format(name))
341             child.setoption(self, value, who)
342             if (value is None and who != 'default' and not child.is_multi()):
343                 child.setowner(self, 'default')
344                 self._cfgimpl_values[name] = copy(child.getdefault())
345             elif (value == [] and who != 'default' and child.is_multi()):
346                 child.setowner(self, ['default' for i in range(len(child.getdefault()))])
347                 self._cfgimpl_values[name] = Multi(copy(child.getdefault()),
348                             config=self, child=child)
349             else:
350                 child.setowner(self, newowner)
351         else:
352             homeconfig = self._cfgimpl_get_toplevel()
353             child.setoption(homeconfig, value, who)
354
355     def set(self, **kwargs):
356         """
357         "do what I mean"-interface to option setting. Searches all paths
358         starting from that config for matches of the optional arguments
359         and sets the found option if the match is not ambiguous.
360         :param kwargs: dict of name strings to values.
361         """
362
363         all_paths = [p.split(".") for p in self.getpaths(allpaths=True)]
364         for key, value in kwargs.iteritems():
365             key_p = key.split('.')
366             candidates = [p for p in all_paths if p[-len(key_p):] == key_p]
367             if len(candidates) == 1:
368                 name = '.'.join(candidates[0])
369                 homeconfig, name = self._cfgimpl_get_home_by_path(name)
370                 try:
371                     getattr(homeconfig, name)
372                 except MandatoryError:
373                     pass
374                 except Exception, e:
375                     raise e # HiddenOptionError or DisabledOptionError
376                 homeconfig.setoption(name, value, self._cfgimpl_owner)
377             elif len(candidates) > 1:
378                 raise AmbigousOptionError(
379                     'more than one option that ends with %s' % (key, ))
380             else:
381                 raise NoMatchingOptionFound(
382                     'there is no option that matches %s'
383                     ' or the option is hidden or disabled'% (key, ))
384
385     def get(self, name):
386         """
387         much like the attribute access way, except that
388         the search for the option is performed recursively in the whole
389         configuration tree.
390         **carefull**: very slow !
391         """
392         paths = self.getpaths(allpaths=True)
393         pathsvalues = []
394         for path in paths:
395             pathname = path.split('.')[-1]
396             if pathname == name:
397                 try:
398                     value = getattr(self, path)
399                     return value
400                 except Exception, e:
401                     raise e
402         raise NotFoundError("option {0} not found in config".format(name))
403
404     def _cfgimpl_get_home_by_path(self, path):
405         """:returns: tuple (config, name)"""
406         path = path.split('.')
407         for step in path[:-1]:
408             self = getattr(self, step)
409         return self, path[-1]
410
411     def _cfgimpl_get_toplevel(self):
412         ":returns: root config"
413         while self._cfgimpl_parent is not None:
414             self = self._cfgimpl_parent
415         return self
416
417     def _cfgimpl_get_path(self):
418         "the path in the attribute access meaning."
419         subpath = []
420         obj = self
421         while obj._cfgimpl_parent is not None:
422             subpath.insert(0, obj._cfgimpl_descr._name)
423             obj = obj._cfgimpl_parent
424         return ".".join(subpath)
425     # ______________________________________________________________________
426     def cfgimpl_previous_value(self, path):
427         "stores the previous value"
428         home, name = self._cfgimpl_get_home_by_path(path)
429         return home._cfgimpl_previous_values[name]
430
431     def get_previous_value(self, name):
432         "for the time being, only the previous Option's value is accessible"
433         return self._cfgimpl_previous_values[name]
434     # ______________________________________________________________________
435     def add_warning(self, warning):
436         "Config implements its own warning pile. Could be useful"
437         self._cfgimpl_get_toplevel()._cfgimpl_warnings.append(warning)
438
439     def get_warnings(self):
440         "Config implements its own warning pile"
441         return self._cfgimpl_get_toplevel()._cfgimpl_warnings
442     # ____________________________________________________________
443     # Config()'s status
444     def cfgimpl_freeze(self):
445         "cannot modify the frozen `Option`'s"
446         rootconfig = self._cfgimpl_get_toplevel()
447         rootconfig._cfgimpl_frozen = True
448         self._cfgimpl_frozen = True
449
450     def cfgimpl_unfreeze(self):
451         "can modify the Options that are frozen"
452         rootconfig = self._cfgimpl_get_toplevel()
453         rootconfig._cfgimpl_frozen = False
454         self._cfgimpl_frozen = False
455
456     def is_frozen(self):
457         "freeze flag at Config level"
458         rootconfig = self._cfgimpl_get_toplevel()
459         return rootconfig._cfgimpl_frozen
460
461     def cfgimpl_read_only(self):
462         "convenience method to freeze, hidde and disable"
463         self.cfgimpl_freeze()
464         rootconfig = self._cfgimpl_get_toplevel()
465         rootconfig.cfgimpl_disable_property('hidden')
466         rootconfig.cfgimpl_enable_property('disabled')
467         rootconfig._cfgimpl_mandatory = True
468
469     def cfgimpl_read_write(self):
470         "convenience method to freeze, hidde and disable"
471         self.cfgimpl_freeze()
472         rootconfig = self._cfgimpl_get_toplevel()
473         rootconfig.cfgimpl_enable_property('hidden')
474         rootconfig.cfgimpl_enable_property('disabled')
475         rootconfig._cfgimpl_mandatory = False
476
477     def cfgimpl_non_mandatory(self):
478         """mandatory at the Config level means that the Config raises an error
479         if a mandatory option is found"""
480         if self._cfgimpl_parent != None:
481             raise MethodCallError("this method root_mandatory shall"
482                                   " not be used with non-root Confit() object")
483         rootconfig = self._cfgimpl_get_toplevel()
484         rootconfig._cfgimpl_mandatory = False
485
486     def cfgimpl_mandatory(self):
487         """mandatory at the Config level means that the Config raises an error
488         if a mandatory option is found"""
489         if self._cfgimpl_parent != None:
490             raise MethodCallError("this method root_mandatory shall"
491                                   " not be used with non-root Confit() object")
492         rootconfig = self._cfgimpl_get_toplevel()
493         rootconfig._cfgimpl_mandatory = True
494
495     def is_mandatory(self):
496         "all mandatory Options shall have a value"
497         rootconfig = self._cfgimpl_get_toplevel()
498         return rootconfig._cfgimpl_mandatory
499     # ____________________________________________________________
500     def getkey(self):
501         return self._cfgimpl_descr.getkey(self)
502
503     def __hash__(self):
504         return hash(self.getkey())
505
506     def __eq__(self, other):
507         "Config comparison"
508         return self.getkey() == other.getkey()
509
510     def __ne__(self, other):
511         "Config comparison"
512         return not self == other
513     # ______________________________________________________________________
514     def __iter__(self):
515         "iteration only on Options (not OptionDescriptions)"
516         for child in self._cfgimpl_descr._children:
517             if isinstance(child, Option):
518                 try:
519                     yield child._name, getattr(self, child._name)
520                 except:
521                     pass # option with properties
522
523     def iter_groups(self, group_type=None):
524         "iteration on OptionDescriptions"
525         if group_type == None:
526             groups = group_types
527         else:
528             if group_type not in group_types:
529                 raise TypeError("Unknown group_type: {0}".format(group_type))
530             groups = [group_type]
531         for child in self._cfgimpl_descr._children:
532             if isinstance(child, OptionDescription):
533                     try:
534                         if child.get_group_type() in groups:
535                             yield child._name, getattr(self, child._name)
536                     except:
537                         pass # hidden, disabled option
538     # ______________________________________________________________________
539     def __str__(self, indent=""):
540         "Config's string representation"
541         lines = []
542         children = [(child._name, child)
543                     for child in self._cfgimpl_descr._children]
544         children.sort()
545         for name, child in children:
546             if self._cfgimpl_value_owners.get(name, None) == 'default':
547                 continue
548             value = getattr(self, name)
549             if isinstance(value, Config):
550                 substr = value.__str__(indent + "    ")
551             else:
552                 substr = "%s    %s = %s" % (indent, name, value)
553             if substr:
554                 lines.append(substr)
555         if indent and not lines:
556             return ''   # hide subgroups with all default values
557         lines.insert(0, "%s[%s]" % (indent, self._cfgimpl_descr._name,))
558         return '\n'.join(lines)
559
560     def getpaths(self, include_groups=False, allpaths=False, mandatory=False):
561         """returns a list of all paths in self, recursively, taking care of
562         the context of properties (hidden/disabled)
563         """
564         paths = []
565         for path in self._cfgimpl_descr.getpaths(include_groups=include_groups):
566             try:
567                 value = getattr(self, path)
568
569             except MandatoryError:
570                 if mandatory or allpaths:
571                     paths.append(path)
572             except PropertiesOptionError:
573                 if allpaths:
574                     paths.append(path) # option which have properties added
575             else:
576                  paths.append(path)
577         return paths
578
579 def make_dict(config, flatten=False):
580     """export the whole config into a `dict`
581     :returns: dict of Option's name (or path) and values"""
582     paths = config.getpaths()
583     pathsvalues = []
584     for path in paths:
585         if flatten:
586             pathname = path.split('.')[-1]
587         else:
588             pathname = path
589         try:
590             value = getattr(config, path)
591             pathsvalues.append((pathname, value))
592         except:
593             pass # this just a hidden or disabled option
594     options = dict(pathsvalues)
595     return options
596
597 def mandatory_warnings(config):
598     """convenience function to trace Options that are mandatory and
599     where no value has been set
600
601     :returns: generator of mandatory Option's path
602     """
603     mandatory = config._cfgimpl_get_toplevel()._cfgimpl_mandatory
604     config._cfgimpl_get_toplevel()._cfgimpl_mandatory = True
605     for path in config._cfgimpl_descr.getpaths(include_groups=True):
606         try:
607             value = getattr(config, path)
608         except MandatoryError:
609             yield path
610         except PropertiesOptionError:
611             pass
612     config._cfgimpl_get_toplevel()._cfgimpl_mandatory = mandatory