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