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