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