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