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