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