tiramisu/config.py can specify return type for find ('option', 'value', 'path') and...
[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 find(self, bytype=None, byname=None, byvalue=None, byattrs=None,
284              type_='option'):
285         """
286             finds a list of options recursively in the config
287
288             :param bytype: Option class (BoolOption, StrOption, ...)
289             :param byname: filter by Option._name
290             :param byvalue: filter by the option's value
291             :param byattrs: dict of option attributes (default, callback...)
292             :returns: list of matching Option objects
293         """
294         return self.cfgimpl_get_context()._find(bytype, byname, byvalue,
295                                                 byattrs, first=False,
296                                                 type_=type_,
297                                                 _subpath=self.getpath())
298
299     def find_first(self, bytype=None, byname=None, byvalue=None, byattrs=None,
300                    type_='option'):
301         """
302             finds an option recursively in the config
303
304             :param bytype: Option class (BoolOption, StrOption, ...)
305             :param byname: filter by Option._name
306             :param byvalue: filter by the option's value
307             :param byattrs: dict of option attributes (default, callback...)
308             :returns: list of matching Option objects
309         """
310         return self.cfgimpl_get_context()._find(bytype, byname, byvalue,
311                                                 byattrs, first=True,
312                                                 type_=type_,
313                                                 _subpath=self.getpath())
314
315     def make_dict(self, flatten=False, _currpath=None, withoption=None, withvalue=None):
316         """export the whole config into a `dict`
317         :returns: dict of Option's name (or path) and values"""
318         pathsvalues = []
319         if _currpath is None:
320             _currpath = []
321         if withoption is None and withvalue is not None:
322             raise ValueError("make_dict can't filtering with value without option")
323         if withoption is not None:
324             mypath = self.getpath()
325             for path in self.cfgimpl_get_context()._find(bytype=Option, byname=withoption,
326                                                          byvalue=withvalue, byattrs=None,
327                                                          first=False, ret='path', _subpath=mypath):
328                 path = '.'.join(path.split('.')[:-1])
329                 opt = self.cfgimpl_get_context().cfgimpl_get_description().get_opt_by_path(path)
330                 if mypath is not None:
331                     if mypath == path:
332                         withoption = None
333                         withvalue = None
334                         break
335                     else:
336                         tmypath = mypath + '.'
337                         if not path.startswith(tmypath):
338                             raise Exception('unexpected path {}, '
339                                             'should start with {}'.format(path, mypath))
340                         path = path[len(tmypath):]
341                 self._make_sub_dict(opt, path, pathsvalues, _currpath, flatten)
342         #withoption can be set to None below !
343         if withoption is None:
344             for opt in self.cfgimpl_get_description().getchildren():
345                 path = opt._name
346                 self._make_sub_dict(opt, path, pathsvalues, _currpath, flatten)
347         if _currpath == []:
348             options = dict(pathsvalues)
349             return options
350         return pathsvalues
351
352     def _make_sub_dict(self, opt, path, pathsvalues, _currpath, flatten):
353         if isinstance(opt, OptionDescription):
354             pathsvalues += getattr(self, path).make_dict(flatten,
355                                                          _currpath + path.split('.'))
356         else:
357             try:
358                 value = self._getattr(opt._name)
359                 if flatten:
360                     name = opt._name
361                 else:
362                     name = '.'.join(_currpath + [opt._name])
363                 pathsvalues.append((name, value))
364             except PropertiesOptionError:
365                 pass  # this just a hidden or disabled option
366
367
368 # ____________________________________________________________
369 class Config(SubConfig):
370     "main configuration management entry"
371     __slots__ = ('_cfgimpl_settings', '_cfgimpl_values')
372
373     def __init__(self, descr):
374         """ Configuration option management master class
375
376         :param descr: describes the configuration schema
377         :type descr: an instance of ``option.OptionDescription``
378         :param parent: is None if the ``Config`` is root parent Config otherwise
379         :type parent: ``Config``
380         :param context: the current root config
381         :type context: `Config`
382         """
383         self._cfgimpl_settings = Setting()
384         self._cfgimpl_values = Values(self)
385         super(Config, self).__init__(descr, None, self)  # , slots)
386         self._cfgimpl_build_all_paths()
387
388     def _cfgimpl_build_all_paths(self):
389         self._cfgimpl_descr.build_cache()
390
391     def unwrap_from_path(self, path):
392         """convenience method to extract and Option() object from the Config()
393         and it is **fast**: finds the option directly in the appropriate
394         namespace
395
396         :returns: Option()
397         """
398         if '.' in path:
399             homeconfig, path = self.cfgimpl_get_home_by_path(path)
400             return getattr(homeconfig._cfgimpl_descr, path)
401         return getattr(self._cfgimpl_descr, path)
402
403     def set(self, **kwargs):
404         """
405         do what I mean"-interface to option setting. Searches all paths
406         starting from that config for matches of the optional arguments
407         and sets the found option if the match is not ambiguous.
408
409         :param kwargs: dict of name strings to values.
410         """
411         all_paths = [p.split(".") for p in self.getpaths(allpaths=True)]
412         for key, value in kwargs.iteritems():
413             key_p = key.split('.')
414             candidates = [p for p in all_paths if p[-len(key_p):] == key_p]
415             if len(candidates) == 1:
416                 name = '.'.join(candidates[0])
417                 homeconfig, name = self.cfgimpl_get_home_by_path(name)
418                 try:
419                     getattr(homeconfig, name)
420                 except MandatoryError:
421                     pass
422                 except Exception, e:
423                     raise e  # HiddenOptionError or DisabledOptionError
424                 homeconfig.setoption(name, value)
425             elif len(candidates) > 1:
426                 raise AmbigousOptionError(
427                     'more than one option that ends with %s' % (key, ))
428             else:
429                 raise NoMatchingOptionFound(
430                     'there is no option that matches %s'
431                     ' or the option is hidden or disabled' % (key, ))
432
433     def getpath(self):
434         return None
435
436     def _find(self, bytype, byname, byvalue, byattrs, first, type_='option',
437               _subpath=None):
438         """
439         convenience method for finding an option that lives only in the subtree
440
441         :param first: return only one option if True, a list otherwise
442         :return: find list or an exception if nothing has been found
443         """
444         def _filter_by_name():
445             if byname is None:
446                 return True
447             if path == byname or path.endswith('.' + byname):
448                 return True
449             else:
450                 return False
451
452         def _filter_by_value():
453             if byvalue is None:
454                 return True
455             try:
456                 value = getattr(self, path)
457                 if value == byvalue:
458                     return True
459             except:  # a property restricts the access of the value
460                 pass
461             return False
462
463         def _filter_by_type():
464             if bytype is None:
465                 return True
466             if isinstance(option, bytype):
467                 return True
468             return False
469
470         def _filter_by_attrs():
471             if byattrs is None:
472                 return True
473             for key, value in byattrs.items():
474                 if not hasattr(option, key):
475                     return False
476                 else:
477                     if getattr(option, key) != value:
478                         return False
479                     else:
480                         continue
481             return True
482         if type_ not in ('option', 'path', 'value'):
483             raise ValueError('unknown type_ type {} for _find'.format(type_))
484         find_results = []
485         opts, paths = self.cfgimpl_get_description()._cache_paths
486         for index in range(0, len(paths)):
487             option = opts[index]
488             if isinstance(option, OptionDescription):
489                 continue
490             path = paths[index]
491             if _subpath is not None and not path.startswith(_subpath + '.'):
492                 continue
493             if not _filter_by_name():
494                 continue
495             if not _filter_by_value():
496                 continue
497             if not _filter_by_type():
498                 continue
499             if not _filter_by_attrs():
500                 continue
501             #remove option with propertyerror, ...
502             try:
503                 value = getattr(self, path)
504             except:  # a property restricts the access of the value
505                 continue
506             if type_ == 'value':
507                 retval = value
508             elif type_ == 'path':
509                 retval = path
510             else:
511                 retval = option
512             if first:
513                 return retval
514             else:
515                 find_results.append(retval)
516         if find_results == []:
517             raise NotFoundError("no option found in config with these criteria")
518         else:
519             return find_results
520
521
522 def mandatory_warnings(config):
523     """convenience function to trace Options that are mandatory and
524     where no value has been set
525
526     :returns: generator of mandatory Option's path
527     """
528     for path in config.cfgimpl_get_description().getpaths(include_groups=True):
529         try:
530             config._getattr(path, force_properties=('mandatory',))
531         except MandatoryError:
532             yield path
533         except PropertiesOptionError:
534             pass