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