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