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