don't launch apply_requires more than needed
[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         #FIXME _validate pour apply_requires ?
397         all_paths = [p.split(".") for p in self.getpaths(allpaths=True)]
398         for key, value in kwargs.iteritems():
399             key_p = key.split('.')
400             candidates = [p for p in all_paths if p[-len(key_p):] == key_p]
401             if len(candidates) == 1:
402                 name = '.'.join(candidates[0])
403                 homeconfig, name = self.cfgimpl_get_home_by_path(name)
404                 try:
405                     getattr(homeconfig, name)
406                 except MandatoryError:
407                     pass
408                 except Exception, e:
409                     raise e  # HiddenOptionError or DisabledOptionError
410                 homeconfig.setoption(name, value)
411             elif len(candidates) > 1:
412                 raise AmbigousOptionError(
413                     'more than one option that ends with %s' % (key, ))
414             else:
415                 raise NoMatchingOptionFound(
416                     'there is no option that matches %s'
417                     ' or the option is hidden or disabled' % (key, ))
418
419     def getpath(self):
420         return None
421
422     def _find(self, bytype, byname, byvalue, byattrs, first, type_='option',
423               _subpath=None):
424         """
425         convenience method for finding an option that lives only in the subtree
426
427         :param first: return only one option if True, a list otherwise
428         :return: find list or an exception if nothing has been found
429         """
430         def _filter_by_name():
431             if byname is None:
432                 return True
433             if path == byname or path.endswith('.' + byname):
434                 return True
435             else:
436                 return False
437
438         def _filter_by_value():
439             if byvalue is None:
440                 return True
441             try:
442                 value = getattr(self, path)
443                 if value == byvalue:
444                     return True
445             except:  # a property restricts the access of the value
446                 pass
447             return False
448
449         def _filter_by_type():
450             if bytype is None:
451                 return True
452             if isinstance(option, bytype):
453                 return True
454             return False
455
456         def _filter_by_attrs():
457             if byattrs is None:
458                 return True
459             for key, value in byattrs.items():
460                 if not hasattr(option, key):
461                     return False
462                 else:
463                     if getattr(option, key) != value:
464                         return False
465                     else:
466                         continue
467             return True
468         if type_ not in ('option', 'path', 'value'):
469             raise ValueError('unknown type_ type {} for _find'.format(type_))
470         find_results = []
471         opts, paths = self.cfgimpl_get_description()._cache_paths
472         for index in range(0, len(paths)):
473             option = opts[index]
474             if isinstance(option, OptionDescription):
475                 continue
476             path = paths[index]
477             if _subpath is not None and not path.startswith(_subpath + '.'):
478                 continue
479             if not _filter_by_name():
480                 continue
481             if not _filter_by_value():
482                 continue
483             if not _filter_by_type():
484                 continue
485             if not _filter_by_attrs():
486                 continue
487             #remove option with propertyerror, ...
488             try:
489                 value = getattr(self, path)
490             except:  # a property restricts the access of the value
491                 continue
492             if type_ == 'value':
493                 retval = value
494             elif type_ == 'path':
495                 retval = path
496             else:
497                 retval = option
498             if first:
499                 return retval
500             else:
501                 find_results.append(retval)
502         if find_results == []:
503             raise NotFoundError("no option found in config with these criteria")
504         else:
505             return find_results
506
507
508 def mandatory_warnings(config):
509     """convenience function to trace Options that are mandatory and
510     where no value has been set
511
512     :returns: generator of mandatory Option's path
513     """
514     for path in config.cfgimpl_get_description().getpaths(include_groups=True):
515         try:
516             config._getattr(path, force_properties=('mandatory',))
517         except MandatoryError:
518             yield path
519         except PropertiesOptionError:
520             pass