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