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