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