0b50a6c1daf26d07066505106522d49e9656c014
[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, NoValueReturned)
27 from tiramisu.option import (OptionDescription, Option, SymLinkOption,
28     group_types, Multi, apply_requires)
29 from tiramisu.setting import settings
30
31 # ____________________________________________________________
32 class Config(object):
33     "main configuration management entry"
34     _cfgimpl_toplevel = None
35
36     def __init__(self, descr, parent=None):
37         """ Configuration option management master class
38         :param descr: describes the configuration schema
39         :type descr: an instance of ``option.OptionDescription``
40         :param parent: is None if the ``Config`` is root parent Config otherwise
41         :type parent: ``Config``
42         """
43         self._cfgimpl_descr = descr
44         self._cfgimpl_value_owners = {}
45         self._cfgimpl_parent = parent
46         "`Config()` indeed is in charge of the `Option()`'s values"
47         self._cfgimpl_values = {}
48         self._cfgimpl_previous_values = {}
49         "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_build()
54
55     def _validate_duplicates(self, children):
56         """duplicates Option names in the schema
57         :type children: list of `Option` or `OptionDescription`
58         """
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):
68         """
69         - builds the config object from the schema
70         - settles various default values for options
71         """
72         self._validate_duplicates(self._cfgimpl_descr._children)
73         for child in self._cfgimpl_descr._children:
74             if isinstance(child, Option):
75                 if child.is_multi():
76                     childdef = Multi(copy(child.getdefault()), config=self,
77                                      child=child)
78                     self._cfgimpl_values[child._name] = childdef
79                     self._cfgimpl_previous_values[child._name] = list(childdef)
80                 else:
81                     childdef = child.getdefault()
82                     self._cfgimpl_values[child._name] = childdef
83                     self._cfgimpl_previous_values[child._name] = childdef
84                 child.setowner(self, 'default')
85             elif isinstance(child, OptionDescription):
86                 self._validate_duplicates(child._children)
87                 self._cfgimpl_values[child._name] = Config(child, parent=self)
88 #        self.override(overrides)
89
90     def cfgimpl_update(self):
91         """dynamically adds `Option()` or `OptionDescription()`
92         """
93         # FIXME this is an update for new options in the schema only
94         #┬ásee the update_child() method of the descr object
95         for child in self._cfgimpl_descr._children:
96             if isinstance(child, Option):
97                 if child._name not in self._cfgimpl_values:
98                     if child.is_multi():
99                         self._cfgimpl_values[child._name] = Multi(
100                                 copy(child.getdefault()), config=self, child=child)
101                     else:
102                         self._cfgimpl_values[child._name] = copy(child.getdefault())
103                     child.setowner(self, 'default')
104             elif isinstance(child, OptionDescription):
105                 if child._name not in self._cfgimpl_values:
106                     self._cfgimpl_values[child._name] = Config(child, parent=self)
107
108     # ____________________________________________________________
109     # attribute methods
110     def __setattr__(self, name, value):
111         "attribute notation mechanism for the setting of the value of an option"
112         if name.startswith('_cfgimpl_'):
113             self.__dict__[name] = value
114             return
115         if '.' in name:
116             homeconfig, name = self._cfgimpl_get_home_by_path(name)
117             return setattr(homeconfig, name, value)
118         if type(getattr(self._cfgimpl_descr, name)) != SymLinkOption:
119             self._validate(name, getattr(self._cfgimpl_descr, name))
120         self.setoption(name, value, settings.owner)
121
122     def _validate(self, name, opt_or_descr, permissive=False):
123         "validation for the setattr and the getattr"
124         apply_requires(opt_or_descr, self)
125         if not isinstance(opt_or_descr, Option) and \
126                 not isinstance(opt_or_descr, OptionDescription):
127             raise TypeError('Unexpected object: {0}'.format(repr(opt_or_descr)))
128         properties = copy(opt_or_descr.properties)
129         for proper in copy(properties):
130             if not settings.has_property(proper):
131                 properties.remove(proper)
132         if permissive:
133             for perm in settings.permissive:
134                 if perm in properties:
135                     properties.remove(perm)
136         if properties != []:
137             raise PropertiesOptionError("trying to access"
138                     " to an option named: {0} with properties"
139                     " {1}".format(name, str(properties)),
140                     properties)
141
142     def _is_empty(self, opt):
143         "convenience method to know if an option is empty"
144         if (not opt.is_multi() and self._cfgimpl_values[opt._name] == None) or \
145             (opt.is_multi() and (self._cfgimpl_values[opt._name] == [] or \
146                 None in self._cfgimpl_values[opt._name])):
147             return True
148         return False
149
150     def _test_mandatory(self, path, opt):
151         # mandatory options
152         mandatory = settings.mandatory
153         if opt.is_mandatory() and mandatory:
154             if self._is_empty(opt) and \
155                     opt.is_empty_by_default():
156                 raise MandatoryError("option: {0} is mandatory "
157                                       "and shall have a value".format(path))
158
159     def __getattr__(self, name):
160         return self._getattr(name)
161
162     def _getattr(self, name, permissive=False):
163         """
164         attribute notation mechanism for accessing the value of an option
165         :param name: attribute name
166         :param permissive: permissive doesn't raise some property error
167                           (see ``settings.permissive``)
168         :return: option's value if name is an option name, OptionDescription
169                  otherwise
170         """
171         # attribute access by passing a path,
172         # for instance getattr(self, "creole.general.family.adresse_ip_eth0")
173         if '.' in name:
174             homeconfig, name = self._cfgimpl_get_home_by_path(name)
175             return homeconfig._getattr(name, permissive)
176         opt_or_descr = getattr(self._cfgimpl_descr, name)
177         # symlink options
178         if type(opt_or_descr) == SymLinkOption:
179             return getattr(self, opt_or_descr.path)
180         if name not in self._cfgimpl_values:
181             raise AttributeError("%s object has no attribute %s" %
182                                  (self.__class__, name))
183         self._validate(name, opt_or_descr, permissive)
184         # special attributes
185         if name.startswith('_cfgimpl_'):
186             # if it were in __dict__ it would have been found already
187             return self.__dict__[name]
188             raise AttributeError("%s object has no attribute %s" %
189                                  (self.__class__, name))
190         if not isinstance(opt_or_descr, OptionDescription):
191             # options with callbacks
192             if opt_or_descr.has_callback():
193                 value = self._cfgimpl_values[name]
194                 if (not opt_or_descr.is_frozen() or \
195                         not opt_or_descr.is_forced_on_freeze()) and \
196                         not opt_or_descr.is_default_owner(self):
197                     return value
198                 try:
199                     result = opt_or_descr.getcallback_value(
200                             self._cfgimpl_get_toplevel())
201                 except NoValueReturned, err:
202                     pass
203                 else:
204                     if opt_or_descr.is_multi():
205                         if not isinstance(result, list):
206                             result = [result]
207                         _result = Multi(result, value.config, value.child)
208                     else:
209                         # this result **shall not** be a list
210                         if isinstance(result, list):
211                             raise ConfigError('invalid calculated value returned'
212                                 ' for option {0} : shall not be a list'.format(name))
213                         _result = result
214                     if _result != None and not opt_or_descr.validate(_result,
215                                 settings.validator):
216                         raise ConfigError('invalid calculated value returned'
217                             ' for option {0}'.format(name))
218                     self._cfgimpl_values[name] = _result
219                     opt_or_descr.setowner(self, 'default')
220             if not opt_or_descr.has_callback() and opt_or_descr.is_forced_on_freeze():
221                 return opt_or_descr.getdefault()
222             self._test_mandatory(name, opt_or_descr)
223             # frozen and force default
224         return self._cfgimpl_values[name]
225
226     def unwrap_from_name(self, name):
227         """convenience method to extract and Option() object from the Config()
228         **and it is slow**: it recursively searches into the namespaces
229
230         :returns: Option()
231         """
232         paths = self.getpaths(allpaths=True)
233         opts = dict([(path, self.unwrap_from_path(path)) for path in paths])
234         all_paths = [p.split(".") for p in self.getpaths()]
235         for pth in all_paths:
236             if name in pth:
237                 return opts[".".join(pth)]
238         raise NotFoundError("name: {0} not found".format(name))
239
240     def unwrap_from_path(self, path):
241         """convenience method to extract and Option() object from the Config()
242         and it is **fast**: finds the option directly in the appropriate
243         namespace
244
245         :returns: Option()
246         """
247         if '.' in path:
248             homeconfig, path = self._cfgimpl_get_home_by_path(path)
249             return getattr(homeconfig._cfgimpl_descr, path)
250         return getattr(self._cfgimpl_descr, path)
251
252     def setoption(self, name, value, who=None):
253         """effectively modifies the value of an Option()
254         (typically called by the __setattr__)
255
256         :param who: is an owner's name
257                     who is **not necessarily** a owner, because it cannot be a list
258         :type who: string
259         """
260         child = getattr(self._cfgimpl_descr, name)
261         if type(child) != SymLinkOption:
262             if who == None:
263                 who = settings.owner
264             if child.is_multi():
265                 if type(value) != Multi:
266                     if type(value) == list:
267                         value = Multi(value, self, child)
268                     else:
269                         raise ConfigError("invalid value for option:"
270                                    " {0} that is set to multi".format(name))
271             child.setoption(self, value, who)
272             child.setowner(self, who)
273         else:
274             homeconfig = self._cfgimpl_get_toplevel()
275             child.setoption(homeconfig, value, who)
276
277     def set(self, **kwargs):
278         """
279         do what I mean"-interface to option setting. Searches all paths
280         starting from that config for matches of the optional arguments
281         and sets the found option if the match is not ambiguous.
282         :param kwargs: dict of name strings to values.
283         """
284         all_paths = [p.split(".") for p in self.getpaths(allpaths=True)]
285         for key, value in kwargs.iteritems():
286             key_p = key.split('.')
287             candidates = [p for p in all_paths if p[-len(key_p):] == key_p]
288             if len(candidates) == 1:
289                 name = '.'.join(candidates[0])
290                 homeconfig, name = self._cfgimpl_get_home_by_path(name)
291                 try:
292                     getattr(homeconfig, name)
293                 except MandatoryError:
294                     pass
295                 except Exception, e:
296                     raise e # HiddenOptionError or DisabledOptionError
297                 homeconfig.setoption(name, value, settings.owner)
298             elif len(candidates) > 1:
299                 raise AmbigousOptionError(
300                     'more than one option that ends with %s' % (key, ))
301             else:
302                 raise NoMatchingOptionFound(
303                     'there is no option that matches %s'
304                     ' or the option is hidden or disabled'% (key, ))
305
306     def get(self, name):
307         """
308         same as a `find_first()` method in a config that has identical names:
309         it returns the first item of an option named `name`
310
311         much like the attribute access way, except that
312         the search for the option is performed recursively in the whole
313         configuration tree.
314         **carefull**: very slow !
315
316         :returns: option value.
317         """
318         paths = self.getpaths(allpaths=True)
319         pathsvalues = []
320         for path in paths:
321             pathname = path.split('.')[-1]
322             if pathname == name:
323                 try:
324                     value = getattr(self, path)
325                     return value
326                 except Exception, e:
327                     raise e
328         raise NotFoundError("option {0} not found in config".format(name))
329
330     def _cfgimpl_get_home_by_path(self, path):
331         """:returns: tuple (config, name)"""
332         path = path.split('.')
333         for step in path[:-1]:
334             self = getattr(self, step)
335         return self, path[-1]
336
337     def _cfgimpl_get_toplevel(self):
338         ":returns: root config"
339         while self._cfgimpl_parent is not None:
340             self = self._cfgimpl_parent
341         return self
342
343     def _cfgimpl_get_path(self):
344         "the path in the attribute access meaning."
345         subpath = []
346         obj = self
347         while obj._cfgimpl_parent is not None:
348             subpath.insert(0, obj._cfgimpl_descr._name)
349             obj = obj._cfgimpl_parent
350         return ".".join(subpath)
351     # ______________________________________________________________________
352     def cfgimpl_previous_value(self, path):
353         "stores the previous value"
354         home, name = self._cfgimpl_get_home_by_path(path)
355         return home._cfgimpl_previous_values[name]
356
357     def get_previous_value(self, name):
358         "for the time being, only the previous Option's value is accessible"
359         return self._cfgimpl_previous_values[name]
360     # ______________________________________________________________________
361     def add_warning(self, warning):
362         "Config implements its own warning pile. Could be useful"
363         self._cfgimpl_get_toplevel()._cfgimpl_warnings.append(warning)
364
365     def get_warnings(self):
366         "Config implements its own warning pile"
367         return self._cfgimpl_get_toplevel()._cfgimpl_warnings
368     # ____________________________________________________________
369     def getkey(self):
370         return self._cfgimpl_descr.getkey(self)
371
372     def __hash__(self):
373         return hash(self.getkey())
374
375     def __eq__(self, other):
376         "Config comparison"
377         return self.getkey() == other.getkey()
378
379     def __ne__(self, other):
380         "Config comparison"
381         return not self == other
382     # ______________________________________________________________________
383     def __iter__(self):
384         """Pythonesque way of parsing group's ordered options.
385         iteration only on Options (not OptionDescriptions)"""
386         for child in self._cfgimpl_descr._children:
387             if isinstance(child, Option):
388                 try:
389                     yield child._name, getattr(self, child._name)
390                 except:
391                     pass # option with properties
392
393     def iter_groups(self, group_type=None):
394         """iteration on groups objects only.
395         All groups are returned if `group_type` is `None`, otherwise the groups
396         can be filtered by categories (families, or whatever).
397         """
398         if group_type == None:
399             groups = group_types
400         else:
401             if group_type not in group_types:
402                 raise TypeError("Unknown group_type: {0}".format(group_type))
403             groups = [group_type]
404         for child in self._cfgimpl_descr._children:
405             if isinstance(child, OptionDescription):
406                 try:
407                     if child.get_group_type() in groups:
408                         yield child._name, getattr(self, child._name)
409                 except:
410                     pass # hidden, disabled option
411     # ______________________________________________________________________
412     def __str__(self, indent=""):
413         "Config's string representation"
414         lines = []
415         children = [(child._name, child)
416                     for child in self._cfgimpl_descr._children]
417         children.sort()
418         for name, child in children:
419             if self._cfgimpl_value_owners.get(name, None) == 'default':
420                 continue
421             value = getattr(self, name)
422             if isinstance(value, Config):
423                 substr = value.__str__(indent + "    ")
424             else:
425                 substr = "%s    %s = %s" % (indent, name, value)
426             if substr:
427                 lines.append(substr)
428         if indent and not lines:
429             return ''   # hide subgroups with all default values
430         lines.insert(0, "%s[%s]" % (indent, self._cfgimpl_descr._name,))
431         return '\n'.join(lines)
432
433     def getpaths(self, include_groups=False, allpaths=False, mandatory=False):
434         """returns a list of all paths in self, recursively, taking care of
435         the context of properties (hidden/disabled)
436
437         :param include_groups: if true, OptionDescription are included
438         :param allpaths: all the options (event the properties protected ones)
439         :param mandatory: includes the mandatory options
440         :returns: list of all paths
441         """
442         paths = []
443         for path in self._cfgimpl_descr.getpaths(include_groups=include_groups):
444             try:
445                 value = getattr(self, path)
446
447             except MandatoryError:
448                 if mandatory or allpaths:
449                     paths.append(path)
450             except PropertiesOptionError:
451                 if allpaths:
452                     paths.append(path) # option which have properties added
453             else:
454                  paths.append(path)
455         return paths
456
457     def _find(self, bytype, byname, byvalue, byattrs, first):
458         """
459             :param first: return only one option if True, a list otherwise
460         """
461         def _filter_by_attrs():
462             if byattrs is None:
463                 return True
464             for key, value in byattrs.items():
465                 if not hasattr(option, key):
466                     return False
467                 else:
468                     if getattr(option, key) != value:
469                         return False
470                     else:
471                         continue
472             return True
473         def _filter_by_name():
474             if byname is None:
475                 return True
476             pathname = path.split('.')[-1]
477             if pathname == byname:
478                 return True
479             else:
480                 return False
481         def _filter_by_value():
482             if byvalue is None:
483                 return True
484             try:
485                 value = getattr(self, path)
486                 if value == byvalue:
487                     return True
488             except Exception, e: # a property restricts the acces to value
489                 pass
490             return False
491         def _filter_by_type():
492             if bytype is None:
493                 return True
494             if isinstance(option, bytype):
495                 return True
496             return False
497
498         find_results = []
499         paths = self.getpaths(allpaths=True)
500         for path in paths:
501             option = self.unwrap_from_path(path)
502             if not _filter_by_name():
503                 continue
504             if not _filter_by_value():
505                 continue
506             if not _filter_by_type():
507                 continue
508             if not _filter_by_attrs():
509                 continue
510             if first:
511                 return option
512             else:
513                 find_results.append(option)
514         if first:
515             return None
516         else:
517             return find_results
518
519     def find(self, bytype=None, byname=None, byvalue=None, byattrs=None):
520         """
521             finds a list of options recursively in the config
522
523             :param bytype: Option class (BoolOption, StrOption, ...)
524             :param byname: filter by Option._name
525             :param byvalue: filter by the option's value
526             :param byattrs: dict of option attributes (default, callback...)
527             :returns: list of matching Option objects
528         """
529         return self._find(bytype, byname, byvalue, byattrs, first=False)
530
531     def find_first(self, bytype=None, byname=None, byvalue=None, byattrs=None):
532         """
533             finds an option recursively in the config
534
535             :param bytype: Option class (BoolOption, StrOption, ...)
536             :param byname: filter by Option._name
537             :param byvalue: filter by the option's value
538             :param byattrs: dict of option attributes (default, callback...)
539             :returns: list of matching Option objects
540         """
541         return self._find(bytype, byname, byvalue, byattrs, first=True)
542
543 def make_dict(config, flatten=False):
544     """export the whole config into a `dict`
545     :returns: dict of Option's name (or path) and values"""
546     paths = config.getpaths()
547     pathsvalues = []
548     for path in paths:
549         if flatten:
550             pathname = path.split('.')[-1]
551         else:
552             pathname = path
553         try:
554             value = getattr(config, path)
555             pathsvalues.append((pathname, value))
556         except:
557             pass # this just a hidden or disabled option
558     options = dict(pathsvalues)
559     return options
560
561 def mandatory_warnings(config):
562     """convenience function to trace Options that are mandatory and
563     where no value has been set
564
565     :returns: generator of mandatory Option's path
566     """
567     mandatory = settings.mandatory
568     settings.mandatory = True
569     for path in config._cfgimpl_descr.getpaths(include_groups=True):
570         try:
571             value = config._getattr(path, permissive=True)
572         except MandatoryError:
573             yield path
574         except PropertiesOptionError:
575             pass
576     settings.mandatory = mandatory