47ee6dc933a273e29588c7f2091cccb87ffedf78
[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         :param descr: describes the configuration schema
40         :type descr: an instance of ``option.OptionDescription``
41         :param parent: is None if the ``Config`` is root parent Config otherwise
42         :type parent: ``Config``
43         :param context: the current root config
44         :type context: `Config`
45         """
46         # main option description
47         self._cfgimpl_descr = descr
48         # sub option descriptions
49         self._cfgimpl_subconfigs = {}
50         self._cfgimpl_parent = parent
51         if context is None:
52             self._cfgimpl_context = self
53         else:
54             self._cfgimpl_context = context
55         if parent == None:
56             self._cfgimpl_settings = Setting()
57             self._cfgimpl_values = OptionValues(self._cfgimpl_context)
58         else:
59             if context is None:
60                 raise ConfigError("cannot find a value for this config")
61             self._cfgimpl_settings = None
62             self._cfgimpl_values = None
63         "warnings are a great idea, let's make up a better use of it"
64         self._cfgimpl_warnings = []
65         self._cfgimpl_toplevel = self._cfgimpl_get_toplevel()
66         self._cfgimpl_build()
67
68     def cfgimpl_get_settings(self):
69         return self._cfgimpl_context._cfgimpl_settings
70
71     def cfgimpl_set_settings(self, settings):
72         if not isinstance(settings, Setting):
73             raise ConfigError("setting not allowed")
74         self._cfgimpl_context._cfgimpl_settings = settings
75
76     def _validate_duplicates(self, children):
77         """duplicates Option names in the schema
78         :type children: list of `Option` or `OptionDescription`
79         """
80         duplicates = []
81         for dup in children:
82             if dup._name not in duplicates:
83                 duplicates.append(dup._name)
84             else:
85                 raise ConflictConfigError('duplicate option name: '
86                     '{0}'.format(dup._name))
87
88     def _cfgimpl_build(self):
89         """
90         - builds the config object from the schema
91         - settles various default values for options
92         """
93         self._validate_duplicates(self._cfgimpl_descr._children)
94         #max len for a master/slave group
95         max_len_child = 0
96         for child in self._cfgimpl_descr._children:
97             if isinstance(child, OptionDescription):
98                 self._validate_duplicates(child._children)
99                 self._cfgimpl_subconfigs[child] = Config(child, parent=self,
100                                                 context=self._cfgimpl_context)
101
102 #    def cfgimpl_update(self):
103 #        """dynamically adds `Option()` or `OptionDescription()`
104 #        """
105 #        # FIXME this is an update for new options in the schema only
106 #        #┬ásee the update_child() method of the descr object
107 #        for child in self._cfgimpl_descr._children:
108 #            if isinstance(child, Option):
109 #                if child._name not in self._cfgimpl_values:
110 #                    if child.is_multi():
111 #                        self._cfgimpl_values[child._name] = Multi(
112 #                                copy(child.getdefault()), config=self, opt=child)
113 #                    else:
114 #                        self._cfgimpl_values[child._name] = copy(child.getdefault())
115 #                    child.setowner(self, owners.default)
116 #            elif isinstance(child, OptionDescription):
117 #                if child._name not in self._cfgimpl_values:
118 #                    self._cfgimpl_values[child._name] = Config(child, parent=self)
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         self.setoption(name, value,
133                             self._cfgimpl_context._cfgimpl_settings.getowner())
134
135     def _validate(self, name, opt_or_descr, permissive=False):
136         "validation for the setattr and the getattr"
137         apply_requires(opt_or_descr, self, permissive=permissive)
138         if not isinstance(opt_or_descr, Option) and \
139                 not isinstance(opt_or_descr, OptionDescription):
140             raise TypeError('Unexpected object: {0}'.format(repr(opt_or_descr)))
141         properties = copy(opt_or_descr.properties)
142         for proper in copy(properties):
143             if not self._cfgimpl_context._cfgimpl_settings.has_property(proper):
144                 properties.remove(proper)
145         if permissive:
146             for perm in self._cfgimpl_context._cfgimpl_settings.permissive:
147                 if perm in properties:
148                     properties.remove(perm)
149         if properties != []:
150             raise PropertiesOptionError("trying to access"
151                     " to an option named: {0} with properties"
152                     " {1}".format(name, str(properties)),
153                     properties)
154
155     def __getattr__(self, name):
156         return self._getattr(name)
157
158     def fill_multi(self, opt, result, use_default_multi=False, default_multi=None):
159         """fills a multi option with default and calculated values
160         """
161         # FIXME C'EST ENCORE DU N'IMPORTE QUOI
162         if not isinstance(result, list):
163             _result = [result]
164         else:
165             _result = result
166         return Multi(_result, self._cfgimpl_context, opt)
167
168     def _getattr(self, name, permissive=False):
169         """
170         attribute notation mechanism for accessing the value of an option
171         :param name: attribute name
172         :param permissive: permissive doesn't raise some property error
173                           (see ``permissive``)
174         :return: option's value if name is an option name, OptionDescription
175                  otherwise
176         """
177         # attribute access by passing a path,
178         # for instance getattr(self, "creole.general.family.adresse_ip_eth0")
179         if '.' in name:
180             homeconfig, name = self._cfgimpl_get_home_by_path(name)
181             return homeconfig._getattr(name, permissive)
182         opt_or_descr = getattr(self._cfgimpl_descr, name)
183         # symlink options
184         if type(opt_or_descr) == SymLinkOption:
185             rootconfig = self._cfgimpl_get_toplevel()
186             return getattr(rootconfig, opt_or_descr.path)
187
188         self._validate(name, opt_or_descr, permissive)
189         if isinstance(opt_or_descr, OptionDescription):
190             if opt_or_descr not in self._cfgimpl_subconfigs:
191                 raise AttributeError("%s with name %s object has no attribute %s" %
192                                  (self.__class__, opt_or_descr._name, name))
193             return self._cfgimpl_subconfigs[opt_or_descr]
194         # special attributes
195         if name.startswith('_cfgimpl_'):
196             # if it were in __dict__ it would have been found already
197             return self.__dict__[name]
198         return self._cfgimpl_context._cfgimpl_values[opt_or_descr]
199
200     def unwrap_from_name(self, name):
201         """convenience method to extract and Option() object from the Config()
202         **and it is slow**: it recursively searches into the namespaces
203
204         :returns: Option()
205         """
206         paths = self.getpaths(allpaths=True)
207         opts = dict([(path, self.unwrap_from_path(path)) for path in paths])
208         all_paths = [p.split(".") for p in self.getpaths()]
209         for pth in all_paths:
210             if name in pth:
211                 return opts[".".join(pth)]
212         raise NotFoundError("name: {0} not found".format(name))
213
214     def unwrap_from_path(self, path):
215         """convenience method to extract and Option() object from the Config()
216         and it is **fast**: finds the option directly in the appropriate
217         namespace
218
219         :returns: Option()
220         """
221         if '.' in path:
222             homeconfig, path = self._cfgimpl_get_home_by_path(path)
223             return getattr(homeconfig._cfgimpl_descr, path)
224         return getattr(self._cfgimpl_descr, path)
225
226     def setoption(self, name, value, who=None):
227         """effectively modifies the value of an Option()
228         (typically called by the __setattr__)
229         :param who : an object that lives in `setting.owners`
230         """
231         child = getattr(self._cfgimpl_descr, name)
232         if type(child) != SymLinkOption:
233             if who == None:
234                 who = self._cfgimpl_context._cfgimpl_settings.owner
235             if child.is_multi():
236                 if not isinstance(who, owners.DefaultOwner):
237                     if type(value) != Multi:
238                         if type(value) == list:
239                             value = Multi(value, self._cfgimpl_context, child)
240                         else:
241                             raise ConfigError("invalid value for option:"
242                                        " {0} that is set to multi".format(name))
243                 else:
244                     value = self.fill_multi(child, child.getdefault(),
245                                         use_default_multi=True,
246                                         default_multi=child.getdefault_multi())
247             if not isinstance(who, owners.Owner):
248                 raise TypeError("invalid owner [{0}] for option: {1}".format(
249                                 str(who), name))
250             child.setoption(self, value)
251             child.setowner(self, who)
252         else:
253             homeconfig = self._cfgimpl_get_toplevel()
254             child.setoption(homeconfig, value)
255
256     def set(self, **kwargs):
257         """
258         do what I mean"-interface to option setting. Searches all paths
259         starting from that config for matches of the optional arguments
260         and sets the found option if the match is not ambiguous.
261         :param kwargs: dict of name strings to values.
262         """
263         all_paths = [p.split(".") for p in self.getpaths(allpaths=True)]
264         for key, value in kwargs.iteritems():
265             key_p = key.split('.')
266             candidates = [p for p in all_paths if p[-len(key_p):] == key_p]
267             if len(candidates) == 1:
268                 name = '.'.join(candidates[0])
269                 homeconfig, name = self._cfgimpl_get_home_by_path(name)
270                 try:
271                     getattr(homeconfig, name)
272                 except MandatoryError:
273                     pass
274                 except Exception, e:
275                     raise e # HiddenOptionError or DisabledOptionError
276                 homeconfig.setoption(name, value,
277                             self._cfgimpl_context._cfgimpl_settings.getowner())
278             elif len(candidates) > 1:
279                 raise AmbigousOptionError(
280                     'more than one option that ends with %s' % (key, ))
281             else:
282                 raise NoMatchingOptionFound(
283                     'there is no option that matches %s'
284                     ' or the option is hidden or disabled'% (key, ))
285
286     def get(self, name):
287         """
288         same as a `find_first()` method in a config that has identical names:
289         it returns the first item of an option named `name`
290
291         much like the attribute access way, except that
292         the search for the option is performed recursively in the whole
293         configuration tree.
294         **carefull**: very slow !
295
296         :returns: option value.
297         """
298         paths = self.getpaths(allpaths=True)
299         pathsvalues = []
300         for path in paths:
301             pathname = path.split('.')[-1]
302             if pathname == name:
303                 try:
304                     value = getattr(self, path)
305                     return value
306                 except Exception, e:
307                     raise e
308         raise NotFoundError("option {0} not found in config".format(name))
309
310     def _cfgimpl_get_home_by_path(self, path):
311         """:returns: tuple (config, name)"""
312         path = path.split('.')
313         for step in path[:-1]:
314             self = getattr(self, step)
315         return self, path[-1]
316
317     def _cfgimpl_get_toplevel(self):
318         ":returns: root config"
319         while self._cfgimpl_parent is not None:
320             self = self._cfgimpl_parent
321         return self
322
323     def _cfgimpl_get_path(self):
324         "the path in the attribute access meaning."
325         subpath = []
326         obj = self
327         while obj._cfgimpl_parent is not None:
328             subpath.insert(0, obj._cfgimpl_descr._name)
329             obj = obj._cfgimpl_parent
330         return ".".join(subpath)
331     # ______________________________________________________________________
332 #    def cfgimpl_previous_value(self, path):
333 #        "stores the previous value"
334 #        home, name = self._cfgimpl_get_home_by_path(path)
335 #        # FIXME  fucking name
336 #        return home._cfgimpl_context._cfgimpl_values.previous_values[name]
337
338 #    def get_previous_value(self, name):
339 #        "for the time being, only the previous Option's value is accessible"
340 #        return self._cfgimpl_context._cfgimpl_values.previous_values[name]
341     # ______________________________________________________________________
342     def add_warning(self, warning):
343         "Config implements its own warning pile. Could be useful"
344         self._cfgimpl_get_toplevel()._cfgimpl_warnings.append(warning)
345
346     def get_warnings(self):
347         "Config implements its own warning pile"
348         return self._cfgimpl_get_toplevel()._cfgimpl_warnings
349     # ____________________________________________________________
350     def getkey(self):
351         return self._cfgimpl_descr.getkey(self)
352
353     def __hash__(self):
354         return hash(self.getkey())
355
356     def __eq__(self, other):
357         "Config comparison"
358         if not isinstance(other, Config):
359             return False
360         return self.getkey() == other.getkey()
361
362     def __ne__(self, other):
363         "Config comparison"
364         return not self == other
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         :param group_type: if defined, is an instance of `groups.GroupType`
381                            or `groups.MasterGroupType` that lives in
382                            `setting.groups`
383
384         """
385         if group_type is not None:
386             if not isinstance(group_type, groups.GroupType):
387                 raise TypeError("Unknown group_type: {0}".format(group_type))
388         for child in self._cfgimpl_descr._children:
389             if isinstance(child, OptionDescription):
390                 try:
391                     if group_type is not None:
392                         if child.get_group_type() == group_type:
393                             yield child._name, getattr(self, child._name)
394                     else:
395                         yield child._name, getattr(self, child._name)
396                 except:
397                     pass
398     # ______________________________________________________________________
399     def __str__(self):
400         "Config's string representation"
401         lines = []
402         for name, grp in self.iter_groups():
403             lines.append("[%s]" % name)
404         for name, value in self:
405             try:
406                 lines.append("%s = %s" % (name, value))
407             except:
408                 pass
409         return '\n'.join(lines)
410
411     __repr__ = __str__
412
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 def make_dict(config, flatten=False):
532     """export the whole config into a `dict`
533     :returns: dict of Option's name (or path) and values"""
534     paths = config.getpaths()
535     pathsvalues = []
536     for path in paths:
537         if flatten:
538             pathname = path.split('.')[-1]
539         else:
540             pathname = path
541         try:
542             value = getattr(config, path)
543             pathsvalues.append((pathname, value))
544         except:
545             pass # this just a hidden or disabled option
546     options = dict(pathsvalues)
547     return options
548
549 def mandatory_warnings(config):
550     """convenience function to trace Options that are mandatory and
551     where no value has been set
552
553     :returns: generator of mandatory Option's path
554     FIXME : CAREFULL : not multi-user
555     """
556     mandatory = config._cfgimpl_context._cfgimpl_settings.mandatory
557     config._cfgimpl_context._cfgimpl_settings.mandatory = True
558     for path in config._cfgimpl_descr.getpaths(include_groups=True):
559         try:
560             value = config._getattr(path, permissive=True)
561         except MandatoryError:
562             yield path
563         except PropertiesOptionError:
564             pass
565     config._cfgimpl_context._cfgimpl_settings.mandatory = mandatory