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