93cfe347c2de0293c962dabb5c059349739a1f48
[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, NotFoundError,
25                             AmbigousOptionError, NoMatchingOptionFound, 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 {}').format(name))
158
159         if setting.has_property('everything_frozen'):
160             raise TypeError(_("cannot set a value to the option {} 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 TypeError(_('cannot change the value to {} for '
166                             'option {} 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 Exception(_('unexpected path {}, '
344                                             'should start with {}').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                     raise e  # HiddenOptionError or DisabledOptionError
428                 child = getattr(homeconfig._cfgimpl_descr, name)
429                 homeconfig.setoption(name, child, value)
430             elif len(candidates) > 1:
431                 raise AmbigousOptionError(
432                     _('more than one option that ends with {}').format(key))
433             else:
434                 raise NoMatchingOptionFound(
435                     _('there is no option that matches {}'
436                       ' or the option is hidden or disabled').format(key))
437
438     def getpath(self):
439         return None
440
441     def _find(self, bytype, byname, byvalue, first, type_='option',
442               _subpath=None):
443         """
444         convenience method for finding an option that lives only in the subtree
445
446         :param first: return only one option if True, a list otherwise
447         :return: find list or an exception if nothing has been found
448         """
449         def _filter_by_name():
450             if byname is None:
451                 return True
452             if path == byname or path.endswith('.' + byname):
453                 return True
454             else:
455                 return False
456
457         def _filter_by_value():
458             if byvalue is None:
459                 return True
460             try:
461                 value = getattr(self, path)
462                 if value == byvalue:
463                     return True
464             except:  # a property restricts the access of the value
465                 pass
466             return False
467
468         def _filter_by_type():
469             if bytype is None:
470                 return True
471             if isinstance(option, bytype):
472                 return True
473             return False
474
475         #def _filter_by_attrs():
476         #    if byattrs is None:
477         #        return True
478         #    for key, val in byattrs.items():
479         #        print "----", path, key
480         #        if path == key or path.endswith('.' + key):
481         #            if value == val:
482         #                return True
483         #            else:
484         #                return False
485         #    return False
486         if type_ not in ('option', 'path', 'value'):
487             raise ValueError(_('unknown type_ type {} for _find').format(type_))
488         find_results = []
489         opts, paths = self.cfgimpl_get_description()._cache_paths
490         for index in range(0, len(paths)):
491             option = opts[index]
492             if isinstance(option, OptionDescription):
493                 continue
494             path = paths[index]
495             if _subpath is not None and not path.startswith(_subpath + '.'):
496                 continue
497             if not _filter_by_name():
498                 continue
499             if not _filter_by_value():
500                 continue
501             #remove option with propertyerror, ...
502             try:
503                 value = getattr(self, path)
504             except:  # a property restricts the access of the value
505                 continue
506             if not _filter_by_type():
507                 continue
508             #if not _filter_by_attrs():
509             #    continue
510             if type_ == 'value':
511                 retval = value
512             elif type_ == 'path':
513                 retval = path
514             else:
515                 retval = option
516             if first:
517                 return retval
518             else:
519                 find_results.append(retval)
520         if find_results == []:
521             raise NotFoundError(_("no option found in config with these criteria"))
522         else:
523             return find_results
524
525
526 def mandatory_warnings(config):
527     """convenience function to trace Options that are mandatory and
528     where no value has been set
529
530     :returns: generator of mandatory Option's path
531     """
532     for path in config.cfgimpl_get_description().getpaths(include_groups=True):
533         try:
534             config._getattr(path, force_properties=('mandatory',))
535         except MandatoryError:
536             yield path
537         except PropertiesOptionError:
538             pass