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