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