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