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