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