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