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