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