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