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