refactor validation
[tiramisu.git] / tiramisu / config.py
1 # -*- coding: utf-8 -*-
2 # Copyright (C) 2012-2013 Team tiramisu (see AUTHORS for all contributors)
3 #
4 # This program is free software: you can redistribute it and/or modify it
5 # under the terms of the GNU Lesser General Public License as published by the
6 # Free Software Foundation, either version 3 of the License, or (at your
7 # option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful, but WITHOUT
10 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
11 # FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License for more
12 # details.
13 #
14 # You should have received a copy of the GNU Lesser General Public License
15 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
16 #
17 # The original `Config` design model is unproudly borrowed from
18 # the rough pypy's guys: http://codespeak.net/svn/pypy/dist/pypy/config/
19 # the whole pypy projet is under MIT licence
20 # ____________________________________________________________
21 "options handler global entry point"
22 import weakref
23 from copy import copy
24
25
26 from tiramisu.error import PropertiesOptionError, ConfigError, ConflictError
27 from tiramisu.option import OptionDescription, Option, SymLinkOption, \
28     DynSymLinkOption
29 from tiramisu.option.baseoption import valid_name
30 from tiramisu.setting import groups, Settings, default_encoding, undefined
31 from tiramisu.storage import get_storages, get_storage, set_storage, \
32     _impl_getstate_setting, get_storages_validation
33 from tiramisu.value import Values, Multi
34 from tiramisu.i18n import _
35
36
37 class SubConfig(object):
38     """Sub configuration management entry.
39     Tree if OptionDescription's responsability. SubConfig are generated
40     on-demand. A Config is also a SubConfig.
41     Root Config is call context below
42     """
43     __slots__ = ('_impl_context', '_impl_descr', '_impl_path')
44
45     def __init__(self, descr, context, subpath=None):
46         """ Configuration option management master class
47
48         :param descr: describes the configuration schema
49         :type descr: an instance of ``option.OptionDescription``
50         :param context: the current root config
51         :type context: `Config`
52         :type subpath: `str` with the path name
53         """
54         # main option description
55         error = False
56         try:
57             if descr is not None and not descr.impl_is_optiondescription():  # pragma: optional cover
58                 error = True
59         except AttributeError:
60             error = True
61         if error:
62             raise TypeError(_('descr must be an optiondescription, not {0}'
63                               ).format(type(descr)))
64         self._impl_descr = descr
65         # sub option descriptions
66         if not isinstance(context, weakref.ReferenceType):  # pragma: optional cover
67             raise ValueError('context must be a Weakref')
68         self._impl_context = context
69         self._impl_path = subpath
70
71     def cfgimpl_reset_cache(self, only_expired=False, only=('values',
72                                                             'settings')):
73         "remove cache (in context)"
74         self._cfgimpl_get_context().cfgimpl_reset_cache(only_expired, only)  # pragma: optional cover
75
76     def cfgimpl_get_home_by_path(self, path, force_permissive=False):
77         """:returns: tuple (config, name)"""
78         path = path.split('.')
79         for step in path[:-1]:
80             self = self.getattr(step,
81                                 force_permissive=force_permissive)
82         return self, path[-1]
83
84     #def __hash__(self):
85     #FIXME
86     #    return hash(self.cfgimpl_get_description().impl_getkey(self))
87
88     #def __eq__(self, other):
89     #FIXME
90     #    "Config's comparison"
91     #    if not isinstance(other, Config):
92     #        return False
93     #    return self.cfgimpl_get_description().impl_getkey(self) == \
94     #        other.cfgimpl_get_description().impl_getkey(other)
95
96     #def __ne__(self, other):
97     #FIXME
98     #    "Config's comparison"
99     #    if not isinstance(other, Config):
100     #        return True
101     #    return not self == other
102
103     # ______________________________________________________________________
104     def __iter__(self, force_permissive=False):
105         """Pythonesque way of parsing group's ordered options.
106         iteration only on Options (not OptionDescriptions)"""
107         for child in self.cfgimpl_get_description()._impl_getchildren(
108                 context=self._cfgimpl_get_context()):
109             if not child.impl_is_optiondescription():
110                 try:
111                     name = child.impl_getname()
112                     yield name, self.getattr(name,
113                                              force_permissive=force_permissive)
114                 except GeneratorExit:  # pragma: optional cover
115                     raise StopIteration
116                 except PropertiesOptionError:  # pragma: optional cover
117                     pass  # option with properties
118
119     def iter_all(self, force_permissive=False):
120         """A way of parsing options **and** groups.
121         iteration on Options and OptionDescriptions."""
122         for child in self.cfgimpl_get_description().impl_getchildren():
123             try:
124                 yield child.impl_getname(), self.getattr(child.impl_getname(),
125                                                          force_permissive=force_permissive)
126             except GeneratorExit:  # pragma: optional cover
127                 raise StopIteration
128             except PropertiesOptionError:  # pragma: optional cover
129                 pass  # option with properties
130
131     def iter_groups(self, group_type=None, force_permissive=False):
132         """iteration on groups objects only.
133         All groups are returned if `group_type` is `None`, otherwise the groups
134         can be filtered by categories (families, or whatever).
135
136         :param group_type: if defined, is an instance of `groups.GroupType`
137                            or `groups.MasterGroupType` that lives in
138                            `setting.groups`
139         """
140         if group_type is not None and not isinstance(group_type,
141                                                      groups.GroupType):  # pragma: optional cover
142             raise TypeError(_("unknown group_type: {0}").format(group_type))
143         for child in self.cfgimpl_get_description()._impl_getchildren(
144                 context=self._cfgimpl_get_context()):
145             if child.impl_is_optiondescription():
146                 try:
147                     if group_type is None or (group_type is not None and
148                                               child.impl_get_group_type()
149                                               == group_type):
150                         name = child.impl_getname()
151                         yield name, self.getattr(name, force_permissive=force_permissive)
152                 except GeneratorExit:  # pragma: optional cover
153                     raise StopIteration
154                 except PropertiesOptionError:  # pragma: optional cover
155                     pass
156     # ______________________________________________________________________
157
158     def __str__(self):
159         "Config's string representation"
160         lines = []
161         for name, grp in self.iter_groups():
162             lines.append("[{0}]".format(name))
163         for name, value in self:
164             try:
165                 lines.append("{0} = {1}".format(name, value))
166             except UnicodeEncodeError:  # pragma: optional cover
167                 lines.append("{0} = {1}".format(name,
168                                                 value.encode(default_encoding)))
169         return '\n'.join(lines)
170
171     __repr__ = __str__
172
173     def _cfgimpl_get_context(self):
174         """context could be None, we need to test it
175         context is None only if all reference to `Config` object is deleted
176         (for example we delete a `Config` and we manipulate a reference to
177         old `SubConfig`, `Values`, `Multi` or `Settings`)
178         """
179         context = self._impl_context()
180         if context is None:  # pragma: optional cover
181             raise ConfigError(_('the context does not exist anymore'))
182         return context
183
184     def cfgimpl_get_description(self):
185         if self._impl_descr is None:  # pragma: optional cover
186             raise ConfigError(_('no option description found for this config'
187                                 ' (may be GroupConfig)'))
188         else:
189             return self._impl_descr
190
191     def cfgimpl_get_settings(self):
192         return self._cfgimpl_get_context()._impl_settings
193
194     def cfgimpl_get_values(self):
195         return self._cfgimpl_get_context()._impl_values
196
197     # ____________________________________________________________
198     # attribute methods
199     def __setattr__(self, name, value):
200         "attribute notation mechanism for the setting of the value of an option"
201         self._setattr(name, value)
202
203     def _setattr(self, name, value, force_permissive=False):
204         if name.startswith('_impl_'):
205             object.__setattr__(self, name, value)
206             return
207         if '.' in name:  # pragma: optional cover
208             homeconfig, name = self.cfgimpl_get_home_by_path(name)
209             return homeconfig._setattr(name, value, force_permissive)
210         context = self._cfgimpl_get_context()
211         child = self.cfgimpl_get_description().__getattr__(name,
212                                                            context=context)
213         if isinstance(child, OptionDescription):
214             raise TypeError(_("can't assign to an OptionDescription"))  # pragma: optional cover
215         elif isinstance(child, SymLinkOption) and \
216                 not isinstance(child, DynSymLinkOption):  # pragma: no dynoptiondescription cover
217             path = context.cfgimpl_get_description().impl_get_path_by_opt(
218                 child._impl_getopt())
219             context._setattr(path, value, force_permissive=force_permissive)
220         else:
221             subpath = self._get_subpath(name)
222             self.cfgimpl_get_values().setitem(child, value, subpath,
223                                               force_permissive=force_permissive)
224
225     def __delattr__(self, name):
226         context = self._cfgimpl_get_context()
227         child = self.cfgimpl_get_description().__getattr__(name, context)
228         self.cfgimpl_get_values().__delitem__(child)
229
230     def __getattr__(self, name):
231         return self.getattr(name)
232
233     def _getattr(self, name, force_permissive=False, validate=True):  # pragma: optional cover
234         """use getattr instead of _getattr
235         """
236         return self.getattr(name, force_permissive, validate)
237
238     def _get_subpath(self, name):
239         if self._impl_path is None:
240             subpath = name
241         else:
242             subpath = self._impl_path + '.' + name
243         return subpath
244
245     def getattr(self, name, force_permissive=False, validate=True):
246         """
247         attribute notation mechanism for accessing the value of an option
248         :param name: attribute name
249         :return: option's value if name is an option name, OptionDescription
250                  otherwise
251         """
252         # attribute access by passing a path,
253         # for instance getattr(self, "creole.general.family.adresse_ip_eth0")
254         if '.' in name:
255             homeconfig, name = self.cfgimpl_get_home_by_path(
256                 name, force_permissive=force_permissive)
257             return homeconfig.getattr(name, force_permissive=force_permissive,
258                                       validate=validate)
259         context = self._cfgimpl_get_context()
260         opt_or_descr = self.cfgimpl_get_description().__getattr__(
261             name, context=context)
262         subpath = self._get_subpath(name)
263         if isinstance(opt_or_descr, DynSymLinkOption):
264             return self.cfgimpl_get_values()._get_cached_item(
265                 opt_or_descr, path=subpath,
266                 validate=validate,
267                 force_permissive=force_permissive)
268         elif isinstance(opt_or_descr, SymLinkOption):  # pragma: no dynoptiondescription cover
269             path = context.cfgimpl_get_description().impl_get_path_by_opt(
270                 opt_or_descr._impl_getopt())
271             return context.getattr(path, validate=validate,
272                                    force_permissive=force_permissive)
273         elif opt_or_descr.impl_is_optiondescription():
274             self.cfgimpl_get_settings().validate_properties(
275                 opt_or_descr, True, False, path=subpath,
276                 force_permissive=force_permissive)
277             return SubConfig(opt_or_descr, self._impl_context, subpath)
278         else:
279             return self.cfgimpl_get_values()._get_cached_item(
280                 opt_or_descr, path=subpath,
281                 validate=validate,
282                 force_permissive=force_permissive)
283
284     def find(self, bytype=None, byname=None, byvalue=undefined, type_='option',
285              check_properties=True, force_permissive=False):
286         """
287             finds a list of options recursively in the config
288
289             :param bytype: Option class (BoolOption, StrOption, ...)
290             :param byname: filter by Option.impl_getname()
291             :param byvalue: filter by the option's value
292             :returns: list of matching Option objects
293         """
294         return self._cfgimpl_get_context()._find(bytype, byname, byvalue,
295                                                  first=False,
296                                                  type_=type_,
297                                                  _subpath=self.cfgimpl_get_path(False),
298                                                  check_properties=check_properties,
299                                                  force_permissive=force_permissive)
300
301     def find_first(self, bytype=None, byname=None, byvalue=undefined,
302                    type_='option', display_error=True, check_properties=True,
303                    force_permissive=False):
304         """
305             finds an option recursively in the config
306
307             :param bytype: Option class (BoolOption, StrOption, ...)
308             :param byname: filter by Option.impl_getname()
309             :param byvalue: filter by the option's value
310             :returns: list of matching Option objects
311         """
312         return self._cfgimpl_get_context()._find(
313             bytype, byname, byvalue, first=True, type_=type_,
314             _subpath=self.cfgimpl_get_path(False), display_error=display_error,
315             check_properties=check_properties,
316             force_permissive=force_permissive)
317
318     def _find(self, bytype, byname, byvalue, first, type_='option',
319               _subpath=None, check_properties=True, display_error=True,
320               force_permissive=False, only_path=undefined,
321               only_option=undefined):
322         """
323         convenience method for finding an option that lives only in the subtree
324
325         :param first: return only one option if True, a list otherwise
326         :return: find list or an exception if nothing has been found
327         """
328
329         def _filter_by_value():
330             if byvalue is undefined:
331                 return True
332             try:
333                 value = self.getattr(path, force_permissive=force_permissive)
334                 if isinstance(value, Multi):
335                     return byvalue in value
336                 else:
337                     return value == byvalue
338             # a property is a restriction upon the access of the value
339             except PropertiesOptionError:  # pragma: optional cover
340                 return False
341
342         if type_ not in ('option', 'path', 'value'):  # pragma: optional cover
343             raise ValueError(_('unknown type_ type {0}'
344                                'for _find').format(type_))
345         find_results = []
346         # if value and/or check_properties are set, need all avalaible option
347         # If first one has no good value or not good property check second one
348         # and so on
349         only_first = first is True and byvalue is None and \
350             check_properties is None
351         if only_path is not undefined:
352             options = [(only_path, only_option)]
353         else:
354             options = self.cfgimpl_get_description().impl_get_options_paths(
355                 bytype, byname, _subpath, only_first,
356                 self._cfgimpl_get_context())
357         for path, option in options:
358             if not _filter_by_value():
359                 continue
360             #remove option with propertyerror, ...
361             if byvalue is undefined and check_properties:
362                 try:
363                     value = self.getattr(path,
364                                          force_permissive=force_permissive)
365                 except PropertiesOptionError:  # pragma: optional cover
366                     # a property restricts the access of the value
367                     continue
368             if type_ == 'value':
369                 retval = value
370             elif type_ == 'path':
371                 retval = path
372             elif type_ == 'option':
373                 retval = option
374             if first:
375                 return retval
376             else:
377                 find_results.append(retval)
378         return self._find_return_results(find_results, display_error)
379
380     def _find_return_results(self, find_results, display_error):
381         if find_results == []:  # pragma: optional cover
382             if display_error:
383                 raise AttributeError(_("no option found in config"
384                                        " with these criteria"))
385             else:
386                 # translation is slow
387                 raise AttributeError()
388         else:
389             return find_results
390
391     def make_dict(self, flatten=False, _currpath=None, withoption=None,
392                   withvalue=undefined, force_permissive=False):
393         """exports the whole config into a `dict`, for example:
394
395         >>> print cfg.make_dict()
396         {'od2.var4': None, 'od2.var5': None, 'od2.var6': None}
397
398
399
400         :param flatten: returns a dict(name=value) instead of a dict(path=value)
401                         ::
402
403                             >>> print cfg.make_dict(flatten=True)
404                             {'var5': None, 'var4': None, 'var6': None}
405
406         :param withoption: returns the options that are present in the very same
407                            `OptionDescription` than the `withoption` itself::
408
409                                 >>> print cfg.make_dict(withoption='var1')
410                                 {'od2.var4': None, 'od2.var5': None,
411                                 'od2.var6': None,
412                                 'od2.var1': u'value',
413                                 'od1.var1': None,
414                                 'od1.var3': None,
415                                 'od1.var2': None}
416
417         :param withvalue: returns the options that have the value `withvalue`
418                           ::
419
420                             >>> print c.make_dict(withoption='var1',
421                                                   withvalue=u'value')
422                             {'od2.var4': None,
423                             'od2.var5': None,
424                             'od2.var6': None,
425                             'od2.var1': u'value'}
426
427         :returns: dict of Option's name (or path) and values
428         """
429         pathsvalues = []
430         if _currpath is None:
431             _currpath = []
432         if withoption is None and withvalue is not undefined:  # pragma: optional cover
433             raise ValueError(_("make_dict can't filtering with value without "
434                                "option"))
435         if withoption is not None:
436             context = self._cfgimpl_get_context()
437             for path in context._find(bytype=None, byname=withoption,
438                                       byvalue=withvalue, first=False,
439                                       type_='path', _subpath=self.cfgimpl_get_path(False),
440                                       force_permissive=force_permissive):
441                 path = '.'.join(path.split('.')[:-1])
442                 opt = context.unwrap_from_path(path, force_permissive=True)
443                 mypath = self.cfgimpl_get_path()
444                 if mypath is not None:
445                     if mypath == path:
446                         withoption = None
447                         withvalue = undefined
448                         break
449                     else:
450                         tmypath = mypath + '.'
451                         if not path.startswith(tmypath):  # pragma: optional cover
452                             raise AttributeError(_('unexpected path {0}, '
453                                                    'should start with {1}'
454                                                    '').format(path, mypath))
455                         path = path[len(tmypath):]
456                 self._make_sub_dict(opt, path, pathsvalues, _currpath, flatten,
457                                     force_permissive=force_permissive)
458         #withoption can be set to None below !
459         if withoption is None:
460             for opt in self.cfgimpl_get_description().impl_getchildren():
461                 path = opt.impl_getname()
462                 self._make_sub_dict(opt, path, pathsvalues, _currpath, flatten,
463                                     force_permissive=force_permissive)
464         if _currpath == []:
465             options = dict(pathsvalues)
466             return options
467         return pathsvalues
468
469     def _make_sub_dict(self, opt, path, pathsvalues, _currpath, flatten,
470                        force_permissive=False):
471         try:
472             if opt.impl_is_optiondescription():
473                 pathsvalues += self.getattr(path,
474                                             force_permissive=force_permissive).make_dict(
475                                                 flatten,
476                                                 _currpath + path.split('.'),
477                                                 force_permissive=force_permissive)
478             else:
479                 value = self.getattr(opt.impl_getname(),
480                                      force_permissive=force_permissive)
481                 if flatten:
482                     name = opt.impl_getname()
483                 else:
484                     name = '.'.join(_currpath + [opt.impl_getname()])
485                 pathsvalues.append((name, value))
486         except PropertiesOptionError:  # pragma: optional cover
487             pass
488
489     def cfgimpl_get_path(self, dyn=True):
490         descr = self.cfgimpl_get_description()
491         if not dyn and descr.impl_is_dynoptiondescription():
492             context_descr = self._cfgimpl_get_context().cfgimpl_get_description()
493             return context_descr.impl_get_path_by_opt(descr._impl_getopt())
494         return self._impl_path
495
496
497 class _CommonConfig(SubConfig):
498     "abstract base class for the Config, GroupConfig and the MetaConfig"
499     __slots__ = ('_impl_values', '_impl_settings', '_impl_meta', '_impl_test')
500
501     def _impl_build_all_caches(self):
502         if not self.cfgimpl_get_description().impl_already_build_caches():
503             self.cfgimpl_get_description().impl_build_cache_consistency()
504             self.cfgimpl_get_description().impl_build_cache_option()
505             self.cfgimpl_get_description().impl_validate_options()
506
507     def read_only(self):
508         "read only is a global config's setting, see `settings.py`"
509         self.cfgimpl_get_settings().read_only()
510
511     def read_write(self):
512         "read write is a global config's setting, see `settings.py`"
513         self.cfgimpl_get_settings().read_write()
514
515     def getowner(self, opt, force_permissive=False):
516         """convenience method to retrieve an option's owner
517         from the config itself
518         """
519         if not isinstance(opt, Option) and \
520                 not isinstance(opt, SymLinkOption) and \
521                 not isinstance(opt, DynSymLinkOption):  # pragma: optional cover
522             raise TypeError(_('opt in getowner must be an option not {0}'
523                               '').format(type(opt)))
524         return self.cfgimpl_get_values().getowner(opt,
525                                                   force_permissive=force_permissive)
526
527     def unwrap_from_path(self, path, force_permissive=False):
528         """convenience method to extract and Option() object from the Config()
529         and it is **fast**: finds the option directly in the appropriate
530         namespace
531
532         :returns: Option()
533         """
534         context = self._cfgimpl_get_context()
535         if '.' in path:
536             homeconfig, path = self.cfgimpl_get_home_by_path(
537                 path, force_permissive=force_permissive)
538             return homeconfig.cfgimpl_get_description().__getattr__(path, context=context)
539         return self.cfgimpl_get_description().__getattr__(path, context=context)
540
541     def cfgimpl_get_path(self, dyn=True):
542         return None
543
544     def cfgimpl_get_meta(self):
545         if self._impl_meta is not None:
546             return self._impl_meta()
547
548     # information
549     def impl_set_information(self, key, value):
550         """updates the information's attribute
551
552         :param key: information's key (ex: "help", "doc"
553         :param value: information's value (ex: "the help string")
554         """
555         self._impl_values.set_information(key, value)
556
557     def impl_get_information(self, key, default=undefined):
558         """retrieves one information's item
559
560         :param key: the item string (ex: "help")
561         """
562         return self._impl_values.get_information(key, default)
563
564     # ----- state
565     def __getstate__(self):
566         if self._impl_meta is not None:
567             raise ConfigError(_('cannot serialize Config with MetaConfig'))  # pragma: optional cover
568         slots = set()
569         for subclass in self.__class__.__mro__:
570             if subclass is not object:
571                 slots.update(subclass.__slots__)
572         slots -= frozenset(['_impl_context', '_impl_meta', '__weakref__'])
573         state = {}
574         for slot in slots:
575             try:
576                 state[slot] = getattr(self, slot)
577             except AttributeError:  # pragma: optional cover
578                 pass
579         storage = self._impl_values._p_._storage
580         if not storage.serializable:
581             raise ConfigError(_('this storage is not serialisable, could be a '
582                               'none persistent storage'))  # pragma: optional cover
583         state['_storage'] = {'session_id': storage.session_id,
584                              'persistent': storage.persistent}
585         state['_impl_setting'] = _impl_getstate_setting()
586         return state
587
588     def __setstate__(self, state):
589         for key, value in state.items():
590             if key not in ['_storage', '_impl_setting']:
591                 setattr(self, key, value)
592         set_storage('config', **state['_impl_setting'])
593         self._impl_context = weakref.ref(self)
594         self._impl_settings.context = weakref.ref(self)
595         self._impl_values.context = weakref.ref(self)
596         storage = get_storage('config', test=self._impl_test, **state['_storage'])
597         self._impl_values._impl_setstate(storage)
598         self._impl_settings._impl_setstate(storage)
599         self._impl_meta = None
600
601     def _gen_fake_values(self):
602         fake_config = Config(self._impl_descr, persistent=False,
603                              force_storages=get_storages_validation())
604         fake_config.cfgimpl_get_values()._p_._values = copy(self.cfgimpl_get_values()._p_.get_modified_values())
605         fake_config.cfgimpl_get_settings()._p_._properties = copy(self.cfgimpl_get_settings()._p_.get_modified_properties())
606         fake_config.cfgimpl_get_settings()._p_._permissives = copy(self.cfgimpl_get_settings()._p_.get_modified_permissives())
607         return fake_config
608
609
610 # ____________________________________________________________
611 class Config(_CommonConfig):
612     "main configuration management entry"
613     __slots__ = ('__weakref__', '_impl_test', '_impl_name')
614
615     def __init__(self, descr, session_id=None, persistent=False,
616                  name=undefined, force_storages=None):
617         """ Configuration option management master class
618
619         :param descr: describes the configuration schema
620         :type descr: an instance of ``option.OptionDescription``
621         :param context: the current root config
622         :type context: `Config`
623         :param session_id: session ID is import with persistent Config to
624         retrieve good session
625         :type session_id: `str`
626         :param persistent: if persistent, don't delete storage when leaving
627         :type persistent: `boolean`
628         """
629         #if force_storages is None:
630         settings, values = get_storages(self, session_id, persistent)
631         #else:
632         #    settings, values = force_storages
633         self._impl_settings = Settings(self, settings)
634         self._impl_values = Values(self, values)
635         super(Config, self).__init__(descr, weakref.ref(self))
636         self._impl_build_all_caches()
637         self._impl_meta = None
638         #undocumented option used only in test script
639         self._impl_test = False
640         if name is undefined:
641             name = 'config'
642             if session_id is not None:
643                 name += session_id
644         if name is not None and not valid_name(name):  # pragma: optional cover
645             raise ValueError(_("invalid name: {0} for config").format(name))
646         self._impl_name = name
647
648     def cfgimpl_reset_cache(self,
649                             only_expired=False,
650                             only=('values', 'settings')):
651         if 'values' in only:
652             self.cfgimpl_get_values().reset_cache(only_expired=only_expired)
653         if 'settings' in only:
654             self.cfgimpl_get_settings().reset_cache(only_expired=only_expired)
655
656     def impl_getname(self):
657         return self._impl_name
658
659     def impl_setname(self, name):
660         self._impl_name = name
661
662
663 class GroupConfig(_CommonConfig):
664     __slots__ = ('__weakref__', '_impl_children', '_impl_name')
665
666     def __init__(self, children, session_id=None, persistent=False,
667                  _descr=None, name=undefined):
668         if not isinstance(children, list):
669             raise ValueError(_("groupconfig's children must be a list"))
670         names = []
671         for child in children:
672             if not isinstance(child, _CommonConfig):
673                 raise ValueError(_("groupconfig's children must be Config, MetaConfig or GroupConfig"))
674             name = child._impl_name
675             if name is None:
676                 raise ValueError(_('name must be set to config before creating groupconfig'))
677             #if name in names:
678             #    raise ValueError(_('config name must be uniq in groupconfig'))
679             names.append(name)
680         if len(names) != len(set(names)):
681             for idx in xrange(1, len(names) + 1):
682                 name = names.pop(0)
683                 if name in names:
684                     raise ConflictError(_('config name must be uniq in '
685                                           'groupconfig for {0}').format(name))
686         self._impl_children = children
687         settings, values = get_storages(self, session_id, persistent)
688         self._impl_settings = Settings(self, settings)
689         self._impl_values = Values(self, values)
690         super(GroupConfig, self).__init__(_descr, weakref.ref(self))
691         self._impl_meta = None
692         #undocumented option used only in test script
693         self._impl_test = False
694         if name is undefined:
695             name = session_id
696         self._impl_name = name
697
698     def cfgimpl_get_children(self):
699         return self._impl_children
700
701     #def cfgimpl_get_context(self):
702     #    "a meta config is a config which has a setting, that is itself"
703     #    return self
704
705     def cfgimpl_reset_cache(self,
706                             only_expired=False,
707                             only=('values', 'settings')):
708         if 'values' in only:
709             self.cfgimpl_get_values().reset_cache(only_expired=only_expired)
710         if 'settings' in only:
711             self.cfgimpl_get_settings().reset_cache(only_expired=only_expired)
712         for child in self._impl_children:
713             child.cfgimpl_reset_cache(only_expired=only_expired, only=only)
714
715     def set_value(self, path, value):
716         """Setattr not in current GroupConfig, but in each children
717         """
718         for child in self._impl_children:
719             try:
720                 if isinstance(child, MetaConfig):
721                     child.set_value(path, value, only_config=True)
722                 elif isinstance(child, GroupConfig):
723                     child.set_value(path, value)
724                 else:
725                     setattr(child, path, value)
726             except PropertiesOptionError:
727                 pass
728
729     def find_firsts(self, byname=None, bypath=undefined, byoption=undefined,
730                     byvalue=undefined, display_error=True, _sub=False,
731                     check_properties=True):
732         """Find first not in current GroupConfig, but in each children
733         """
734         ret = []
735
736         #if MetaConfig, all children have same OptionDescription in context
737         #so search only one time the option for all children
738         if bypath is undefined and byname is not None and \
739                 isinstance(self, MetaConfig):
740             bypath = self._find(bytype=None, byvalue=undefined, byname=byname,
741                                 first=True, type_='path',
742                                 check_properties=None,
743                                 display_error=display_error)
744             byname = None
745             byoption = self.cfgimpl_get_description(
746             ).impl_get_opt_by_path(bypath)
747
748         for child in self._impl_children:
749             try:
750                 if isinstance(child, GroupConfig):
751                     ret.extend(child.find_firsts(byname=byname, bypath=bypath,
752                                                  byoption=byoption,
753                                                  byvalue=byvalue,
754                                                  check_properties=check_properties,
755                                                  display_error=False,
756                                                  _sub=True))
757                 else:
758                     child._find(None, byname, byvalue, first=True,
759                                 type_='path', display_error=False,
760                                 check_properties=check_properties,
761                                 only_path=bypath, only_option=byoption)
762                     ret.append(child)
763             except AttributeError:
764                 pass
765         if _sub:
766             return ret
767         else:
768             return GroupConfig(self._find_return_results(ret, display_error))
769
770     def __repr__(self):
771         return object.__repr__(self)
772
773     def __str__(self):
774         ret = ''
775         for child in self._impl_children:
776             ret += '({0})\n'.format(child._impl_name)
777         try:
778             ret += super(GroupConfig, self).__str__()
779         except ConfigError:
780             pass
781         return ret
782
783     def getattr(self, name, force_permissive=False, validate=True):
784         for child in self._impl_children:
785             if name == child._impl_name:
786                 return child
787         return super(GroupConfig, self).getattr(name, force_permissive,
788                                                 validate)
789
790
791 class MetaConfig(GroupConfig):
792     __slots__ = tuple()
793
794     def __init__(self, children, session_id=None, persistent=False,
795                  name=undefined):
796         descr = None
797         for child in children:
798             if not isinstance(child, _CommonConfig):
799                 raise TypeError(_("metaconfig's children "
800                                   "should be config, not {0}"
801                                   ).format(type(child)))
802             if child.cfgimpl_get_meta() is not None:
803                 raise ValueError(_("child has already a metaconfig's"))
804             if descr is None:
805                 descr = child.cfgimpl_get_description()
806             elif not descr is child.cfgimpl_get_description():
807                 raise ValueError(_('all config in metaconfig must '
808                                    'have the same optiondescription'))
809             child._impl_meta = weakref.ref(self)
810
811         super(MetaConfig, self).__init__(children, session_id, persistent,
812                                          descr, name)
813
814     def set_value(self, path, value, force_default=False,
815                   force_dont_change_value=False, force_default_if_same=False,
816                   only_config=False):
817         if only_config:
818             if force_default or force_default_if_same or force_dont_change_value:
819                 raise ValueError(_('force_default, force_default_if_same or '
820                                    'force_dont_change_value cannot be set with'
821                                    ' only_config'))
822             return super(MetaConfig, self).set_value(path, value)
823         if force_default or force_default_if_same or force_dont_change_value:
824             if force_default and force_dont_change_value:
825                 raise ValueError(_('force_default and force_dont_change_value'
826                                    ' cannot be set together'))
827             opt = self.cfgimpl_get_description().impl_get_opt_by_path(path)
828             for child in self._impl_children:
829                 if force_default_if_same or force_default:
830                     if force_default_if_same:
831                         if not child.cfgimpl_get_values()._contains(path):
832                             child_value = undefined
833                         else:
834                             child_value = child.getattr(path)
835                     if force_default or value == child_value:
836                         child.cfgimpl_get_values().reset(opt, path=path,
837                                                          validate=False)
838                         continue
839                 if force_dont_change_value:
840                     child_value = child.getattr(path)
841                     if value != child_value:
842                         setattr(child, path, child_value)
843
844         setattr(self, path, value)