config's string representation
[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)
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             return getattr(self, opt_or_descr.path)
180         if name not in self._cfgimpl_values:
181             raise AttributeError("%s object has no attribute %s" %
182                                  (self.__class__, name))
183         self._validate(name, opt_or_descr, permissive)
184         # special attributes
185         if name.startswith('_cfgimpl_'):
186             # if it were in __dict__ it would have been found already
187             return self.__dict__[name]
188             raise AttributeError("%s object has no attribute %s" %
189                                  (self.__class__, name))
190         if not isinstance(opt_or_descr, OptionDescription):
191             # options with callbacks
192             if opt_or_descr.has_callback():
193                 value = self._cfgimpl_values[name]
194                 if (not opt_or_descr.is_frozen() or \
195                         not opt_or_descr.is_forced_on_freeze()) and \
196                         not opt_or_descr.is_default_owner(self):
197                     return value
198                 try:
199                     result = opt_or_descr.getcallback_value(
200                             self._cfgimpl_get_toplevel())
201                 except NoValueReturned, err:
202                     pass
203                 else:
204                     if opt_or_descr.is_multi():
205                         if not isinstance(result, list):
206                             result = [result]
207                         _result = Multi(result, value.config, value.child)
208                     else:
209                         # this result **shall not** be a list
210                         if isinstance(result, list):
211                             raise ConfigError('invalid calculated value returned'
212                                 ' for option {0} : shall not be a list'.format(name))
213                         _result = result
214                     if _result != None and not opt_or_descr.validate(_result,
215                                 settings.validator):
216                         raise ConfigError('invalid calculated value returned'
217                             ' for option {0}'.format(name))
218                     self._cfgimpl_values[name] = _result
219                     opt_or_descr.setowner(self, 'default')
220             if not opt_or_descr.has_callback() and opt_or_descr.is_forced_on_freeze():
221                 return opt_or_descr.getdefault()
222             self._test_mandatory(name, opt_or_descr)
223             # frozen and force default
224         return self._cfgimpl_values[name]
225
226     def unwrap_from_name(self, name):
227         """convenience method to extract and Option() object from the Config()
228         **and it is slow**: it recursively searches into the namespaces
229
230         :returns: Option()
231         """
232         paths = self.getpaths(allpaths=True)
233         opts = dict([(path, self.unwrap_from_path(path)) for path in paths])
234         all_paths = [p.split(".") for p in self.getpaths()]
235         for pth in all_paths:
236             if name in pth:
237                 return opts[".".join(pth)]
238         raise NotFoundError("name: {0} not found".format(name))
239
240     def unwrap_from_path(self, path):
241         """convenience method to extract and Option() object from the Config()
242         and it is **fast**: finds the option directly in the appropriate
243         namespace
244
245         :returns: Option()
246         """
247         if '.' in path:
248             homeconfig, path = self._cfgimpl_get_home_by_path(path)
249             return getattr(homeconfig._cfgimpl_descr, path)
250         return getattr(self._cfgimpl_descr, path)
251
252     def setoption(self, name, value, who=None):
253         """effectively modifies the value of an Option()
254         (typically called by the __setattr__)
255
256         :param who: is an owner's name
257                     who is **not necessarily** a owner, because it cannot be a list
258         :type who: string
259         """
260         child = getattr(self._cfgimpl_descr, name)
261         if type(child) != SymLinkOption:
262             if who == None:
263                 who = settings.owner
264             if child.is_multi():
265                 if type(value) != Multi:
266                     if type(value) == list:
267                         value = Multi(value, self, child)
268                     else:
269                         raise ConfigError("invalid value for option:"
270                                    " {0} that is set to multi".format(name))
271             child.setoption(self, value, who)
272             child.setowner(self, who)
273         else:
274             homeconfig = self._cfgimpl_get_toplevel()
275             child.setoption(homeconfig, value, who)
276
277     def set(self, **kwargs):
278         """
279         do what I mean"-interface to option setting. Searches all paths
280         starting from that config for matches of the optional arguments
281         and sets the found option if the match is not ambiguous.
282         :param kwargs: dict of name strings to values.
283         """
284         all_paths = [p.split(".") for p in self.getpaths(allpaths=True)]
285         for key, value in kwargs.iteritems():
286             key_p = key.split('.')
287             candidates = [p for p in all_paths if p[-len(key_p):] == key_p]
288             if len(candidates) == 1:
289                 name = '.'.join(candidates[0])
290                 homeconfig, name = self._cfgimpl_get_home_by_path(name)
291                 try:
292                     getattr(homeconfig, name)
293                 except MandatoryError:
294                     pass
295                 except Exception, e:
296                     raise e # HiddenOptionError or DisabledOptionError
297                 homeconfig.setoption(name, value, settings.owner)
298             elif len(candidates) > 1:
299                 raise AmbigousOptionError(
300                     'more than one option that ends with %s' % (key, ))
301             else:
302                 raise NoMatchingOptionFound(
303                     'there is no option that matches %s'
304                     ' or the option is hidden or disabled'% (key, ))
305
306     def get(self, name):
307         """
308         same as a `find_first()` method in a config that has identical names:
309         it returns the first item of an option named `name`
310
311         much like the attribute access way, except that
312         the search for the option is performed recursively in the whole
313         configuration tree.
314         **carefull**: very slow !
315
316         :returns: option value.
317         """
318         paths = self.getpaths(allpaths=True)
319         pathsvalues = []
320         for path in paths:
321             pathname = path.split('.')[-1]
322             if pathname == name:
323                 try:
324                     value = getattr(self, path)
325                     return value
326                 except Exception, e:
327                     raise e
328         raise NotFoundError("option {0} not found in config".format(name))
329
330     def _cfgimpl_get_home_by_path(self, path):
331         """:returns: tuple (config, name)"""
332         path = path.split('.')
333         for step in path[:-1]:
334             self = getattr(self, step)
335         return self, path[-1]
336
337     def _cfgimpl_get_toplevel(self):
338         ":returns: root config"
339         while self._cfgimpl_parent is not None:
340             self = self._cfgimpl_parent
341         return self
342
343     def _cfgimpl_get_path(self):
344         "the path in the attribute access meaning."
345         subpath = []
346         obj = self
347         while obj._cfgimpl_parent is not None:
348             subpath.insert(0, obj._cfgimpl_descr._name)
349             obj = obj._cfgimpl_parent
350         return ".".join(subpath)
351     # ______________________________________________________________________
352     def cfgimpl_previous_value(self, path):
353         "stores the previous value"
354         home, name = self._cfgimpl_get_home_by_path(path)
355         return home._cfgimpl_previous_values[name]
356
357     def get_previous_value(self, name):
358         "for the time being, only the previous Option's value is accessible"
359         return self._cfgimpl_previous_values[name]
360     # ______________________________________________________________________
361     def add_warning(self, warning):
362         "Config implements its own warning pile. Could be useful"
363         self._cfgimpl_get_toplevel()._cfgimpl_warnings.append(warning)
364
365     def get_warnings(self):
366         "Config implements its own warning pile"
367         return self._cfgimpl_get_toplevel()._cfgimpl_warnings
368     # ____________________________________________________________
369     def getkey(self):
370         return self._cfgimpl_descr.getkey(self)
371
372     def __hash__(self):
373         return hash(self.getkey())
374
375     def __eq__(self, other):
376         "Config comparison"
377         return self.getkey() == other.getkey()
378
379     def __ne__(self, other):
380         "Config comparison"
381         return not self == other
382     # ______________________________________________________________________
383     def __iter__(self):
384         """Pythonesque way of parsing group's ordered options.
385         iteration only on Options (not OptionDescriptions)"""
386         for child in self._cfgimpl_descr._children:
387             if isinstance(child, Option):
388                 try:
389                     yield child._name, getattr(self, child._name)
390                 except:
391                     pass # option with properties
392
393     def iter_groups(self, group_type=None):
394         """iteration on groups objects only.
395         All groups are returned if `group_type` is `None`, otherwise the groups
396         can be filtered by categories (families, or whatever).
397         """
398         if group_type == None:
399             groups = group_types
400         else:
401             if group_type not in group_types:
402                 raise TypeError("Unknown group_type: {0}".format(group_type))
403             groups = [group_type]
404         for child in self._cfgimpl_descr._children:
405             if isinstance(child, OptionDescription):
406                 try:
407                     if child.get_group_type() in groups:
408                         yield child._name, getattr(self, child._name)
409                 except:
410                     pass # hidden, disabled option
411     # ______________________________________________________________________
412     def __str__(self):
413         "Config's string representation"
414         lines = []
415         for name, grp in self.iter_groups():
416             lines.append("[%s]" % name)
417         for name, value in self:
418             try:
419                 lines.append("%s = %s" % (name, value))
420             except:
421                 pass
422         return '\n'.join(lines)
423
424     __repr__ = __str__
425
426
427     def getpaths(self, include_groups=False, allpaths=False, mandatory=False):
428         """returns a list of all paths in self, recursively, taking care of
429         the context of properties (hidden/disabled)
430
431         :param include_groups: if true, OptionDescription are included
432         :param allpaths: all the options (event the properties protected ones)
433         :param mandatory: includes the mandatory options
434         :returns: list of all paths
435         """
436         paths = []
437         for path in self._cfgimpl_descr.getpaths(include_groups=include_groups):
438             try:
439                 value = getattr(self, path)
440
441             except MandatoryError:
442                 if mandatory or allpaths:
443                     paths.append(path)
444             except PropertiesOptionError:
445                 if allpaths:
446                     paths.append(path) # option which have properties added
447             else:
448                  paths.append(path)
449         return paths
450
451     def _find(self, bytype, byname, byvalue, byattrs, first):
452         """
453             :param first: return only one option if True, a list otherwise
454         """
455         def _filter_by_attrs():
456             if byattrs is None:
457                 return True
458             for key, value in byattrs.items():
459                 if not hasattr(option, key):
460                     return False
461                 else:
462                     if getattr(option, key) != value:
463                         return False
464                     else:
465                         continue
466             return True
467         def _filter_by_name():
468             if byname is None:
469                 return True
470             pathname = path.split('.')[-1]
471             if pathname == byname:
472                 return True
473             else:
474                 return False
475         def _filter_by_value():
476             if byvalue is None:
477                 return True
478             try:
479                 value = getattr(self, path)
480                 if value == byvalue:
481                     return True
482             except Exception, e: # a property restricts the acces to value
483                 pass
484             return False
485         def _filter_by_type():
486             if bytype is None:
487                 return True
488             if isinstance(option, bytype):
489                 return True
490             return False
491
492         find_results = []
493         paths = self.getpaths(allpaths=True)
494         for path in paths:
495             option = self.unwrap_from_path(path)
496             if not _filter_by_name():
497                 continue
498             if not _filter_by_value():
499                 continue
500             if not _filter_by_type():
501                 continue
502             if not _filter_by_attrs():
503                 continue
504             if first:
505                 return option
506             else:
507                 find_results.append(option)
508         if first:
509             return None
510         else:
511             return find_results
512
513     def find(self, bytype=None, byname=None, byvalue=None, byattrs=None):
514         """
515             finds a list of options recursively in the config
516
517             :param bytype: Option class (BoolOption, StrOption, ...)
518             :param byname: filter by Option._name
519             :param byvalue: filter by the option's value
520             :param byattrs: dict of option attributes (default, callback...)
521             :returns: list of matching Option objects
522         """
523         return self._find(bytype, byname, byvalue, byattrs, first=False)
524
525     def find_first(self, bytype=None, byname=None, byvalue=None, byattrs=None):
526         """
527             finds an option recursively in the config
528
529             :param bytype: Option class (BoolOption, StrOption, ...)
530             :param byname: filter by Option._name
531             :param byvalue: filter by the option's value
532             :param byattrs: dict of option attributes (default, callback...)
533             :returns: list of matching Option objects
534         """
535         return self._find(bytype, byname, byvalue, byattrs, first=True)
536
537 def make_dict(config, flatten=False):
538     """export the whole config into a `dict`
539     :returns: dict of Option's name (or path) and values"""
540     paths = config.getpaths()
541     pathsvalues = []
542     for path in paths:
543         if flatten:
544             pathname = path.split('.')[-1]
545         else:
546             pathname = path
547         try:
548             value = getattr(config, path)
549             pathsvalues.append((pathname, value))
550         except:
551             pass # this just a hidden or disabled option
552     options = dict(pathsvalues)
553     return options
554
555 def mandatory_warnings(config):
556     """convenience function to trace Options that are mandatory and
557     where no value has been set
558
559     :returns: generator of mandatory Option's path
560     """
561     mandatory = settings.mandatory
562     settings.mandatory = True
563     for path in config._cfgimpl_descr.getpaths(include_groups=True):
564         try:
565             value = config._getattr(path, permissive=True)
566         except MandatoryError:
567             yield path
568         except PropertiesOptionError:
569             pass
570     settings.mandatory = mandatory