58e9067a399027e1ae6a242c09e58847ac51e867
[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     # ______________________________________________________________________
331     def add_warning(self, warning):
332         "Config implements its own warning pile. Could be useful"
333         self._cfgimpl_get_toplevel()._cfgimpl_warnings.append(warning)
334
335     def get_warnings(self):
336         "Config implements its own warning pile"
337         return self._cfgimpl_get_toplevel()._cfgimpl_warnings
338
339     # ____________________________________________________________
340     def getkey(self):
341         return self._cfgimpl_descr.getkey(self)
342
343     def __hash__(self):
344         return hash(self.getkey())
345
346     def __eq__(self, other):
347         "Config comparison"
348         if not isinstance(other, Config):
349             return False
350         return self.getkey() == other.getkey()
351
352     def __ne__(self, other):
353         "Config comparison"
354         return not self == other
355
356     # ______________________________________________________________________
357     def __iter__(self):
358         """Pythonesque way of parsing group's ordered options.
359         iteration only on Options (not OptionDescriptions)"""
360         for child in self._cfgimpl_descr._children:
361             if not isinstance(child, OptionDescription):
362                 try:
363                     yield child._name, getattr(self, child._name)
364                 except:
365                     pass  # option with properties
366
367     def iter_groups(self, group_type=None):
368         """iteration on groups objects only.
369         All groups are returned if `group_type` is `None`, otherwise the groups
370         can be filtered by categories (families, or whatever).
371
372         :param group_type: if defined, is an instance of `groups.GroupType`
373                            or `groups.MasterGroupType` that lives in
374                            `setting.groups`
375
376         """
377         if group_type is not None:
378             if not isinstance(group_type, groups.GroupType):
379                 raise TypeError("Unknown group_type: {0}".format(group_type))
380         for child in self._cfgimpl_descr._children:
381             if isinstance(child, OptionDescription):
382                 try:
383                     if group_type is not None:
384                         if child.get_group_type() == group_type:
385                             yield child._name, getattr(self, child._name)
386                     else:
387                         yield child._name, getattr(self, child._name)
388                 except:
389                     pass
390     # ______________________________________________________________________
391     def __str__(self):
392         "Config's string representation"
393         lines = []
394         for name, grp in self.iter_groups():
395             lines.append("[%s]" % name)
396         for name, value in self:
397             try:
398                 lines.append("%s = %s" % (name, value))
399             except:
400                 pass
401         return '\n'.join(lines)
402
403     __repr__ = __str__
404
405     def getpaths(self, include_groups=False, allpaths=False, mandatory=False):
406         """returns a list of all paths in self, recursively, taking care of
407         the context of properties (hidden/disabled)
408
409         :param include_groups: if true, OptionDescription are included
410         :param allpaths: all the options (event the properties protected ones)
411         :param mandatory: includes the mandatory options
412         :returns: list of all paths
413         """
414         paths = []
415         for path in self._cfgimpl_descr.getpaths(include_groups=include_groups):
416             try:
417                 value = getattr(self, path)
418
419             except MandatoryError:
420                 if mandatory or allpaths:
421                     paths.append(path)
422             except PropertiesOptionError:
423                 if allpaths:
424                     paths.append(path)  # option which have properties added
425             else:
426                 paths.append(path)
427         return paths
428
429     def _find(self, bytype, byname, byvalue, byattrs, first):
430         """
431         convenience method for finding an option that lives only in the subtree
432
433         :param first: return only one option if True, a list otherwise
434         :return: find list or an exception if nothing has been found
435         """
436         def _filter_by_attrs():
437             if byattrs is None:
438                 return True
439             for key, value in byattrs.items():
440                 if not hasattr(option, key):
441                     return False
442                 else:
443                     if getattr(option, key) != value:
444                         return False
445                     else:
446                         continue
447             return True
448         def _filter_by_name():
449             if byname is None:
450                 return True
451             pathname = path.split('.')[-1]
452             if pathname == byname:
453                 return True
454             else:
455                 return False
456         def _filter_by_value():
457             if byvalue is None:
458                 return True
459             try:
460                 value = getattr(self, path)
461                 if value == byvalue:
462                     return True
463             except:  # a property restricts the access of the value
464                 pass
465             return False
466         def _filter_by_type():
467             if bytype is None:
468                 return True
469             if isinstance(option, bytype):
470                 return True
471             return False
472
473         find_results = []
474         paths = self.getpaths(allpaths=True)
475         for path in paths:
476             try:
477                 option = self.unwrap_from_path(path)
478             except PropertiesOptionError, err:
479                 continue
480             if not _filter_by_name():
481                 continue
482             if not _filter_by_value():
483                 continue
484             if not _filter_by_type():
485                 continue
486             if not _filter_by_attrs():
487                 continue
488             if first:
489                 return option
490             else:
491                 find_results.append(option)
492
493         if find_results == []:
494             raise NotFoundError("no option found in config with these criteria")
495         else:
496             return find_results
497
498     def find(self, bytype=None, byname=None, byvalue=None, byattrs=None):
499         """
500             finds a list of options recursively in the config
501
502             :param bytype: Option class (BoolOption, StrOption, ...)
503             :param byname: filter by Option._name
504             :param byvalue: filter by the option's value
505             :param byattrs: dict of option attributes (default, callback...)
506             :returns: list of matching Option objects
507         """
508         return self._find(bytype, byname, byvalue, byattrs, first=False)
509
510     def find_first(self, bytype=None, byname=None, byvalue=None, byattrs=None):
511         """
512             finds an option recursively in the config
513
514             :param bytype: Option class (BoolOption, StrOption, ...)
515             :param byname: filter by Option._name
516             :param byvalue: filter by the option's value
517             :param byattrs: dict of option attributes (default, callback...)
518             :returns: list of matching Option objects
519         """
520         return self._find(bytype, byname, byvalue, byattrs, first=True)
521
522
523 def make_dict(config, flatten=False):
524     """export the whole config into a `dict`
525     :returns: dict of Option's name (or path) and values"""
526     paths = config.getpaths()
527     pathsvalues = []
528     for path in paths:
529         if flatten:
530             pathname = path.split('.')[-1]
531         else:
532             pathname = path
533         try:
534             value = getattr(config, path)
535             pathsvalues.append((pathname, value))
536         except:
537             pass  # this just a hidden or disabled option
538     options = dict(pathsvalues)
539     return options
540
541
542 def mandatory_warnings(config):
543     """convenience function to trace Options that are mandatory and
544     where no value has been set
545
546     :returns: generator of mandatory Option's path
547     FIXME : CAREFULL : not multi-user
548     """
549     mandatory = config._cfgimpl_context._cfgimpl_settings.mandatory
550     config._cfgimpl_context._cfgimpl_settings.mandatory = True
551     for path in config._cfgimpl_descr.getpaths(include_groups=True):
552         try:
553             value = config._getattr(path, permissive=True)
554         except MandatoryError:
555             yield path
556         except PropertiesOptionError:
557             pass
558     config._cfgimpl_context._cfgimpl_settings.mandatory = mandatory