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