review exception
[tiramisu.git] / tiramisu / config.py
1 # -*- coding: utf-8 -*-
2 "pretty small and local configuration management tool"
3 # Copyright (C) 2012-2013 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 inspect import getmembers, ismethod
24 from tiramisu.error import (PropertiesOptionError, ConfigError,
25                             AmbigousOptionError, MandatoryError)
26 from tiramisu.option import OptionDescription, Option, SymLinkOption
27 from tiramisu.setting import groups, Setting, apply_requires
28 from tiramisu.value import Values
29 from tiramisu.i18n import _
30
31
32 class SubConfig(object):
33     "sub configuration management entry"
34     __slots__ = ('_cfgimpl_descr', '_cfgimpl_context')
35
36     def __init__(self, descr, context):
37         """ Configuration option management master class
38
39         :param descr: describes the configuration schema
40         :type descr: an instance of ``option.OptionDescription``
41         :param context: the current root config
42         :type context: `Config`
43         """
44         # main option description
45         self._cfgimpl_descr = descr
46         # sub option descriptions
47         self._cfgimpl_context = context
48
49     def cfgimpl_get_context(self):
50         return self._cfgimpl_context
51
52     def cfgimpl_get_settings(self):
53         return self._cfgimpl_context._cfgimpl_settings
54
55     def cfgimpl_get_values(self):
56         return self._cfgimpl_context._cfgimpl_values
57
58     def cfgimpl_get_consistancies(self):
59         return self.cfgimpl_get_context().cfgimpl_get_description()._consistancies
60
61     def cfgimpl_get_description(self):
62         return self._cfgimpl_descr
63
64     # ____________________________________________________________
65     # attribute methods
66     def __setattr__(self, name, value):
67         "attribute notation mechanism for the setting of the value of an option"
68         if name.startswith('_cfgimpl_'):
69             #self.__dict__[name] = value
70             object.__setattr__(self, name, value)
71             return
72         self._setattr(name, value)
73
74     def _setattr(self, name, value, force_permissive=False):
75         if '.' in name:
76             homeconfig, name = self.cfgimpl_get_home_by_path(name)
77             return homeconfig.__setattr__(name, value)
78         child = getattr(self._cfgimpl_descr, name)
79         if type(child) != SymLinkOption:
80             self._validate(name, getattr(self._cfgimpl_descr, name), force_permissive=force_permissive)
81             self.setoption(name, child, value)
82         else:
83             child.setoption(self.cfgimpl_get_context(), value)
84
85     def _validate(self, name, opt_or_descr, force_permissive=False):
86         "validation for the setattr and the getattr"
87         if not isinstance(opt_or_descr, Option) and \
88                 not isinstance(opt_or_descr, OptionDescription):
89             raise TypeError(_('unexpected object: {0}').format(repr(opt_or_descr)))
90         properties = set(self.cfgimpl_get_settings().get_properties(opt_or_descr))
91         #remove this properties, those properties are validate in value/setting
92         properties = properties - set(['mandatory', 'frozen'])
93         set_properties = set(self.cfgimpl_get_settings().get_properties())
94         properties = properties & set_properties
95         if force_permissive is True or self.cfgimpl_get_settings().has_property('permissive', is_apply_req=False):
96             properties = properties - set(self.cfgimpl_get_settings().get_permissive())
97         properties = properties - set(self.cfgimpl_get_settings().get_permissive(opt_or_descr))
98         properties = list(properties)
99         if properties != []:
100             raise PropertiesOptionError(_("trying to access"
101                                         " to an option named: {0} with properties"
102                                         " {1}").format(name, str(properties)),
103                                         properties)
104
105     def __getattr__(self, name):
106         return self._getattr(name)
107
108     def _getattr(self, name, force_permissive=False, force_properties=None,
109                  validate=True):
110         """
111         attribute notation mechanism for accessing the value of an option
112         :param name: attribute name
113         :return: option's value if name is an option name, OptionDescription
114                  otherwise
115         """
116         # attribute access by passing a path,
117         # for instance getattr(self, "creole.general.family.adresse_ip_eth0")
118         if '.' in name:
119             homeconfig, name = self.cfgimpl_get_home_by_path(name,
120                                                              force_permissive=force_permissive,
121                                                              force_properties=force_properties)
122             return homeconfig._getattr(name, force_permissive=force_permissive,
123                                        force_properties=force_properties,
124                                        validate=validate)
125         opt_or_descr = getattr(self._cfgimpl_descr, name)
126         # symlink options
127         if type(opt_or_descr) == SymLinkOption:
128             rootconfig = self.cfgimpl_get_context()
129             path = rootconfig.cfgimpl_get_description().get_path_by_opt(opt_or_descr.opt)
130             return rootconfig._getattr(path, validate=validate)
131         self._validate(name, opt_or_descr, force_permissive=force_permissive)
132         if isinstance(opt_or_descr, OptionDescription):
133             children = self.cfgimpl_get_description()._children
134             if opt_or_descr not in children[1]:
135                 raise AttributeError(_("{0} with name {1} object has "
136                                      "no attribute {2}").format(self.__class__,
137                                                                 opt_or_descr._name,
138                                                                 name))
139             return SubConfig(opt_or_descr, self._cfgimpl_context)
140         # special attributes
141         if name.startswith('_cfgimpl_'):
142             # if it were in __dict__ it would have been found already
143             object.__getattr__(self, name)
144         return self.cfgimpl_get_values()._getitem(opt_or_descr,
145                                                   force_properties=force_properties,
146                                                   validate=validate)
147
148     def setoption(self, name, child, value):
149         """effectively modifies the value of an Option()
150         (typically called by the __setattr__)
151         """
152         setting = self.cfgimpl_get_settings()
153         #needed ?
154         apply_requires(child, self)
155         #needed to ?
156         if child not in self._cfgimpl_descr._children[1]:
157             raise AttributeError(_('unknown option {0}').format(name))
158
159         if setting.has_property('everything_frozen'):
160             raise ConfigError(_("cannot set a value to the option {0} if the whole "
161                               "config has been frozen").format(name))
162
163         if setting.has_property('frozen') and setting.has_property('frozen',
164                                                                    child, is_apply_req=False):
165             raise ConfigError(_('cannot change the value to {0} for '
166                               'option {1} this option is frozen').format(str(value), name))
167         self.cfgimpl_get_values()[child] = value
168
169     def cfgimpl_get_home_by_path(self, path, force_permissive=False, force_properties=None):
170         """:returns: tuple (config, name)"""
171         path = path.split('.')
172         for step in path[:-1]:
173             self = self._getattr(step,
174                                  force_permissive=force_permissive,
175                                  force_properties=force_properties)
176         return self, path[-1]
177
178     def getkey(self):
179         return self._cfgimpl_descr.getkey(self)
180
181     def __hash__(self):
182         return hash(self.getkey())
183
184     def __eq__(self, other):
185         "Config comparison"
186         if not isinstance(other, Config):
187             return False
188         return self.getkey() == other.getkey()
189
190     def __ne__(self, other):
191         "Config comparison"
192         return not self == other
193
194     # ______________________________________________________________________
195     def __iter__(self):
196         """Pythonesque way of parsing group's ordered options.
197         iteration only on Options (not OptionDescriptions)"""
198         for child in self._cfgimpl_descr._children[1]:
199             if not isinstance(child, OptionDescription):
200                 try:
201                     yield child._name, getattr(self, child._name)
202                 except GeneratorExit:
203                     raise StopIteration
204                 except:
205                     pass  # option with properties
206
207     def iter_all(self):
208         """A way of parsing options **and** groups.
209         iteration on Options and OptionDescriptions."""
210         for child in self._cfgimpl_descr._children[1]:
211             try:
212                 yield child._name, getattr(self, child._name)
213             except GeneratorExit:
214                 raise StopIteration
215             except:
216                 pass  # option with properties
217
218     def iter_groups(self, group_type=None):
219         """iteration on groups objects only.
220         All groups are returned if `group_type` is `None`, otherwise the groups
221         can be filtered by categories (families, or whatever).
222
223         :param group_type: if defined, is an instance of `groups.GroupType`
224                            or `groups.MasterGroupType` that lives in
225                            `setting.groups`
226
227         """
228         if group_type is not None:
229             if not isinstance(group_type, groups.GroupType):
230                 raise TypeError(_("unknown group_type: {0}").format(group_type))
231         for child in self._cfgimpl_descr._children[1]:
232             if isinstance(child, OptionDescription):
233                 try:
234                     if group_type is not None:
235                         if child.get_group_type() == group_type:
236                             yield child._name, getattr(self, child._name)
237                     else:
238                         yield child._name, getattr(self, child._name)
239                 except GeneratorExit:
240                     raise StopIteration
241                 except:
242                     pass
243     # ______________________________________________________________________
244
245     def __str__(self):
246         "Config's string representation"
247         lines = []
248         for name, grp in self.iter_groups():
249             lines.append("[%s]" % name)
250         for name, value in self:
251             try:
252                 lines.append("%s = %s" % (name, value))
253             except:
254                 pass
255         return '\n'.join(lines)
256
257     __repr__ = __str__
258
259     def getpaths(self, include_groups=False, allpaths=False, mandatory=False):
260         """returns a list of all paths in self, recursively, taking care of
261         the context of properties (hidden/disabled)
262
263         :param include_groups: if true, OptionDescription are included
264         :param allpaths: all the options (event the properties protected ones)
265         :param mandatory: includes the mandatory options
266         :returns: list of all paths
267         """
268         paths = []
269         for path in self._cfgimpl_descr.getpaths(include_groups=include_groups):
270             if allpaths:
271                 paths.append(path)
272             else:
273                 try:
274                     getattr(self, path)
275                 except MandatoryError:
276                     if mandatory:
277                         paths.append(path)
278                 except PropertiesOptionError:
279                     pass
280                 else:
281                     paths.append(path)
282         return paths
283
284     def getpath(self):
285         descr = self.cfgimpl_get_description()
286         context_descr = self.cfgimpl_get_context().cfgimpl_get_description()
287         return context_descr.get_path_by_opt(descr)
288
289     def find(self, bytype=None, byname=None, byvalue=None, type_='option'):
290         """
291             finds a list of options recursively in the config
292
293             :param bytype: Option class (BoolOption, StrOption, ...)
294             :param byname: filter by Option._name
295             :param byvalue: filter by the option's value
296             :returns: list of matching Option objects
297         """
298         return self.cfgimpl_get_context()._find(bytype, byname, byvalue,
299                                                 first=False,
300                                                 type_=type_,
301                                                 _subpath=self.getpath())
302
303     def find_first(self, bytype=None, byname=None, byvalue=None, type_='option'):
304         """
305             finds an option recursively in the config
306
307             :param bytype: Option class (BoolOption, StrOption, ...)
308             :param byname: filter by Option._name
309             :param byvalue: filter by the option's value
310             :returns: list of matching Option objects
311         """
312         return self.cfgimpl_get_context()._find(bytype, byname, byvalue,
313                                                 first=True,
314                                                 type_=type_,
315                                                 _subpath=self.getpath())
316
317     def make_dict(self, flatten=False, _currpath=None, withoption=None, withvalue=None):
318         """export the whole config into a `dict`
319         :returns: dict of Option's name (or path) and values"""
320         pathsvalues = []
321         if _currpath is None:
322             _currpath = []
323         if withoption is None and withvalue is not None:
324             raise ValueError(_("make_dict can't filtering with value without option"))
325         if withoption is not None:
326             mypath = self.getpath()
327             for path in self.cfgimpl_get_context()._find(bytype=Option,
328                                                          byname=withoption,
329                                                          byvalue=withvalue,
330                                                          first=False,
331                                                          type_='path',
332                                                          _subpath=mypath):
333                 path = '.'.join(path.split('.')[:-1])
334                 opt = self.cfgimpl_get_context().cfgimpl_get_description().get_opt_by_path(path)
335                 if mypath is not None:
336                     if mypath == path:
337                         withoption = None
338                         withvalue = None
339                         break
340                     else:
341                         tmypath = mypath + '.'
342                         if not path.startswith(tmypath):
343                             raise AttributeError(_('unexpected path {0}, '
344                                                  'should start with {1}').format(path, mypath))
345                         path = path[len(tmypath):]
346                 self._make_sub_dict(opt, path, pathsvalues, _currpath, flatten)
347         #withoption can be set to None below !
348         if withoption is None:
349             for opt in self.cfgimpl_get_description().getchildren():
350                 path = opt._name
351                 self._make_sub_dict(opt, path, pathsvalues, _currpath, flatten)
352         if _currpath == []:
353             options = dict(pathsvalues)
354             return options
355         return pathsvalues
356
357     def _make_sub_dict(self, opt, path, pathsvalues, _currpath, flatten):
358         if isinstance(opt, OptionDescription):
359             pathsvalues += getattr(self, path).make_dict(flatten,
360                                                          _currpath + path.split('.'))
361         else:
362             try:
363                 value = self._getattr(opt._name)
364                 if flatten:
365                     name = opt._name
366                 else:
367                     name = '.'.join(_currpath + [opt._name])
368                 pathsvalues.append((name, value))
369             except PropertiesOptionError:
370                 pass  # this just a hidden or disabled option
371
372
373 # ____________________________________________________________
374 class Config(SubConfig):
375     "main configuration management entry"
376     __slots__ = ('_cfgimpl_settings', '_cfgimpl_values')
377
378     def __init__(self, descr):
379         """ Configuration option management master class
380
381         :param descr: describes the configuration schema
382         :type descr: an instance of ``option.OptionDescription``
383         :param context: the current root config
384         :type context: `Config`
385         """
386         self._cfgimpl_settings = Setting(self)
387         self._cfgimpl_values = Values(self)
388         super(Config, self).__init__(descr, self)  # , slots)
389         self._cfgimpl_build_all_paths()
390
391     def _cfgimpl_build_all_paths(self):
392         self._cfgimpl_descr.build_cache()
393
394     def unwrap_from_path(self, path):
395         """convenience method to extract and Option() object from the Config()
396         and it is **fast**: finds the option directly in the appropriate
397         namespace
398
399         :returns: Option()
400         """
401         if '.' in path:
402             homeconfig, path = self.cfgimpl_get_home_by_path(path)
403             return getattr(homeconfig._cfgimpl_descr, path)
404         return getattr(self._cfgimpl_descr, path)
405
406     def set(self, **kwargs):
407         """
408         do what I mean"-interface to option setting. Searches all paths
409         starting from that config for matches of the optional arguments
410         and sets the found option if the match is not ambiguous.
411
412         :param kwargs: dict of name strings to values.
413         """
414         #opts, paths = self.cfgimpl_get_description()._cache_paths
415         all_paths = [p.split(".") for p in self.getpaths(allpaths=True)]
416         for key, value in kwargs.iteritems():
417             key_p = key.split('.')
418             candidates = [p for p in all_paths if p[-len(key_p):] == key_p]
419             if len(candidates) == 1:
420                 name = '.'.join(candidates[0])
421                 homeconfig, name = self.cfgimpl_get_home_by_path(name)
422                 try:
423                     getattr(homeconfig, name)
424                 except MandatoryError:
425                     pass
426                 except Exception, e:
427                     #FIXME hu? should have Exception ?
428                     raise e  # HiddenOptionError or DisabledOptionError
429                 child = getattr(homeconfig._cfgimpl_descr, name)
430                 homeconfig.setoption(name, child, value)
431             elif len(candidates) > 1:
432                 raise AmbigousOptionError(
433                     _('more than one option that ends with {}').format(key))
434             else:
435                 raise AttributeError(
436                     _('there is no option that matches {}'
437                       ' or the option is hidden or disabled').format(key))
438
439     def getpath(self):
440         return None
441
442     def _find(self, bytype, byname, byvalue, first, type_='option',
443               _subpath=None):
444         """
445         convenience method for finding an option that lives only in the subtree
446
447         :param first: return only one option if True, a list otherwise
448         :return: find list or an exception if nothing has been found
449         """
450         def _filter_by_name():
451             if byname is None:
452                 return True
453             if path == byname or path.endswith('.' + byname):
454                 return True
455             else:
456                 return False
457
458         def _filter_by_value():
459             if byvalue is None:
460                 return True
461             try:
462                 value = getattr(self, path)
463                 if value == byvalue:
464                     return True
465             except:  # a property restricts the access of the value
466                 pass
467             return False
468
469         def _filter_by_type():
470             if bytype is None:
471                 return True
472             if isinstance(option, bytype):
473                 return True
474             return False
475
476         #def _filter_by_attrs():
477         #    if byattrs is None:
478         #        return True
479         #    for key, val in byattrs.items():
480         #        print "----", path, key
481         #        if path == key or path.endswith('.' + key):
482         #            if value == val:
483         #                return True
484         #            else:
485         #                return False
486         #    return False
487         if type_ not in ('option', 'path', 'value'):
488             raise ValueError(_('unknown type_ type {0} for _find').format(type_))
489         find_results = []
490         opts, paths = self.cfgimpl_get_description()._cache_paths
491         for index in range(0, len(paths)):
492             option = opts[index]
493             if isinstance(option, OptionDescription):
494                 continue
495             path = paths[index]
496             if _subpath is not None and not path.startswith(_subpath + '.'):
497                 continue
498             if not _filter_by_name():
499                 continue
500             if not _filter_by_value():
501                 continue
502             #remove option with propertyerror, ...
503             try:
504                 value = getattr(self, path)
505             except:  # a property restricts the access of the value
506                 continue
507             if not _filter_by_type():
508                 continue
509             #if not _filter_by_attrs():
510             #    continue
511             if type_ == 'value':
512                 retval = value
513             elif type_ == 'path':
514                 retval = path
515             else:
516                 retval = option
517             if first:
518                 return retval
519             else:
520                 find_results.append(retval)
521         if find_results == []:
522             raise AttributeError(_("no option found in config with these criteria"))
523         else:
524             return find_results
525
526
527 def mandatory_warnings(config):
528     """convenience function to trace Options that are mandatory and
529     where no value has been set
530
531     :returns: generator of mandatory Option's path
532     """
533     for path in config.cfgimpl_get_description().getpaths(include_groups=True):
534         try:
535             config._getattr(path, force_properties=('mandatory',))
536         except MandatoryError:
537             yield path
538         except PropertiesOptionError:
539             pass