option's name shall not have an api's method name
[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 copy import copy
24 from inspect import getmembers, ismethod
25 from tiramisu.error import (PropertiesOptionError, ConfigError, NotFoundError,
26     AmbigousOptionError, ConflictConfigError, NoMatchingOptionFound,
27     MandatoryError, MethodCallError, NoValueReturned)
28 from tiramisu.option import (OptionDescription, Option, SymLinkOption,
29     apply_requires)
30 from tiramisu.setting import groups, owners, Setting
31 from tiramisu.value import Values
32
33
34 # ____________________________________________________________
35 class Config(object):
36     "main configuration management entry"
37     _cfgimpl_toplevel = None
38
39     def __init__(self, descr, parent=None, context=None):
40         """ Configuration option management master class
41
42         :param descr: describes the configuration schema
43         :type descr: an instance of ``option.OptionDescription``
44         :param parent: is None if the ``Config`` is root parent Config otherwise
45         :type parent: ``Config``
46         :param context: the current root config
47         :type context: `Config`
48         """
49         # main option description
50         self._cfgimpl_descr = descr
51         # sub option descriptions
52         self._cfgimpl_subconfigs = {}
53         self._cfgimpl_parent = parent
54         if context is None:
55             self._cfgimpl_context = self
56         else:
57             self._cfgimpl_context = context
58         if parent is None:
59             self._cfgimpl_settings = Setting()
60             self._cfgimpl_values = Values(self._cfgimpl_context)
61         else:
62             if context is None:
63                 raise ConfigError("cannot find a value for this config")
64             self._cfgimpl_settings = None
65             self._cfgimpl_values = None
66         "warnings are a great idea, let's make up a better use of it"
67         self._cfgimpl_warnings = []
68         self._cfgimpl_toplevel = self._cfgimpl_get_toplevel()
69         # some api members shall not be used as option's names !
70         methods = getmembers(self, ismethod)
71         self._cfgimpl_slots = [key for key, value in methods \
72                               if not key.startswith("_")]
73         self._cfgimpl_build()
74
75     def cfgimpl_get_settings(self):
76         return self._cfgimpl_context._cfgimpl_settings
77
78     def cfgimpl_set_settings(self, settings):
79         if not isinstance(settings, Setting):
80             raise ConfigError("setting not allowed")
81         self._cfgimpl_context._cfgimpl_settings = settings
82
83     def _validate_duplicates(self, children):
84         """duplicates Option names in the schema
85         :type children: list of `Option` or `OptionDescription`
86         """
87         duplicates = []
88         for dup in children:
89             if dup._name not in duplicates:
90                 duplicates.append(dup._name)
91             else:
92                 raise ConflictConfigError('duplicate option name: '
93                     '{0}'.format(dup._name))
94
95     def _cfgimpl_build(self):
96         """
97         - builds the config object from the schema
98         - settles various default values for options
99         """
100         self._validate_duplicates(self._cfgimpl_descr._children)
101         if self._cfgimpl_descr.group_type == groups.master:
102             mastername = self._cfgimpl_descr._name
103             masteropt = getattr(self._cfgimpl_descr, mastername)
104             self._cfgimpl_context._cfgimpl_values.masters[masteropt] = []
105
106         for child in self._cfgimpl_descr._children:
107             if isinstance(child, OptionDescription):
108                 self._validate_duplicates(child._children)
109                 self._cfgimpl_subconfigs[child] = Config(child, parent=self,
110                                                 context=self._cfgimpl_context)
111             else:
112                 if child._name in self._cfgimpl_slots:
113                     raise NameError("invalid name for the option:"
114                                     " {0}".format(child._name))
115
116             if (self._cfgimpl_descr.group_type == groups.master and
117                     child != masteropt):
118                 self._cfgimpl_context._cfgimpl_values.slaves[child] = masteropt
119                 self._cfgimpl_context._cfgimpl_values.masters[masteropt].append(child)
120
121     # ____________________________________________________________
122     # attribute methods
123     def __setattr__(self, name, value):
124         "attribute notation mechanism for the setting of the value of an option"
125         if name.startswith('_cfgimpl_'):
126             self.__dict__[name] = value
127             return
128         if '.' in name:
129             homeconfig, name = self._cfgimpl_get_home_by_path(name)
130             return setattr(homeconfig, name, value)
131         if type(getattr(self._cfgimpl_descr, name)) != SymLinkOption:
132             self._validate(name, getattr(self._cfgimpl_descr, name))
133         if name in self._cfgimpl_slots:
134             raise NameError("invalid name for the option:"
135                             " {0}".format(name))
136         self.setoption(name, value)
137
138     def _validate(self, name, opt_or_descr, permissive=False):
139         "validation for the setattr and the getattr"
140         apply_requires(opt_or_descr, self, permissive=permissive)
141         if not isinstance(opt_or_descr, Option) and \
142                 not isinstance(opt_or_descr, OptionDescription):
143             raise TypeError('Unexpected object: {0}'.format(repr(opt_or_descr)))
144         properties = copy(opt_or_descr.properties)
145         for proper in copy(properties):
146             if not self._cfgimpl_context._cfgimpl_settings.has_property(proper):
147                 properties.remove(proper)
148         if permissive:
149             for perm in self._cfgimpl_context._cfgimpl_settings.permissive:
150                 if perm in properties:
151                     properties.remove(perm)
152         if properties != []:
153             raise PropertiesOptionError("trying to access"
154                     " to an option named: {0} with properties"
155                     " {1}".format(name, str(properties)),
156                     properties)
157
158     def __getattr__(self, name):
159         return self._getattr(name)
160
161 #    def fill_multi(self, opt, result, use_default_multi=False, default_multi=None):
162 #        """fills a multi option with default and calculated values
163 #        """
164 #        # FIXME C'EST ENCORE DU N'IMPORTE QUOI
165 #        if not isinstance(result, list):
166 #            _result = [result]
167 #        else:
168 #            _result = result
169 #        return Multi(_result, self._cfgimpl_context, opt)
170
171     def _getattr(self, name, permissive=False):
172         """
173         attribute notation mechanism for accessing the value of an option
174         :param name: attribute name
175         :param permissive: permissive doesn't raise some property error
176                           (see ``permissive``)
177         :return: option's value if name is an option name, OptionDescription
178                  otherwise
179         """
180         # attribute access by passing a path,
181         # for instance getattr(self, "creole.general.family.adresse_ip_eth0")
182         if '.' in name:
183             homeconfig, name = self._cfgimpl_get_home_by_path(name)
184             return homeconfig._getattr(name, permissive)
185         opt_or_descr = getattr(self._cfgimpl_descr, name)
186         # symlink options
187         if type(opt_or_descr) == SymLinkOption:
188             rootconfig = self._cfgimpl_get_toplevel()
189             return getattr(rootconfig, opt_or_descr.path)
190
191         self._validate(name, opt_or_descr, permissive)
192         if isinstance(opt_or_descr, OptionDescription):
193             if opt_or_descr not in self._cfgimpl_subconfigs:
194                 raise AttributeError("%s with name %s object has no attribute %s" %
195                                  (self.__class__, opt_or_descr._name, name))
196             return self._cfgimpl_subconfigs[opt_or_descr]
197         # special attributes
198         if name.startswith('_cfgimpl_'):
199             # if it were in __dict__ it would have been found already
200             return self.__dict__[name]
201         return self._cfgimpl_context._cfgimpl_values[opt_or_descr]
202
203     def unwrap_from_name(self, name):
204         """convenience method to extract and Option() object from the Config()
205         **and it is slow**: it recursively searches into the namespaces
206
207         :returns: Option()
208         """
209         paths = self.getpaths(allpaths=True)
210         opts = dict([(path, self.unwrap_from_path(path)) for path in paths])
211         all_paths = [p.split(".") for p in self.getpaths()]
212         for pth in all_paths:
213             if name in pth:
214                 return opts[".".join(pth)]
215         raise NotFoundError("name: {0} not found".format(name))
216
217     def unwrap_from_path(self, path):
218         """convenience method to extract and Option() object from the Config()
219         and it is **fast**: finds the option directly in the appropriate
220         namespace
221
222         :returns: Option()
223         """
224         if '.' in path:
225             homeconfig, path = self._cfgimpl_get_home_by_path(path)
226             return getattr(homeconfig._cfgimpl_descr, path)
227         return getattr(self._cfgimpl_descr, path)
228
229     def setoption(self, name, value, who=None):
230         """effectively modifies the value of an Option()
231         (typically called by the __setattr__)
232         """
233         child = getattr(self._cfgimpl_descr, name)
234         child.setoption(self, value)
235
236     def set(self, **kwargs):
237         """
238         do what I mean"-interface to option setting. Searches all paths
239         starting from that config for matches of the optional arguments
240         and sets the found option if the match is not ambiguous.
241
242         :param kwargs: dict of name strings to values.
243         """
244         all_paths = [p.split(".") for p in self.getpaths(allpaths=True)]
245         for key, value in kwargs.iteritems():
246             key_p = key.split('.')
247             candidates = [p for p in all_paths if p[-len(key_p):] == key_p]
248             if len(candidates) == 1:
249                 name = '.'.join(candidates[0])
250                 homeconfig, name = self._cfgimpl_get_home_by_path(name)
251                 try:
252                     getattr(homeconfig, name)
253                 except MandatoryError:
254                     pass
255                 except Exception, e:
256                     raise e  # HiddenOptionError or DisabledOptionError
257                 homeconfig.setoption(name, value)
258             elif len(candidates) > 1:
259                 raise AmbigousOptionError(
260                     'more than one option that ends with %s' % (key, ))
261             else:
262                 raise NoMatchingOptionFound(
263                     'there is no option that matches %s'
264                     ' or the option is hidden or disabled' % (key, ))
265
266     def get(self, name):
267         """
268         same as a `find_first()` method in a config that has identical names:
269         it returns the first item of an option named `name`
270
271         much like the attribute access way, except that
272         the search for the option is performed recursively in the whole
273         configuration tree.
274         **carefull**: very slow !
275
276         :returns: option value.
277         """
278         paths = self.getpaths(allpaths=True)
279         pathsvalues = []
280         for path in paths:
281             pathname = path.split('.')[-1]
282             if pathname == name:
283                 try:
284                     value = getattr(self, path)
285                     return value
286                 except Exception, e:
287                     raise e
288         raise NotFoundError("option {0} not found in config".format(name))
289
290     def _cfgimpl_get_home_by_path(self, path):
291         """:returns: tuple (config, name)"""
292         path = path.split('.')
293         for step in path[:-1]:
294             self = getattr(self, step)
295         return self, path[-1]
296
297     def _cfgimpl_get_toplevel(self):
298         ":returns: root config"
299         while self._cfgimpl_parent is not None:
300             self = self._cfgimpl_parent
301         return self
302
303     def _cfgimpl_get_path(self):
304         "the path in the attribute access meaning."
305         subpath = []
306         obj = self
307         while obj._cfgimpl_parent is not None:
308             subpath.insert(0, obj._cfgimpl_descr._name)
309             obj = obj._cfgimpl_parent
310         return ".".join(subpath)
311     # ______________________________________________________________________
312 #    def cfgimpl_previous_value(self, path):
313 #        "stores the previous value"
314 #        home, name = self._cfgimpl_get_home_by_path(path)
315 #        # FIXME  fucking name
316 #        return home._cfgimpl_context._cfgimpl_values.previous_values[name]
317
318 #    def get_previous_value(self, name):
319 #        "for the time being, only the previous Option's value is accessible"
320 #        return self._cfgimpl_context._cfgimpl_values.previous_values[name]
321     # ______________________________________________________________________
322     def add_warning(self, warning):
323         "Config implements its own warning pile. Could be useful"
324         self._cfgimpl_get_toplevel()._cfgimpl_warnings.append(warning)
325
326     def get_warnings(self):
327         "Config implements its own warning pile"
328         return self._cfgimpl_get_toplevel()._cfgimpl_warnings
329     # ____________________________________________________________
330     def getkey(self):
331         return self._cfgimpl_descr.getkey(self)
332
333     def __hash__(self):
334         return hash(self.getkey())
335
336     def __eq__(self, other):
337         "Config comparison"
338         if not isinstance(other, Config):
339             return False
340         return self.getkey() == other.getkey()
341
342     def __ne__(self, other):
343         "Config comparison"
344         return not self == other
345     # ______________________________________________________________________
346     def __iter__(self):
347         """Pythonesque way of parsing group's ordered options.
348         iteration only on Options (not OptionDescriptions)"""
349         for child in self._cfgimpl_descr._children:
350             if not isinstance(child, OptionDescription):
351                 try:
352                     yield child._name, getattr(self, child._name)
353                 except:
354                     pass # option with properties
355
356     def iter_groups(self, group_type=None):
357         """iteration on groups objects only.
358         All groups are returned if `group_type` is `None`, otherwise the groups
359         can be filtered by categories (families, or whatever).
360
361         :param group_type: if defined, is an instance of `groups.GroupType`
362                            or `groups.MasterGroupType` that lives in
363                            `setting.groups`
364
365         """
366         if group_type is not None:
367             if not isinstance(group_type, groups.GroupType):
368                 raise TypeError("Unknown group_type: {0}".format(group_type))
369         for child in self._cfgimpl_descr._children:
370             if isinstance(child, OptionDescription):
371                 try:
372                     if group_type is not None:
373                         if child.get_group_type() == group_type:
374                             yield child._name, getattr(self, child._name)
375                     else:
376                         yield child._name, getattr(self, child._name)
377                 except:
378                     pass
379     # ______________________________________________________________________
380     def __str__(self):
381         "Config's string representation"
382         lines = []
383         for name, grp in self.iter_groups():
384             lines.append("[%s]" % name)
385         for name, value in self:
386             try:
387                 lines.append("%s = %s" % (name, value))
388             except:
389                 pass
390         return '\n'.join(lines)
391
392     __repr__ = __str__
393
394     def getpaths(self, include_groups=False, allpaths=False, mandatory=False):
395         """returns a list of all paths in self, recursively, taking care of
396         the context of properties (hidden/disabled)
397
398         :param include_groups: if true, OptionDescription are included
399         :param allpaths: all the options (event the properties protected ones)
400         :param mandatory: includes the mandatory options
401         :returns: list of all paths
402         """
403         paths = []
404         for path in self._cfgimpl_descr.getpaths(include_groups=include_groups):
405             try:
406                 value = getattr(self, path)
407
408             except MandatoryError:
409                 if mandatory or allpaths:
410                     paths.append(path)
411             except PropertiesOptionError:
412                 if allpaths:
413                     paths.append(path) # option which have properties added
414             else:
415                  paths.append(path)
416         return paths
417
418     def _find(self, bytype, byname, byvalue, byattrs, first):
419         """
420         convenience method for finding an option that lives only in the subtree
421
422         :param first: return only one option if True, a list otherwise
423         :return: find list or an exception if nothing has been found
424         """
425         def _filter_by_attrs():
426             if byattrs is None:
427                 return True
428             for key, value in byattrs.items():
429                 if not hasattr(option, key):
430                     return False
431                 else:
432                     if getattr(option, key) != value:
433                         return False
434                     else:
435                         continue
436             return True
437         def _filter_by_name():
438             if byname is None:
439                 return True
440             pathname = path.split('.')[-1]
441             if pathname == byname:
442                 return True
443             else:
444                 return False
445         def _filter_by_value():
446             if byvalue is None:
447                 return True
448             try:
449                 value = getattr(self, path)
450                 if value == byvalue:
451                     return True
452             except:  # a property restricts the access of the value
453                 pass
454             return False
455         def _filter_by_type():
456             if bytype is None:
457                 return True
458             if isinstance(option, bytype):
459                 return True
460             return False
461
462         find_results = []
463         paths = self.getpaths(allpaths=True)
464         for path in paths:
465             try:
466                 option = self.unwrap_from_path(path)
467             except PropertiesOptionError, err:
468                 continue
469             if not _filter_by_name():
470                 continue
471             if not _filter_by_value():
472                 continue
473             if not _filter_by_type():
474                 continue
475             if not _filter_by_attrs():
476                 continue
477             if first:
478                 return option
479             else:
480                 find_results.append(option)
481
482         if find_results == []:
483             raise NotFoundError("no option found in config with these criteria")
484         else:
485             return find_results
486
487     def find(self, bytype=None, byname=None, byvalue=None, byattrs=None):
488         """
489             finds a list of options recursively in the config
490
491             :param bytype: Option class (BoolOption, StrOption, ...)
492             :param byname: filter by Option._name
493             :param byvalue: filter by the option's value
494             :param byattrs: dict of option attributes (default, callback...)
495             :returns: list of matching Option objects
496         """
497         return self._find(bytype, byname, byvalue, byattrs, first=False)
498
499     def find_first(self, bytype=None, byname=None, byvalue=None, byattrs=None):
500         """
501             finds an option recursively in the config
502
503             :param bytype: Option class (BoolOption, StrOption, ...)
504             :param byname: filter by Option._name
505             :param byvalue: filter by the option's value
506             :param byattrs: dict of option attributes (default, callback...)
507             :returns: list of matching Option objects
508         """
509         return self._find(bytype, byname, byvalue, byattrs, first=True)
510
511
512 def make_dict(config, flatten=False):
513     """export the whole config into a `dict`
514     :returns: dict of Option's name (or path) and values"""
515     paths = config.getpaths()
516     pathsvalues = []
517     for path in paths:
518         if flatten:
519             pathname = path.split('.')[-1]
520         else:
521             pathname = path
522         try:
523             value = getattr(config, path)
524             pathsvalues.append((pathname, value))
525         except:
526             pass  # this just a hidden or disabled option
527     options = dict(pathsvalues)
528     return options
529
530
531 def mandatory_warnings(config):
532     """convenience function to trace Options that are mandatory and
533     where no value has been set
534
535     :returns: generator of mandatory Option's path
536     FIXME : CAREFULL : not multi-user
537     """
538     mandatory = config._cfgimpl_context._cfgimpl_settings.mandatory
539     config._cfgimpl_context._cfgimpl_settings.mandatory = True
540     for path in config._cfgimpl_descr.getpaths(include_groups=True):
541         try:
542             value = config._getattr(path, permissive=True)
543         except MandatoryError:
544             yield path
545         except PropertiesOptionError:
546             pass
547     config._cfgimpl_context._cfgimpl_settings.mandatory = mandatory