add option name's validation and rename Option method with objimpl_
[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
25 from tiramisu.option import OptionDescription, Option, SymLinkOption
26 from tiramisu.setting import groups, Setting
27 from tiramisu.value import Values
28 from tiramisu.i18n import _
29
30
31 class SubConfig(object):
32     "sub configuration management entry"
33     __slots__ = ('_cfgimpl_descr', '_cfgimpl_context')
34
35     def __init__(self, descr, context):
36         """ Configuration option management master class
37
38         :param descr: describes the configuration schema
39         :type descr: an instance of ``option.OptionDescription``
40         :param context: the current root config
41         :type context: `Config`
42         """
43         # main option description
44         if not isinstance(descr, OptionDescription):
45             raise ValueError(_('descr must be an optiondescription, not {0}').format(type(descr)))
46         self._cfgimpl_descr = descr
47         # sub option descriptions
48         self._cfgimpl_context = context
49
50     def cfgimpl_get_context(self):
51         return self._cfgimpl_context
52
53     def cfgimpl_get_settings(self):
54         return self._cfgimpl_context._cfgimpl_settings
55
56     def cfgimpl_get_values(self):
57         return self._cfgimpl_context._cfgimpl_values
58
59     def cfgimpl_get_consistancies(self):
60         return self.cfgimpl_get_context().cfgimpl_get_description()._consistancies
61
62     def cfgimpl_get_description(self):
63         return self._cfgimpl_descr
64
65     # ____________________________________________________________
66     # attribute methods
67     def __setattr__(self, name, value):
68         "attribute notation mechanism for the setting of the value of an option"
69         if name.startswith('_cfgimpl_'):
70             #self.__dict__[name] = value
71             object.__setattr__(self, name, value)
72             return
73         self._setattr(name, value)
74
75     def cfgimpl_reset_cache(self, only_expired=False, only=('values', 'settings')):
76         self.cfgimpl_get_context().cfgimpl_reset_cache(only_expired, only)
77
78     def _setattr(self, name, value, force_permissive=False):
79         if '.' in name:
80             homeconfig, name = self.cfgimpl_get_home_by_path(name)
81             return homeconfig.__setattr__(name, value)
82         child = getattr(self._cfgimpl_descr, name)
83         if type(child) != SymLinkOption:
84             self.cfgimpl_get_values().setitem(child, value,
85                                               force_permissive=force_permissive)
86         else:
87             child._setoption(self.cfgimpl_get_context(), value)
88
89     def __delattr__(self, name):
90         child = getattr(self._cfgimpl_descr, name)
91         del(self.cfgimpl_get_values()[child])
92
93     def __getattr__(self, name):
94         return self._getattr(name)
95
96     def _getattr(self, name, force_permissive=False, force_properties=None,
97                  validate=True):
98         """
99         attribute notation mechanism for accessing the value of an option
100         :param name: attribute name
101         :return: option's value if name is an option name, OptionDescription
102                  otherwise
103         """
104         # attribute access by passing a path,
105         # for instance getattr(self, "creole.general.family.adresse_ip_eth0")
106         if '.' in name:
107             homeconfig, name = self.cfgimpl_get_home_by_path(name,
108                                                              force_permissive=force_permissive,
109                                                              force_properties=force_properties)
110             return homeconfig._getattr(name, force_permissive=force_permissive,
111                                        force_properties=force_properties,
112                                        validate=validate)
113         # special attributes
114         if name.startswith('_cfgimpl_') or name.startswith('cfgimpl_'):
115             # if it were in __dict__ it would have been found already
116             object.__getattr__(self, name)
117         opt_or_descr = getattr(self.cfgimpl_get_description(), name)
118         # symlink options
119         if isinstance(opt_or_descr, SymLinkOption):
120             rootconfig = self.cfgimpl_get_context()
121             path = rootconfig.cfgimpl_get_description().objimpl_get_path_by_opt(opt_or_descr.opt)
122             return rootconfig._getattr(path, validate=validate,
123                                        force_properties=force_properties,
124                                        force_permissive=force_permissive)
125         elif isinstance(opt_or_descr, OptionDescription):
126             self.cfgimpl_get_settings().validate_properties(opt_or_descr,
127                                                             True, False,
128                                                             force_permissive=force_permissive,
129                                                             force_properties=force_properties)
130             return SubConfig(opt_or_descr, self._cfgimpl_context)
131         else:
132             return self.cfgimpl_get_values().getitem(opt_or_descr,
133                                                      validate=validate,
134                                                      force_properties=force_properties,
135                                                      force_permissive=force_permissive)
136
137     def cfgimpl_get_home_by_path(self, path, force_permissive=False, force_properties=None):
138         """:returns: tuple (config, name)"""
139         path = path.split('.')
140         for step in path[:-1]:
141             self = self._getattr(step,
142                                  force_permissive=force_permissive,
143                                  force_properties=force_properties)
144         return self, path[-1]
145
146     def __hash__(self):
147         return hash(self.cfgimpl_get_description().objimpl_getkey(self))
148
149     def __eq__(self, other):
150         "Config comparison"
151         if not isinstance(other, Config):
152             return False
153         return self.cfgimpl_get_description().objimpl_getkey(self) == \
154             other.cfgimpl_get_description().objimpl_getkey(other)
155
156     def __ne__(self, other):
157         "Config comparison"
158         if not isinstance(other, Config):
159             return False
160         return not self == other
161
162     # ______________________________________________________________________
163     def __iter__(self):
164         """Pythonesque way of parsing group's ordered options.
165         iteration only on Options (not OptionDescriptions)"""
166         for child in self.cfgimpl_get_description().objimpl_getchildren():
167             if not isinstance(child, OptionDescription):
168                 try:
169                     yield child._name, getattr(self, child._name)
170                 except GeneratorExit:
171                     raise StopIteration
172                 except PropertiesOptionError:
173                     pass  # option with properties
174
175     def iter_all(self):
176         """A way of parsing options **and** groups.
177         iteration on Options and OptionDescriptions."""
178         for child in self.cfgimpl_get_description().objimpl_getchildren():
179             try:
180                 yield child._name, getattr(self, child._name)
181             except GeneratorExit:
182                 raise StopIteration
183             except PropertiesOptionError:
184                 pass  # option with properties
185
186     def iter_groups(self, group_type=None):
187         """iteration on groups objects only.
188         All groups are returned if `group_type` is `None`, otherwise the groups
189         can be filtered by categories (families, or whatever).
190
191         :param group_type: if defined, is an instance of `groups.GroupType`
192                            or `groups.MasterGroupType` that lives in
193                            `setting.groups`
194
195         """
196         if group_type is not None:
197             if not isinstance(group_type, groups.GroupType):
198                 raise TypeError(_("unknown group_type: {0}").format(group_type))
199         for child in self.cfgimpl_get_description().objimpl_getchildren():
200             if isinstance(child, OptionDescription):
201                 try:
202                     if group_type is None or (group_type is not None and
203                                               child.objimpl_get_group_type() == group_type):
204                         yield child._name, getattr(self, child._name)
205                 except GeneratorExit:
206                     raise StopIteration
207                 except PropertiesOptionError:
208                     pass
209     # ______________________________________________________________________
210
211     def __str__(self):
212         "Config's string representation"
213         lines = []
214         for name, grp in self.iter_groups():
215             lines.append("[%s]" % name)
216         for name, value in self:
217             try:
218                 lines.append("%s = %s" % (name, value))
219             except PropertiesOptionError:
220                 pass
221         return '\n'.join(lines)
222
223     __repr__ = __str__
224
225     def cfgimpl_get_path(self):
226         descr = self.cfgimpl_get_description()
227         context_descr = self.cfgimpl_get_context().cfgimpl_get_description()
228         return context_descr.objimpl_get_path_by_opt(descr)
229
230     def find(self, bytype=None, byname=None, byvalue=None, type_='option'):
231         """
232             finds a list of options recursively in the config
233
234             :param bytype: Option class (BoolOption, StrOption, ...)
235             :param byname: filter by Option._name
236             :param byvalue: filter by the option's value
237             :returns: list of matching Option objects
238         """
239         return self.cfgimpl_get_context()._find(bytype, byname, byvalue,
240                                                 first=False,
241                                                 type_=type_,
242                                                 _subpath=self.cfgimpl_get_path())
243
244     def find_first(self, bytype=None, byname=None, byvalue=None, type_='option'):
245         """
246             finds an option recursively in the config
247
248             :param bytype: Option class (BoolOption, StrOption, ...)
249             :param byname: filter by Option._name
250             :param byvalue: filter by the option's value
251             :returns: list of matching Option objects
252         """
253         return self.cfgimpl_get_context()._find(bytype, byname, byvalue,
254                                                 first=True,
255                                                 type_=type_,
256                                                 _subpath=self.cfgimpl_get_path())
257
258     def make_dict(self, flatten=False, _currpath=None, withoption=None, withvalue=None):
259         """export the whole config into a `dict`
260         :returns: dict of Option's name (or path) and values"""
261         pathsvalues = []
262         if _currpath is None:
263             _currpath = []
264         if withoption is None and withvalue is not None:
265             raise ValueError(_("make_dict can't filtering with value without option"))
266         if withoption is not None:
267             mypath = self.cfgimpl_get_path()
268             for path in self.cfgimpl_get_context()._find(bytype=Option,
269                                                          byname=withoption,
270                                                          byvalue=withvalue,
271                                                          first=False,
272                                                          type_='path',
273                                                          _subpath=mypath):
274                 path = '.'.join(path.split('.')[:-1])
275                 opt = self.cfgimpl_get_context().cfgimpl_get_description().objimpl_get_opt_by_path(path)
276                 if mypath is not None:
277                     if mypath == path:
278                         withoption = None
279                         withvalue = None
280                         break
281                     else:
282                         tmypath = mypath + '.'
283                         if not path.startswith(tmypath):
284                             raise AttributeError(_('unexpected path {0}, '
285                                                  'should start with {1}').format(path, mypath))
286                         path = path[len(tmypath):]
287                 self._make_sub_dict(opt, path, pathsvalues, _currpath, flatten)
288         #withoption can be set to None below !
289         if withoption is None:
290             for opt in self.cfgimpl_get_description().objimpl_getchildren():
291                 path = opt._name
292                 self._make_sub_dict(opt, path, pathsvalues, _currpath, flatten)
293         if _currpath == []:
294             options = dict(pathsvalues)
295             return options
296         return pathsvalues
297
298     def _make_sub_dict(self, opt, path, pathsvalues, _currpath, flatten):
299         if isinstance(opt, OptionDescription):
300             pathsvalues += getattr(self, path).make_dict(flatten,
301                                                          _currpath + path.split('.'))
302         else:
303             try:
304                 value = self._getattr(opt._name)
305                 if flatten:
306                     name = opt._name
307                 else:
308                     name = '.'.join(_currpath + [opt._name])
309                 pathsvalues.append((name, value))
310             except PropertiesOptionError:
311                 pass  # this just a hidden or disabled option
312
313
314 # ____________________________________________________________
315 class Config(SubConfig):
316     "main configuration management entry"
317     __slots__ = ('_cfgimpl_settings', '_cfgimpl_values')
318
319     def __init__(self, descr):
320         """ Configuration option management master class
321
322         :param descr: describes the configuration schema
323         :type descr: an instance of ``option.OptionDescription``
324         :param context: the current root config
325         :type context: `Config`
326         """
327         self._cfgimpl_settings = Setting(self)
328         self._cfgimpl_values = Values(self)
329         super(Config, self).__init__(descr, self)  # , slots)
330         self._cfgimpl_build_all_paths()
331
332     def _cfgimpl_build_all_paths(self):
333         self._cfgimpl_descr.objimpl_build_cache()
334
335     def cfgimpl_reset_cache(self, only_expired=False, only=('values', 'settings')):
336         if 'values' in only:
337             self.cfgimpl_get_values().reset_cache(only_expired=only_expired)
338         if 'settings' in only:
339             self.cfgimpl_get_settings().reset_cache(only_expired=only_expired)
340
341     def unwrap_from_path(self, path):
342         """convenience method to extract and Option() object from the Config()
343         and it is **fast**: finds the option directly in the appropriate
344         namespace
345
346         :returns: Option()
347         """
348         if '.' in path:
349             homeconfig, path = self.cfgimpl_get_home_by_path(path)
350             return getattr(homeconfig._cfgimpl_descr, path)
351         return getattr(self._cfgimpl_descr, path)
352
353     def cfgimpl_get_path(self):
354         return None
355
356     def _find(self, bytype, byname, byvalue, first, type_='option',
357               _subpath=None):
358         """
359         convenience method for finding an option that lives only in the subtree
360
361         :param first: return only one option if True, a list otherwise
362         :return: find list or an exception if nothing has been found
363         """
364         def _filter_by_name():
365             if byname is None:
366                 return True
367             if path == byname or path.endswith('.' + byname):
368                 return True
369             else:
370                 return False
371
372         def _filter_by_value():
373             if byvalue is None:
374                 return True
375             try:
376                 value = getattr(self, path)
377                 if value == byvalue:
378                     return True
379             except PropertiesOptionError:  # a property restricts the access of the value
380                 pass
381             return False
382
383         def _filter_by_type():
384             if bytype is None:
385                 return True
386             if isinstance(option, bytype):
387                 return True
388             return False
389
390         #def _filter_by_attrs():
391         #    if byattrs is None:
392         #        return True
393         #    for key, val in byattrs.items():
394         #        print "----", path, key
395         #        if path == key or path.endswith('.' + key):
396         #            if value == val:
397         #                return True
398         #            else:
399         #                return False
400         #    return False
401         if type_ not in ('option', 'path', 'value'):
402             raise ValueError(_('unknown type_ type {0} for _find').format(type_))
403         find_results = []
404         opts, paths = self.cfgimpl_get_description()._cache_paths
405         for index in range(0, len(paths)):
406             option = opts[index]
407             if isinstance(option, OptionDescription):
408                 continue
409             path = paths[index]
410             if _subpath is not None and not path.startswith(_subpath + '.'):
411                 continue
412             if not _filter_by_name():
413                 continue
414             if not _filter_by_value():
415                 continue
416             #remove option with propertyerror, ...
417             try:
418                 value = getattr(self, path)
419             except PropertiesOptionError:  # a property restricts the access of the value
420                 continue
421             if not _filter_by_type():
422                 continue
423             #if not _filter_by_attrs():
424             #    continue
425             if type_ == 'value':
426                 retval = value
427             elif type_ == 'path':
428                 retval = path
429             else:
430                 retval = option
431             if first:
432                 return retval
433             else:
434                 find_results.append(retval)
435         if find_results == []:
436             raise AttributeError(_("no option found in config with these criteria"))
437         else:
438             return find_results
439
440
441 def mandatory_warnings(config):
442     """convenience function to trace Options that are mandatory and
443     where no value has been set
444
445     :returns: generator of mandatory Option's path
446     """
447     #if value in cache, properties are not calculated
448     config.cfgimpl_reset_cache(only=('values',))
449     for path in config.cfgimpl_get_description().objimpl_getpaths(include_groups=True):
450         try:
451             config._getattr(path, force_properties=('mandatory',))
452         except PropertiesOptionError, err:
453             if err.proptype == ['mandatory']:
454                 yield path
455     config.cfgimpl_reset_cache(only=('values',))