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