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