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