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