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