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