2a8aeee106bfd9c239f08a7027033d1757561d12
[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, Owner)
29
30 # ______________________________________________________________________
31 # generic owner. 'default' is the general config owner after init time
32 default_owner = 'user'
33
34 # ____________________________________________________________
35 class Config(object):
36     "properties attribute: the name of a property enables this property"
37     _cfgimpl_properties = ['hidden', 'disabled']
38     _cfgimpl_permissive = []
39     "mandatory means: a mandatory option has to have a value that is not None"
40     _cfgimpl_mandatory = True
41     _cfgimpl_frozen = True
42     _cfgimpl_owner = default_owner
43     _cfgimpl_toplevel = None
44
45     def __init__(self, descr, parent=None):
46         """ Configuration option management master class
47         :param descr: describes the configuration schema
48         :type descr: an instance of ``option.OptionDescription``
49         :param parent: is None if the ``Config`` is root parent Config otherwise
50         :type parent: ``Config``
51         """
52         self._cfgimpl_descr = descr
53         self._cfgimpl_value_owners = {}
54         self._cfgimpl_parent = parent
55         "`Config()` indeed is in charge of the `Option()`'s values"
56         self._cfgimpl_values = {}
57         self._cfgimpl_previous_values = {}
58         "warnings are a great idea, let's make up a better use of it"
59         self._cfgimpl_warnings = []
60         self._cfgimpl_toplevel = self._cfgimpl_get_toplevel()
61         '`freeze()` allows us to carry out this calculation again if necessary'
62         self._cfgimpl_frozen = self._cfgimpl_toplevel._cfgimpl_frozen
63         self._cfgimpl_build()
64
65     def _validate_duplicates(self, children):
66         """duplicates Option names in the schema
67         :type children: list of `Option` or `OptionDescription`
68         """
69         duplicates = []
70         for dup in children:
71             if dup._name not in duplicates:
72                 duplicates.append(dup._name)
73             else:
74                 raise ConflictConfigError('duplicate option name: '
75                     '{0}'.format(dup._name))
76
77     def _cfgimpl_build(self):
78         """
79         - builds the config object from the schema
80         - settles various default values for options
81         """
82         self._validate_duplicates(self._cfgimpl_descr._children)
83         for child in self._cfgimpl_descr._children:
84             if isinstance(child, Option):
85                 if child.is_multi():
86                     childdef = Multi(copy(child.getdefault()), config=self,
87                                      child=child)
88                     self._cfgimpl_values[child._name] = childdef
89                     self._cfgimpl_previous_values[child._name] = list(childdef)
90                     self._cfgimpl_value_owners[child._name] = ['default' \
91                         for i in range(len(child.getdefault() ))]
92                 else:
93                     childdef = child.getdefault()
94                     self._cfgimpl_values[child._name] = childdef
95                     self._cfgimpl_previous_values[child._name] = childdef
96                     self._cfgimpl_value_owners[child._name] = 'default'
97             elif isinstance(child, OptionDescription):
98                 self._validate_duplicates(child._children)
99                 self._cfgimpl_values[child._name] = Config(child, parent=self)
100 #        self.override(overrides)
101
102     def cfgimpl_set_permissive(self, permissive):
103         if not isinstance(permissive, list):
104             raise TypeError('permissive must be a list')
105         self._cfgimpl_permissive = permissive
106
107     def cfgimpl_update(self):
108         """dynamically adds `Option()` or `OptionDescription()`
109         """
110         # FIXME this is an update for new options in the schema only
111         #┬ásee the update_child() method of the descr object
112         for child in self._cfgimpl_descr._children:
113             if isinstance(child, Option):
114                 if child._name not in self._cfgimpl_values:
115                     if child.is_multi():
116                         self._cfgimpl_values[child._name] = Multi(
117                                 copy(child.getdefault()), config=self, child=child)
118                         self._cfgimpl_value_owners[child._name] = ['default' \
119                                 for i in range(len(child.getdefault() ))]
120                     else:
121                         self._cfgimpl_values[child._name] = copy(child.getdefault())
122                         self._cfgimpl_value_owners[child._name] = 'default'
123             elif isinstance(child, OptionDescription):
124                 if child._name not in self._cfgimpl_values:
125                     self._cfgimpl_values[child._name] = Config(child, parent=self)
126
127 #    def override(self, overrides):
128 #        """
129 #        overrides default values. This marks the overridden values as defaults.
130 #        :param overrides: is a dictionary of path strings to values.
131 #        """
132 #        for name, value in overrides.iteritems():
133 #            homeconfig, name = self._cfgimpl_get_home_by_path(name)
134 #            homeconfig.setoption(name, value, 'default')
135
136     def cfgimpl_set_owner(self, owner):
137         ":param owner: sets the default value for owner at the Config level"
138         self._cfgimpl_owner = owner
139         for child in self._cfgimpl_descr._children:
140             if isinstance(child, OptionDescription):
141                 self._cfgimpl_values[child._name].cfgimpl_set_owner(owner)
142     # ____________________________________________________________
143     # properties methods
144     def _cfgimpl_has_properties(self):
145         "has properties means the Config's properties attribute is not empty"
146         return bool(len(self._cfgimpl_properties))
147
148     def _cfgimpl_has_property(self, propname):
149         """has property propname in the Config's properties attribute
150         :param property: string wich is the name of the property"""
151         return propname in self._cfgimpl_properties
152
153     def cfgimpl_enable_property(self, propname):
154         "puts property propname in the Config's properties attribute"
155         if self._cfgimpl_parent != None:
156             raise MethodCallError("this method root_hide() shall not be"
157                                   "used with non-root Config() object")
158         if propname not in self._cfgimpl_properties:
159             self._cfgimpl_properties.append(propname)
160
161     def cfgimpl_disable_property(self, propname):
162         "deletes property propname in the Config's properties attribute"
163         if self._cfgimpl_parent != None:
164             raise MethodCallError("this method root_hide() shall not be"
165                                   "used with non-root Config() object")
166         if self._cfgimpl_has_property(propname):
167             self._cfgimpl_properties.remove(propname)
168     # ____________________________________________________________
169     # attribute methods
170     def __setattr__(self, name, value):
171         "attribute notation mechanism for the setting of the value of an option"
172         if name.startswith('_cfgimpl_'):
173             self.__dict__[name] = value
174             return
175         if '.' in name:
176             homeconfig, name = self._cfgimpl_get_home_by_path(name)
177             return setattr(homeconfig, name, value)
178         if type(getattr(self._cfgimpl_descr, name)) != SymLinkOption:
179             self._validate(name, getattr(self._cfgimpl_descr, name))
180         self.setoption(name, value, self._cfgimpl_owner)
181
182     def _validate(self, name, opt_or_descr, permissive=False):
183         "validation for the setattr and the getattr"
184         apply_requires(opt_or_descr, self)
185         if not isinstance(opt_or_descr, Option) and \
186                 not isinstance(opt_or_descr, OptionDescription):
187             raise TypeError('Unexpected object: {0}'.format(repr(opt_or_descr)))
188         properties = copy(opt_or_descr.properties)
189         for proper in copy(properties):
190             if not self._cfgimpl_toplevel._cfgimpl_has_property(proper):
191                 properties.remove(proper)
192         if permissive:
193             for perm in self._cfgimpl_toplevel._cfgimpl_permissive:
194                 if perm in properties:
195                     properties.remove(perm)
196         if properties != []:
197             raise PropertiesOptionError("trying to access"
198                     " to an option named: {0} with properties"
199                     " {1}".format(name, str(properties)),
200                     properties)
201
202     def _is_empty(self, opt):
203         "convenience method to know if an option is empty"
204         if (not opt.is_multi() and self._cfgimpl_values[opt._name] == None) or \
205             (opt.is_multi() and (self._cfgimpl_values[opt._name] == [] or \
206                 None in self._cfgimpl_values[opt._name])):
207             return True
208         return False
209
210     def _test_mandatory(self, path, opt):
211         # mandatory options
212         homeconfig = self._cfgimpl_get_toplevel()
213         mandatory = homeconfig._cfgimpl_mandatory
214         if opt.is_mandatory() and mandatory:
215             if self._is_empty(opt) and \
216                     opt.is_empty_by_default():
217                 raise MandatoryError("option: {0} is mandatory "
218                                       "and shall have a value".format(path))
219
220     def __getattr__(self, name):
221         return self._getattr(name)
222
223     def _getattr(self, name, permissive=False):
224         """
225         attribute notation mechanism for accessing the value of an option
226         :param name: attribute name
227         :param permissive: permissive doesn't raise some property error
228         (see ``_cfgimpl_permissive``)
229         :return: option's value if name is an option name, OptionDescription
230         otherwise
231         """
232         # attribute access by passing a path,
233         # for instance getattr(self, "creole.general.family.adresse_ip_eth0")
234         if '.' in name:
235             homeconfig, name = self._cfgimpl_get_home_by_path(name)
236             return homeconfig._getattr(name, permissive)
237         opt_or_descr = getattr(self._cfgimpl_descr, name)
238         # symlink options
239         if type(opt_or_descr) == SymLinkOption:
240             return getattr(self, opt_or_descr.path)
241         if name not in self._cfgimpl_values:
242             raise AttributeError("%s object has no attribute %s" %
243                                  (self.__class__, name))
244         self._validate(name, opt_or_descr, permissive)
245         # special attributes
246         if name.startswith('_cfgimpl_'):
247             # if it were in __dict__ it would have been found already
248             return self.__dict__[name]
249             raise AttributeError("%s object has no attribute %s" %
250                                  (self.__class__, name))
251         if not isinstance(opt_or_descr, OptionDescription):
252             # options with callbacks (fill or auto)
253             if opt_or_descr.has_callback():
254                 value = self._cfgimpl_values[name]
255                 if (not opt_or_descr.is_frozen() or \
256                         not opt_or_descr.is_forced_on_freeze()) and \
257                         not opt_or_descr.is_default_owner(self, all_default=False):
258                         #not opt_or_descr.getowner(self) == 'default':
259                     if opt_or_descr.is_multi():
260                         if None not in value:
261                             return value
262                     else:
263                         return value
264                 try:
265                     result = opt_or_descr.getcallback_value(
266                             self._cfgimpl_get_toplevel())
267                 except NoValueReturned, err:
268                     pass
269                 else:
270                     if opt_or_descr.is_multi():
271                         owners = copy(self._cfgimpl_value_owners[name])
272                         self._cfgimpl_value_owners[name] = []
273                         if not isinstance(result, list):
274                             # for example, [1, 2, 3, None] -> [1, 2, 3, result]
275                             _result = Multi([result], value.config, value.child)
276                             for cpt in range(len(value)):
277                                 val = value[cpt]
278                                 if len(owners) > cpt:
279                                     if owners[cpt] == 'default':
280                                         _result.append(result)
281                                         self._cfgimpl_value_owners[name][cpt] = 'default'
282                                     else:
283                                         _result.append(val)
284                                 else:
285                                     _result.append(val)
286                                     self._cfgimpl_value_owners[name][cpt] = 'default'
287                         else:
288                             # for example, [1, None, 2, None] + [a, b, c, d]
289                             # = [1, b, 2, d]
290                             _result = Multi([], value.config, value.child)
291                             for cpt in range(max(len(value), len(result))):
292                                 if len(value) > cpt:
293                                     val = value[cpt]
294                                 else:
295                                     val = ''
296                                 if len(result) > cpt:
297                                     rval = result[cpt]
298                                 if len(owners) > cpt:
299                                     if owners[cpt] == 'default':
300                                         _result.append(rval)
301                                         self._cfgimpl_value_owners[name][cpt] = 'default'
302                                     else:
303                                         _result.append(val)
304                                 else:
305                                     _result.append(rval)
306                                     self._cfgimpl_value_owners[name][cpt] = 'default'
307                     else:
308                         # this result **shall not** be a list
309                         if isinstance(result, list):
310                             raise ConfigError('invalid calculated value returned'
311                                 ' for option {0} : shall not be a list'.format(name))
312                         _result = result
313                     if _result != None and not opt_or_descr.validate(_result):
314                         raise ConfigError('invalid calculated value returned'
315                             ' for option {0}'.format(name))
316                     self._cfgimpl_values[name] = _result
317             self._test_mandatory(name, opt_or_descr)
318             # frozen and force default
319             if not opt_or_descr.has_callback() and opt_or_descr.is_forced_on_freeze():
320                 return opt_or_descr.getdefault()
321
322         return self._cfgimpl_values[name]
323
324     def unwrap_from_name(self, name):
325         """convenience method to extract and Option() object from the Config()
326         **and it is slow**: it recursively searches into the namespaces
327
328         :returns: Option()
329         """
330         paths = self.getpaths(allpaths=True)
331         opts = dict([(path, self.unwrap_from_path(path)) for path in paths])
332         all_paths = [p.split(".") for p in self.getpaths()]
333         for pth in all_paths:
334             if name in pth:
335                 return opts[".".join(pth)]
336         raise NotFoundError("name: {0} not found".format(name))
337
338     def unwrap_from_path(self, path):
339         """convenience method to extract and Option() object from the Config()
340         and it is **fast**: finds the option directly in the appropriate
341         namespace
342
343         :returns: Option()
344         """
345         if '.' in path:
346             homeconfig, path = self._cfgimpl_get_home_by_path(path)
347             return getattr(homeconfig._cfgimpl_descr, path)
348         return getattr(self._cfgimpl_descr, path)
349
350     def __delattr__(self, name):
351         "if you use delattr you are responsible for all bad things happening"
352         if name.startswith('_cfgimpl_'):
353             del self.__dict__[name]
354             return
355         self._cfgimpl_value_owners[name] = 'default'
356         opt = getattr(self._cfgimpl_descr, name)
357         if isinstance(opt, OptionDescription):
358             raise AttributeError("can't option subgroup")
359         self._cfgimpl_values[name] = getattr(opt, 'default', None)
360
361     def setoption(self, name, value, who=None):
362         """effectively modifies the value of an Option()
363         (typically called by the __setattr__)
364
365         :param who: is an owner's name
366         who is **not necessarily** a owner, because it cannot be a list
367         :type who: string
368         """
369         child = getattr(self._cfgimpl_descr, name)
370         if type(child) != SymLinkOption:
371             if who == None:
372                 who = self._cfgimpl_owner
373             if child.is_multi():
374                 if type(value) != Multi:
375                     if type(value) == list:
376                         value = Multi(value, self, child)
377                     else:
378                         raise ConfigError("invalid value for option:"
379                                    " {0} that is set to multi".format(name))
380                 newowner = [who for i in range(len(value))]
381             else:
382                 newowner = who
383             child.setoption(self, value, who)
384             if child.is_multi() and value == [] and who != 'default':
385                 child.setowner(self, Owner(who))
386             else:
387                 child.setowner(self, newowner)
388         else:
389             homeconfig = self._cfgimpl_get_toplevel()
390             child.setoption(homeconfig, value, who)
391
392     def set(self, **kwargs):
393         """
394         "do what I mean"-interface to option setting. Searches all paths
395         starting from that config for matches of the optional arguments
396         and sets the found option if the match is not ambiguous.
397         :param kwargs: dict of name strings to values.
398         """
399         all_paths = [p.split(".") for p in self.getpaths(allpaths=True)]
400         for key, value in kwargs.iteritems():
401             key_p = key.split('.')
402             candidates = [p for p in all_paths if p[-len(key_p):] == key_p]
403             if len(candidates) == 1:
404                 name = '.'.join(candidates[0])
405                 homeconfig, name = self._cfgimpl_get_home_by_path(name)
406                 try:
407                     getattr(homeconfig, name)
408                 except MandatoryError:
409                     pass
410                 except Exception, e:
411                     raise e # HiddenOptionError or DisabledOptionError
412                 homeconfig.setoption(name, value, self._cfgimpl_owner)
413             elif len(candidates) > 1:
414                 raise AmbigousOptionError(
415                     'more than one option that ends with %s' % (key, ))
416             else:
417                 raise NoMatchingOptionFound(
418                     'there is no option that matches %s'
419                     ' or the option is hidden or disabled'% (key, ))
420
421     def get(self, name):
422         """
423         same as a find_first() method in a config that has identical names
424         that is : Returns the first item of an option named 'name'
425
426         much like the attribute access way, except that
427         the search for the option is performed recursively in the whole
428         configuration tree.
429         **carefull**: very slow !
430
431         :returns: option value.
432         """
433         paths = self.getpaths(allpaths=True)
434         pathsvalues = []
435         for path in paths:
436             pathname = path.split('.')[-1]
437             if pathname == name:
438                 try:
439                     value = getattr(self, path)
440                     return value
441                 except Exception, e:
442                     raise e
443         raise NotFoundError("option {0} not found in config".format(name))
444
445     def _cfgimpl_get_home_by_path(self, path):
446         """:returns: tuple (config, name)"""
447         path = path.split('.')
448         for step in path[:-1]:
449             self = getattr(self, step)
450         return self, path[-1]
451
452     def _cfgimpl_get_toplevel(self):
453         ":returns: root config"
454         while self._cfgimpl_parent is not None:
455             self = self._cfgimpl_parent
456         return self
457
458     def _cfgimpl_get_path(self):
459         "the path in the attribute access meaning."
460         subpath = []
461         obj = self
462         while obj._cfgimpl_parent is not None:
463             subpath.insert(0, obj._cfgimpl_descr._name)
464             obj = obj._cfgimpl_parent
465         return ".".join(subpath)
466     # ______________________________________________________________________
467     def cfgimpl_previous_value(self, path):
468         "stores the previous value"
469         home, name = self._cfgimpl_get_home_by_path(path)
470         return home._cfgimpl_previous_values[name]
471
472     def get_previous_value(self, name):
473         "for the time being, only the previous Option's value is accessible"
474         return self._cfgimpl_previous_values[name]
475     # ______________________________________________________________________
476     def add_warning(self, warning):
477         "Config implements its own warning pile. Could be useful"
478         self._cfgimpl_get_toplevel()._cfgimpl_warnings.append(warning)
479
480     def get_warnings(self):
481         "Config implements its own warning pile"
482         return self._cfgimpl_get_toplevel()._cfgimpl_warnings
483     # ____________________________________________________________
484     # Config()'s status
485     def cfgimpl_freeze(self):
486         "cannot modify the frozen `Option`'s"
487         rootconfig = self._cfgimpl_get_toplevel()
488         rootconfig._cfgimpl_frozen = True
489         self._cfgimpl_frozen = True
490
491     def cfgimpl_unfreeze(self):
492         "can modify the Options that are frozen"
493         rootconfig = self._cfgimpl_get_toplevel()
494         rootconfig._cfgimpl_frozen = False
495         self._cfgimpl_frozen = False
496
497     def is_frozen(self):
498         "freeze flag at Config level"
499         rootconfig = self._cfgimpl_get_toplevel()
500         return rootconfig._cfgimpl_frozen
501
502     def cfgimpl_read_only(self):
503         "convenience method to freeze, hidde and disable"
504         self.cfgimpl_freeze()
505         rootconfig = self._cfgimpl_get_toplevel()
506         rootconfig.cfgimpl_disable_property('hidden')
507         rootconfig.cfgimpl_enable_property('disabled')
508         rootconfig._cfgimpl_mandatory = True
509
510     def cfgimpl_read_write(self):
511         "convenience method to freeze, hidde and disable"
512         self.cfgimpl_freeze()
513         rootconfig = self._cfgimpl_get_toplevel()
514         rootconfig.cfgimpl_enable_property('hidden')
515         rootconfig.cfgimpl_enable_property('disabled')
516         rootconfig._cfgimpl_mandatory = False
517
518     def cfgimpl_non_mandatory(self):
519         """mandatory at the Config level means that the Config raises an error
520         if a mandatory option is found"""
521         if self._cfgimpl_parent != None:
522             raise MethodCallError("this method root_mandatory shall"
523                                   " not be used with non-root Confit() object")
524         rootconfig = self._cfgimpl_get_toplevel()
525         rootconfig._cfgimpl_mandatory = False
526
527     def cfgimpl_mandatory(self):
528         """mandatory at the Config level means that the Config raises an error
529         if a mandatory option is found"""
530         if self._cfgimpl_parent != None:
531             raise MethodCallError("this method root_mandatory shall"
532                                   " not be used with non-root Confit() object")
533         rootconfig = self._cfgimpl_get_toplevel()
534         rootconfig._cfgimpl_mandatory = True
535
536     def is_mandatory(self):
537         "all mandatory Options shall have a value"
538         rootconfig = self._cfgimpl_get_toplevel()
539         return rootconfig._cfgimpl_mandatory
540     # ____________________________________________________________
541     def getkey(self):
542         return self._cfgimpl_descr.getkey(self)
543
544     def __hash__(self):
545         return hash(self.getkey())
546
547     def __eq__(self, other):
548         "Config comparison"
549         return self.getkey() == other.getkey()
550
551     def __ne__(self, other):
552         "Config comparison"
553         return not self == other
554     # ______________________________________________________________________
555     def __iter__(self):
556         "iteration only on Options (not OptionDescriptions)"
557         for child in self._cfgimpl_descr._children:
558             if isinstance(child, Option):
559                 try:
560                     yield child._name, getattr(self, child._name)
561                 except:
562                     pass # option with properties
563
564     def iter_groups(self, group_type=None):
565         "iteration on OptionDescriptions"
566         if group_type == None:
567             groups = group_types
568         else:
569             if group_type not in group_types:
570                 raise TypeError("Unknown group_type: {0}".format(group_type))
571             groups = [group_type]
572         for child in self._cfgimpl_descr._children:
573             if isinstance(child, OptionDescription):
574                 try:
575                     if child.get_group_type() in groups:
576                         yield child._name, getattr(self, child._name)
577                 except:
578                     pass # hidden, disabled option
579     # ______________________________________________________________________
580     def __str__(self, indent=""):
581         "Config's string representation"
582         lines = []
583         children = [(child._name, child)
584                     for child in self._cfgimpl_descr._children]
585         children.sort()
586         for name, child in children:
587             if self._cfgimpl_value_owners.get(name, None) == 'default':
588                 continue
589             value = getattr(self, name)
590             if isinstance(value, Config):
591                 substr = value.__str__(indent + "    ")
592             else:
593                 substr = "%s    %s = %s" % (indent, name, value)
594             if substr:
595                 lines.append(substr)
596         if indent and not lines:
597             return ''   # hide subgroups with all default values
598         lines.insert(0, "%s[%s]" % (indent, self._cfgimpl_descr._name,))
599         return '\n'.join(lines)
600
601     def getpaths(self, include_groups=False, allpaths=False, mandatory=False):
602         """returns a list of all paths in self, recursively, taking care of
603         the context of properties (hidden/disabled)
604
605         :param include_groups: if true, OptionDescription are included
606         :param allpaths: all the options (event the properties protected ones)
607         :param mandatory: includes the mandatory options
608         :returns: list of all paths
609         """
610         paths = []
611         for path in self._cfgimpl_descr.getpaths(include_groups=include_groups):
612             try:
613                 value = getattr(self, path)
614
615             except MandatoryError:
616                 if mandatory or allpaths:
617                     paths.append(path)
618             except PropertiesOptionError:
619                 if allpaths:
620                     paths.append(path) # option which have properties added
621             else:
622                  paths.append(path)
623         return paths
624
625     def _find(self, bytype, byname, byvalue, byattrs, first):
626         """
627             :param first: return only one option if True, a list otherwise
628         """
629         def _filter_by_attrs():
630             if byattrs is None:
631                 return True
632             for key, value in byattrs.items():
633                 if not hasattr(option, key):
634                     return False
635                 else:
636                     if getattr(option, key) != value:
637                         return False
638                     else:
639                         continue
640             return True
641         def _filter_by_name():
642             if byname is None:
643                 return True
644             pathname = path.split('.')[-1]
645             if pathname == byname:
646                 return True
647             else:
648                 return False
649         def _filter_by_value():
650             if byvalue is None:
651                 return True
652             try:
653                 value = getattr(self, path)
654                 if value == byvalue:
655                     return True
656             except Exception, e: # a property restricts the acces to value
657                 pass
658             return False
659         def _filter_by_type():
660             if bytype is None:
661                 return True
662             if isinstance(option, bytype):
663                 return True
664             return False
665
666         find_results = []
667         paths = self.getpaths(allpaths=True)
668         for path in paths:
669             option = self.unwrap_from_path(path)
670             if not _filter_by_name():
671                 continue
672             if not _filter_by_value():
673                 continue
674             if not _filter_by_type():
675                 continue
676             if not _filter_by_attrs():
677                 continue
678             if first:
679                 return option
680             else:
681                 find_results.append(option)
682         if first:
683             return None
684         else:
685             return find_results
686
687     def find(self, bytype=None, byname=None, byvalue=None, byattrs=None):
688         """
689             finds a list of options recursively in the config
690
691             :param bytype: Option class (BoolOption, StrOption, ...)
692             :param byname: filter by Option._name
693             :param byvalue: filter by the option's value
694             :param byattrs: dict of option attributes (default, callback...)
695             :returns: list of matching Option objects
696         """
697         return self._find(bytype, byname, byvalue, byattrs, first=False)
698
699     def find_first(self, bytype=None, byname=None, byvalue=None, byattrs=None):
700         """
701             finds an option recursively in the config
702
703             :param bytype: Option class (BoolOption, StrOption, ...)
704             :param byname: filter by Option._name
705             :param byvalue: filter by the option's value
706             :param byattrs: dict of option attributes (default, callback...)
707             :returns: list of matching Option objects
708         """
709         return self._find(bytype, byname, byvalue, byattrs, first=True)
710
711 def make_dict(config, flatten=False):
712     """export the whole config into a `dict`
713     :returns: dict of Option's name (or path) and values"""
714     paths = config.getpaths()
715     pathsvalues = []
716     for path in paths:
717         if flatten:
718             pathname = path.split('.')[-1]
719         else:
720             pathname = path
721         try:
722             value = getattr(config, path)
723             pathsvalues.append((pathname, value))
724         except:
725             pass # this just a hidden or disabled option
726     options = dict(pathsvalues)
727     return options
728
729 def mandatory_warnings(config):
730     """convenience function to trace Options that are mandatory and
731     where no value has been set
732
733     :returns: generator of mandatory Option's path
734     """
735     mandatory = config._cfgimpl_get_toplevel()._cfgimpl_mandatory
736     config._cfgimpl_get_toplevel()._cfgimpl_mandatory = True
737     for path in config._cfgimpl_descr.getpaths(include_groups=True):
738         try:
739             value = config._getattr(path, permissive=True)
740         except MandatoryError:
741             yield path
742         except PropertiesOptionError:
743             pass
744     config._cfgimpl_get_toplevel()._cfgimpl_mandatory = mandatory