409d6eb57f1af1c4a71ff381687a5c1ea3a3ddcd
[tiramisu.git] / tiramisu / option.py
1 # -*- coding: utf-8 -*-
2 "option types and option description"
3 # Copyright (C) 2012-2013 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 import re
24 import sys
25 from copy import copy, deepcopy
26 from types import FunctionType
27 from IPy import IP
28 import warnings
29
30 from tiramisu.error import ConfigError, ConflictError, ValueWarning
31 from tiramisu.setting import groups, multitypes
32 from tiramisu.i18n import _
33 from tiramisu.autolib import carry_out_calculation
34
35 name_regexp = re.compile(r'^\d+')
36 forbidden_names = ('iter_all', 'iter_group', 'find', 'find_first',
37                    'make_dict', 'unwrap_from_path', 'read_only',
38                    'read_write', 'getowner', 'set_contexts')
39
40
41 def valid_name(name):
42     "an option's name is a str and does not start with 'impl' or 'cfgimpl'"
43     if not isinstance(name, str):
44         return False
45     if re.match(name_regexp, name) is None and not name.startswith('_') \
46             and name not in forbidden_names \
47             and not name.startswith('impl_') \
48             and not name.startswith('cfgimpl_'):
49         return True
50     else:
51         return False
52 #____________________________________________________________
53 #
54
55
56 class BaseOption(object):
57     """This abstract base class stands for attribute access
58     in options that have to be set only once, it is of course done in the
59     __setattr__ method
60     """
61     __slots__ = ('_name', '_requires', '_properties', '_readonly',
62                  '_calc_properties', '_impl_informations',
63                  '_state_readonly', '_state_requires', '_stated')
64
65     def __init__(self, name, doc, requires, properties):
66         if not valid_name(name):
67             raise ValueError(_("invalid name: {0} for option").format(name))
68         self._name = name
69         self._impl_informations = {}
70         self.impl_set_information('doc', doc)
71         self._calc_properties, self._requires = validate_requires_arg(
72             requires, self._name)
73         if properties is None:
74             properties = tuple()
75         if not isinstance(properties, tuple):
76             raise TypeError(_('invalid properties type {0} for {1},'
77                             ' must be a tuple').format(
78                                 type(properties),
79                                 self._name))
80         if self._calc_properties is not None and properties is not tuple():
81             set_forbidden_properties = set(properties) & self._calc_properties
82             if set_forbidden_properties != frozenset():
83                 raise ValueError('conflict: properties already set in '
84                                  'requirement {0}'.format(
85                                      list(set_forbidden_properties)))
86         self._properties = properties  # 'hidden', 'disabled'...
87
88     def __setattr__(self, name, value):
89         """set once and only once some attributes in the option,
90         like `_name`. `_name` cannot be changed one the option and
91         pushed in the :class:`tiramisu.option.OptionDescription`.
92
93         if the attribute `_readonly` is set to `True`, the option is
94         "frozen" (which has noting to do with the high level "freeze"
95         propertie or "read_only" property)
96         """
97         if not name.startswith('_state') and not name.startswith('_cache'):
98             is_readonly = False
99             # never change _name
100             if name == '_name':
101                 try:
102                     self._name
103                     #so _name is already set
104                     is_readonly = True
105                 except:
106                     pass
107             elif name != '_readonly':
108                 try:
109                     if self._readonly is True:
110                         is_readonly = True
111                 except AttributeError:
112                     self._readonly = False
113             if is_readonly:
114                 raise AttributeError(_("'{0}' ({1}) object attribute '{2}' is"
115                                        " read-only").format(
116                                            self.__class__.__name__,
117                                            self._name,
118                                            name))
119         object.__setattr__(self, name, value)
120
121     # information
122     def impl_set_information(self, key, value):
123         """updates the information's attribute
124         (which is a dictionary)
125
126         :param key: information's key (ex: "help", "doc"
127         :param value: information's value (ex: "the help string")
128         """
129         self._impl_informations[key] = value
130
131     def impl_get_information(self, key, default=None):
132         """retrieves one information's item
133
134         :param key: the item string (ex: "help")
135         """
136         if key in self._impl_informations:
137             return self._impl_informations[key]
138         elif default is not None:
139             return default
140         else:
141             raise ValueError(_("information's item not found: {0}").format(
142                 key))
143
144     def _impl_convert_requires(self, descr, load=False):
145         """export of the requires during the serialization process
146
147         :type descr: :class:`tiramisu.option.OptionDescription`
148         :param load: `True` if we are at the init of the option description
149         :type load: bool
150         """
151         if not load and self._requires is None:
152             self._state_requires = None
153         elif load and self._state_requires is None:
154             self._requires = None
155             del(self._state_requires)
156         else:
157             if load:
158                 _requires = self._state_requires
159             else:
160                 _requires = self._requires
161             new_value = []
162             for requires in _requires:
163                 new_requires = []
164                 for require in requires:
165                     if load:
166                         new_require = [descr.impl_get_opt_by_path(require[0])]
167                     else:
168                         new_require = [descr.impl_get_path_by_opt(require[0])]
169                     new_require.extend(require[1:])
170                     new_requires.append(tuple(new_require))
171                 new_value.append(tuple(new_requires))
172             if load:
173                 del(self._state_requires)
174                 self._requires = new_value
175             else:
176                 self._state_requires = new_value
177
178     # serialize
179     def _impl_getstate(self, descr):
180         """the under the hood stuff that need to be done
181         before the serialization.
182
183         :param descr: the parent :class:`tiramisu.option.OptionDescription`
184         """
185         self._stated = True
186         for func in dir(self):
187             if func.startswith('_impl_convert_'):
188                 getattr(self, func)(descr)
189         self._state_readonly = self._readonly
190
191     def __getstate__(self, stated=True):
192         """special method to enable the serialization with pickle
193         Usualy, a `__getstate__` method does'nt need any parameter,
194         but somme under the hood stuff need to be done before this action
195
196         :parameter stated: if stated is `True`, the serialization protocol
197                            can be performed, not ready yet otherwise
198         :parameter type: bool
199         """
200         try:
201             self._stated
202         except AttributeError:
203             raise SystemError(_('cannot serialize Option, '
204                                 'only in OptionDescription'))
205         slots = set()
206         for subclass in self.__class__.__mro__:
207             if subclass is not object:
208                 slots.update(subclass.__slots__)
209         slots -= frozenset(['_cache_paths', '_cache_consistencies',
210                             '__weakref__'])
211         states = {}
212         for slot in slots:
213             # remove variable if save variable converted
214             # in _state_xxxx variable
215             if '_state' + slot not in slots:
216                 if slot.startswith('_state'):
217                     # should exists
218                     states[slot] = getattr(self, slot)
219                     # remove _state_xxx variable
220                     self.__delattr__(slot)
221                 else:
222                     try:
223                         states[slot] = getattr(self, slot)
224                     except AttributeError:
225                         pass
226         if not stated:
227             del(states['_stated'])
228         return states
229
230     # unserialize
231     def _impl_setstate(self, descr):
232         """the under the hood stuff that need to be done
233         before the serialization.
234
235         :type descr: :class:`tiramisu.option.OptionDescription`
236         """
237         for func in dir(self):
238             if func.startswith('_impl_convert_'):
239                 getattr(self, func)(descr, load=True)
240         try:
241             self._readonly = self._state_readonly
242             del(self._state_readonly)
243             del(self._stated)
244         except AttributeError:
245             pass
246
247     def __setstate__(self, state):
248         """special method that enables us to serialize (pickle)
249
250         Usualy, a `__setstate__` method does'nt need any parameter,
251         but somme under the hood stuff need to be done before this action
252
253         :parameter state: a dict is passed to the loads, it is the attributes
254                           of the options object
255         :type state: dict
256         """
257         for key, value in state.items():
258             setattr(self, key, value)
259
260
261 class Option(BaseOption):
262     """
263     Abstract base class for configuration option's.
264
265     Reminder: an Option object is **not** a container for the value.
266     """
267     __slots__ = ('_multi', '_validator', '_default_multi', '_default',
268                  '_state_callback', '_callback', '_multitype',
269                  '_consistencies', '_warnings_only', '_master_slaves',
270                  '_state_consistencies', '__weakref__')
271     _empty = ''
272
273     def __init__(self, name, doc, default=None, default_multi=None,
274                  requires=None, multi=False, callback=None,
275                  callback_params=None, validator=None, validator_params=None,
276                  properties=None, warnings_only=False):
277         """
278         :param name: the option's name
279         :param doc: the option's description
280         :param default: specifies the default value of the option,
281                         for a multi : ['bla', 'bla', 'bla']
282         :param default_multi: 'bla' (used in case of a reset to default only at
283                         a given index)
284         :param requires: is a list of names of options located anywhere
285                          in the configuration.
286         :param multi: if true, the option's value is a list
287         :param callback: the name of a function. If set, the function's output
288                          is responsible of the option's value
289         :param callback_params: the callback's parameter
290         :param validator: the name of a function which stands for a custom
291                           validation of the value
292         :param validator_params: the validator's parameters
293         :param properties: tuple of default properties
294         :param warnings_only: _validator and _consistencies don't raise if True
295                              Values()._warning contain message
296
297         """
298         super(Option, self).__init__(name, doc, requires, properties)
299         self._multi = multi
300         if validator is not None:
301             validate_callback(validator, validator_params, 'validator')
302             self._validator = (validator, validator_params)
303         else:
304             self._validator = None
305         if not self._multi and default_multi is not None:
306             raise ValueError(_("a default_multi is set whereas multi is False"
307                              " in option: {0}").format(name))
308         if default_multi is not None:
309             try:
310                 self._validate(default_multi)
311             except ValueError as err:
312                 raise ValueError(_("invalid default_multi value {0} "
313                                    "for option {1}: {2}").format(
314                                        str(default_multi), name, err))
315         if callback is not None and (default is not None or
316                                      default_multi is not None):
317             raise ValueError(_("default value not allowed if option: {0} "
318                              "is calculated").format(name))
319         if callback is None and callback_params is not None:
320             raise ValueError(_("params defined for a callback function but "
321                              "no callback defined"
322                              " yet for option {0}").format(name))
323         if callback is not None:
324             validate_callback(callback, callback_params, 'callback')
325             self._callback = (callback, callback_params)
326         else:
327             self._callback = None
328         if self._multi:
329             if default is None:
330                 default = []
331             self._multitype = multitypes.default
332             self._default_multi = default_multi
333         self._warnings_only = warnings_only
334         self.impl_validate(default)
335         self._default = default
336         self._consistencies = None
337
338     def _launch_consistency(self, func, option, value, context, index,
339                             all_cons_opts):
340         """Launch consistency now
341
342         :param func: function name, this name should start with _cons_
343         :type func: `str`
344         :param option: option that value is changing
345         :type option: `tiramisu.option.Option`
346         :param value: new value of this option
347         :param context: Config's context, if None, check default value instead
348         :type context: `tiramisu.config.Config`
349         :param index: only for multi option, consistency should be launch for
350                       specified index
351         :type index: `int`
352         :param all_cons_opts: all options concerne by this consistency
353         :type all_cons_opts: `list` of `tiramisu.option.Option`
354         """
355         if context is not None:
356             descr = context.cfgimpl_get_description()
357         #option is also in all_cons_opts
358         if option not in all_cons_opts:
359             raise ConfigError(_('option not in all_cons_opts'))
360
361         all_cons_vals = []
362         for opt in all_cons_opts:
363             #get value
364             if option == opt:
365                 opt_value = value
366             else:
367                 #if context, calculate value, otherwise get default value
368                 if context is not None:
369                     opt_value = context._getattr(
370                         descr.impl_get_path_by_opt(opt), validate=False)
371                 else:
372                     opt_value = opt.impl_getdefault()
373
374             #append value
375             if not self.impl_is_multi() or option == opt:
376                 all_cons_vals.append(opt_value)
377             else:
378                 #value is not already set, could be higher index
379                 try:
380                     all_cons_vals.append(opt_value[index])
381                 except IndexError:
382                     #so return if no value
383                     return
384         getattr(self, func)(all_cons_opts, all_cons_vals)
385
386     def impl_validate(self, value, context=None, validate=True,
387                       force_index=None):
388         """
389         :param value: the option's value
390         :param context: Config's context
391         :type context: :class:`tiramisu.config.Config`
392         :param validate: if true enables ``self._validator`` validation
393         :type validate: boolean
394         :param force_no_multi: if multi, value has to be a list
395                                not if force_no_multi is True
396         :type force_no_multi: boolean
397         """
398         if not validate:
399             return
400
401         def val_validator(val):
402             if self._validator is not None:
403                 if self._validator[1] is not None:
404                     validator_params = deepcopy(self._validator[1])
405                     if '' in validator_params:
406                         lst = list(validator_params[''])
407                         lst.insert(0, val)
408                         validator_params[''] = tuple(lst)
409                     else:
410                         validator_params[''] = (val,)
411                 else:
412                     validator_params = {'': (val,)}
413                 # Raise ValueError if not valid
414                 carry_out_calculation(self, config=context,
415                                       callback=self._validator[0],
416                                       callback_params=validator_params)
417
418         def do_validation(_value, _index=None):
419             if _value is None:
420                 return
421             # option validation
422             try:
423                 self._validate(_value)
424             except ValueError as err:
425                 raise ValueError(_('invalid value for option {0}: {1}'
426                                    '').format(self._name, err))
427             try:
428                 # valid with self._validator
429                 val_validator(_value)
430                 # if not context launch consistency validation
431                 if context is not None:
432                     descr._valid_consistency(self, _value, context, _index)
433                 self._second_level_validation(_value)
434             except ValueError as err:
435                 msg = _("invalid value for option {0}: {1}").format(
436                     self._name, err)
437                 if self._warnings_only:
438                     warnings.warn_explicit(ValueWarning(msg, self),
439                                            ValueWarning,
440                                            self.__class__.__name__, 0)
441                 else:
442                     raise ValueError(msg)
443
444         # generic calculation
445         if context is not None:
446             descr = context.cfgimpl_get_description()
447
448         if not self._multi or force_index is not None:
449             do_validation(value, force_index)
450         else:
451             if not isinstance(value, list):
452                 raise ValueError(_("invalid value {0} for option {1} which must be a list").format(value, self._name))
453             for index, val in enumerate(value):
454                 do_validation(val, index)
455
456     def impl_getdefault(self):
457         "accessing the default value"
458         return self._default
459
460     def impl_getdefault_multi(self):
461         "accessing the default value for a multi"
462         return self._default_multi
463
464     def impl_get_multitype(self):
465         return self._multitype
466
467     def impl_get_master_slaves(self):
468         return self._master_slaves
469
470     def impl_is_empty_by_default(self):
471         "no default value has been set yet"
472         if ((not self.impl_is_multi() and self._default is None) or
473                 (self.impl_is_multi() and (self._default == []
474                                            or None in self._default))):
475             return True
476         return False
477
478     def impl_getdoc(self):
479         "accesses the Option's doc"
480         return self.impl_get_information('doc')
481
482     def impl_has_callback(self):
483         "to know if a callback has been defined or not"
484         if self._callback is None:
485             return False
486         else:
487             return True
488
489     def impl_getkey(self, value):
490         return value
491
492     def impl_is_multi(self):
493         return self._multi
494
495     def impl_add_consistency(self, func, *other_opts):
496         """Add consistency means that value will be validate with other_opts
497         option's values.
498
499         :param func: function's name
500         :type func: `str`
501         :param other_opts: options used to validate value
502         :type other_opts: `list` of `tiramisu.option.Option`
503         """
504         if self._consistencies is None:
505             self._consistencies = []
506         for opt in other_opts:
507             if not isinstance(opt, Option):
508                 raise ConfigError(_('consistency should be set with an option'))
509             if self is opt:
510                 raise ConfigError(_('cannot add consistency with itself'))
511             if self.impl_is_multi() != opt.impl_is_multi():
512                 raise ConfigError(_('every options in consistency should be '
513                                     'multi or none'))
514         func = '_cons_{0}'.format(func)
515         all_cons_opts = tuple([self] + list(other_opts))
516         value = self.impl_getdefault()
517         if value is not None:
518             if self.impl_is_multi():
519                 for idx, val in enumerate(value):
520                     self._launch_consistency(func, self, val, None,
521                                              idx, all_cons_opts)
522             else:
523                 self._launch_consistency(func, self, value, None,
524                                          None, all_cons_opts)
525         self._consistencies.append((func, all_cons_opts))
526         self.impl_validate(self.impl_getdefault())
527
528     def _cons_not_equal(self, opts, vals):
529         for idx_inf, val_inf in enumerate(vals):
530             for idx_sup, val_sup in enumerate(vals[idx_inf + 1:]):
531                 if val_inf == val_sup is not None:
532                     raise ValueError(_("same value for {0} and {1}").format(
533                         opts[idx_inf]._name, opts[idx_inf + idx_sup + 1]._name))
534
535     def _impl_convert_callbacks(self, descr, load=False):
536         if not load and self._callback is None:
537             self._state_callback = None
538         elif load and self._state_callback is None:
539             self._callback = None
540             del(self._state_callback)
541         else:
542             if load:
543                 callback, callback_params = self._state_callback
544             else:
545                 callback, callback_params = self._callback
546             if callback_params is not None:
547                 cllbck_prms = {}
548                 for key, values in callback_params.items():
549                     vls = []
550                     for value in values:
551                         if isinstance(value, tuple):
552                             if load:
553                                 value = (descr.impl_get_opt_by_path(value[0]),
554                                          value[1])
555                             else:
556                                 value = (descr.impl_get_path_by_opt(value[0]),
557                                          value[1])
558                         vls.append(value)
559                     cllbck_prms[key] = tuple(vls)
560             else:
561                 cllbck_prms = None
562
563             if load:
564                 del(self._state_callback)
565                 self._callback = (callback, cllbck_prms)
566             else:
567                 self._state_callback = (callback, cllbck_prms)
568
569     # serialize/unserialize
570     def _impl_convert_consistencies(self, descr, load=False):
571         """during serialization process, many things have to be done.
572         one of them is the localisation of the options.
573         The paths are set once for all.
574
575         :type descr: :class:`tiramisu.option.OptionDescription`
576         :param load: `True` if we are at the init of the option description
577         :type load: bool
578         """
579         if not load and self._consistencies is None:
580             self._state_consistencies = None
581         elif load and self._state_consistencies is None:
582             self._consistencies = None
583             del(self._state_consistencies)
584         else:
585             if load:
586                 consistencies = self._state_consistencies
587             else:
588                 consistencies = self._consistencies
589             if isinstance(consistencies, list):
590                 new_value = []
591                 for consistency in consistencies:
592                     values = []
593                     for obj in consistency[1]:
594                         if load:
595                             values.append(descr.impl_get_opt_by_path(obj))
596                         else:
597                             values.append(descr.impl_get_path_by_opt(obj))
598                     new_value.append((consistency[0], tuple(values)))
599
600             else:
601                 new_value = {}
602                 for key, _consistencies in consistencies.items():
603                     new_value[key] = []
604                     for key_cons, _cons in _consistencies:
605                         _list_cons = []
606                         for _con in _cons:
607                             if load:
608                                 _list_cons.append(
609                                     descr.impl_get_opt_by_path(_con))
610                             else:
611                                 _list_cons.append(
612                                     descr.impl_get_path_by_opt(_con))
613                         new_value[key].append((key_cons, tuple(_list_cons)))
614             if load:
615                 del(self._state_consistencies)
616                 self._consistencies = new_value
617             else:
618                 self._state_consistencies = new_value
619
620     def _second_level_validation(self, value):
621         pass
622
623
624 class ChoiceOption(Option):
625     """represents a choice out of several objects.
626
627     The option can also have the value ``None``
628     """
629
630     __slots__ = ('_values', '_open_values')
631     _opt_type = 'string'
632
633     def __init__(self, name, doc, values, default=None, default_multi=None,
634                  requires=None, multi=False, callback=None,
635                  callback_params=None, open_values=False, validator=None,
636                  validator_params=None, properties=None, warnings_only=False):
637         """
638         :param values: is a list of values the option can possibly take
639         """
640         if not isinstance(values, tuple):
641             raise TypeError(_('values must be a tuple for {0}').format(name))
642         self._values = values
643         if open_values not in (True, False):
644             raise TypeError(_('open_values must be a boolean for '
645                             '{0}').format(name))
646         self._open_values = open_values
647         super(ChoiceOption, self).__init__(name, doc, default=default,
648                                            default_multi=default_multi,
649                                            callback=callback,
650                                            callback_params=callback_params,
651                                            requires=requires,
652                                            multi=multi,
653                                            validator=validator,
654                                            validator_params=validator_params,
655                                            properties=properties,
656                                            warnings_only=warnings_only)
657
658     def impl_get_values(self):
659         return self._values
660
661     def impl_is_openvalues(self):
662         return self._open_values
663
664     def _validate(self, value):
665         if not self.impl_is_openvalues() and not value in self.impl_get_values():
666             raise ValueError(_('value {0} is not permitted, '
667                                'only {1} is allowed'
668                                '').format(value, self._values))
669
670
671 class BoolOption(Option):
672     "represents a choice between ``True`` and ``False``"
673     __slots__ = tuple()
674     _opt_type = 'bool'
675
676     def _validate(self, value):
677         if not isinstance(value, bool):
678             raise ValueError(_('invalid boolean'))
679
680
681 class IntOption(Option):
682     "represents a choice of an integer"
683     __slots__ = tuple()
684     _opt_type = 'int'
685
686     def _validate(self, value):
687         if not isinstance(value, int):
688             raise ValueError(_('invalid integer'))
689
690
691 class FloatOption(Option):
692     "represents a choice of a floating point number"
693     __slots__ = tuple()
694     _opt_type = 'float'
695
696     def _validate(self, value):
697         if not isinstance(value, float):
698             raise ValueError(_('invalid float'))
699
700
701 class StrOption(Option):
702     "represents the choice of a string"
703     __slots__ = tuple()
704     _opt_type = 'string'
705
706     def _validate(self, value):
707         if not isinstance(value, str):
708             raise ValueError(_('invalid string'))
709
710
711 if sys.version_info[0] >= 3:
712     #UnicodeOption is same as StrOption in python 3+
713     class UnicodeOption(StrOption):
714         __slots__ = tuple()
715         pass
716 else:
717     class UnicodeOption(Option):
718         "represents the choice of a unicode string"
719         __slots__ = tuple()
720         _opt_type = 'unicode'
721         _empty = u''
722
723         def _validate(self, value):
724             if not isinstance(value, unicode):
725                 raise ValueError(_('invalid unicode'))
726
727
728 class SymLinkOption(BaseOption):
729     __slots__ = ('_name', '_opt', '_state_opt')
730     _opt_type = 'symlink'
731     #not return _opt consistencies
732     _consistencies = None
733
734     def __init__(self, name, opt):
735         self._name = name
736         if not isinstance(opt, Option):
737             raise ValueError(_('malformed symlinkoption '
738                                'must be an option '
739                                'for symlink {0}').format(name))
740         self._opt = opt
741         self._readonly = True
742
743     def __getattr__(self, name):
744         if name in ('_name', '_opt', '_opt_type', '_readonly'):
745             return object.__getattr__(self, name)
746         else:
747             return getattr(self._opt, name)
748
749     def _impl_getstate(self, descr):
750         super(SymLinkOption, self)._impl_getstate(descr)
751         self._state_opt = descr.impl_get_path_by_opt(self._opt)
752
753     def _impl_setstate(self, descr):
754         self._opt = descr.impl_get_opt_by_path(self._state_opt)
755         del(self._state_opt)
756         super(SymLinkOption, self)._impl_setstate(descr)
757
758
759 class IPOption(Option):
760     "represents the choice of an ip"
761     __slots__ = ('_private_only', '_allow_reserved')
762     _opt_type = 'ip'
763
764     def __init__(self, name, doc, default=None, default_multi=None,
765                  requires=None, multi=False, callback=None,
766                  callback_params=None, validator=None, validator_params=None,
767                  properties=None, private_only=False, allow_reserved=False,
768                  warnings_only=False):
769         self._private_only = private_only
770         self._allow_reserved = allow_reserved
771         super(IPOption, self).__init__(name, doc, default=default,
772                                        default_multi=default_multi,
773                                        callback=callback,
774                                        callback_params=callback_params,
775                                        requires=requires,
776                                        multi=multi,
777                                        validator=validator,
778                                        validator_params=validator_params,
779                                        properties=properties,
780                                        warnings_only=warnings_only)
781
782     def _validate(self, value):
783         # sometimes an ip term starts with a zero
784         # but this does not fit in some case, for example bind does not like it
785         try:
786             for val in value.split('.'):
787                 if val.startswith("0") and len(val) > 1:
788                     raise ValueError(_('invalid IP'))
789         except AttributeError:
790             #if integer for example
791             raise ValueError(_('invalid IP'))
792         # 'standard' validation
793         try:
794             IP('{0}/32'.format(value))
795         except ValueError:
796             raise ValueError(_('invalid IP'))
797
798     def _second_level_validation(self, value):
799         ip = IP('{0}/32'.format(value))
800         if not self._allow_reserved and ip.iptype() == 'RESERVED':
801             raise ValueError(_("invalid IP, mustn't not be in reserved class"))
802         if self._private_only and not ip.iptype() == 'PRIVATE':
803             raise ValueError(_("invalid IP, must be in private class"))
804
805
806 class PortOption(Option):
807     """represents the choice of a port
808     The port numbers are divided into three ranges:
809     the well-known ports,
810     the registered ports,
811     and the dynamic or private ports.
812     You can actived this three range.
813     Port number 0 is reserved and can't be used.
814     see: http://en.wikipedia.org/wiki/Port_numbers
815     """
816     __slots__ = ('_allow_range', '_allow_zero', '_min_value', '_max_value')
817     _opt_type = 'port'
818
819     def __init__(self, name, doc, default=None, default_multi=None,
820                  requires=None, multi=False, callback=None,
821                  callback_params=None, validator=None, validator_params=None,
822                  properties=None, allow_range=False, allow_zero=False,
823                  allow_wellknown=True, allow_registred=True,
824                  allow_private=False, warnings_only=False):
825         self._allow_range = allow_range
826         self._min_value = None
827         self._max_value = None
828         ports_min = [0, 1, 1024, 49152]
829         ports_max = [0, 1023, 49151, 65535]
830         is_finally = False
831         for index, allowed in enumerate([allow_zero,
832                                          allow_wellknown,
833                                          allow_registred,
834                                          allow_private]):
835             if self._min_value is None:
836                 if allowed:
837                     self._min_value = ports_min[index]
838             elif not allowed:
839                 is_finally = True
840             elif allowed and is_finally:
841                 raise ValueError(_('inconsistency in allowed range'))
842             if allowed:
843                 self._max_value = ports_max[index]
844
845         if self._max_value is None:
846             raise ValueError(_('max value is empty'))
847
848         super(PortOption, self).__init__(name, doc, default=default,
849                                          default_multi=default_multi,
850                                          callback=callback,
851                                          callback_params=callback_params,
852                                          requires=requires,
853                                          multi=multi,
854                                          validator=validator,
855                                          validator_params=validator_params,
856                                          properties=properties,
857                                          warnings_only=warnings_only)
858
859     def _validate(self, value):
860         if self._allow_range and ":" in str(value):
861             value = str(value).split(':')
862             if len(value) != 2:
863                 raise ValueError(_('invalid part, range must have two values '
864                                  'only'))
865             if not value[0] < value[1]:
866                 raise ValueError(_('invalid port, first port in range must be'
867                                  ' smaller than the second one'))
868         else:
869             value = [value]
870
871         for val in value:
872             try:
873                 if not self._min_value <= int(val) <= self._max_value:
874                     raise ValueError(_('invalid port, must be an between {0} '
875                                      'and {1}').format(self._min_value,
876                                                       self._max_value))
877             except ValueError:
878                 raise ValueError(_('invalid port'))
879
880
881 class NetworkOption(Option):
882     "represents the choice of a network"
883     __slots__ = tuple()
884     _opt_type = 'network'
885
886     def _validate(self, value):
887         try:
888             IP(value)
889         except ValueError:
890             raise ValueError(_('invalid network address'))
891
892     def _second_level_validation(self, value):
893         ip = IP(value)
894         if ip.iptype() == 'RESERVED':
895             raise ValueError(_("invalid network address, must not be in reserved class"))
896
897
898 class NetmaskOption(Option):
899     "represents the choice of a netmask"
900     __slots__ = tuple()
901     _opt_type = 'netmask'
902
903     def _validate(self, value):
904         try:
905             IP('0.0.0.0/{0}'.format(value))
906         except ValueError:
907             raise ValueError(_('invalid netmask address'))
908
909     def _cons_network_netmask(self, opts, vals):
910         #opts must be (netmask, network) options
911         if None in vals:
912             return
913         self.__cons_netmask(opts, vals[0], vals[1], False)
914
915     def _cons_ip_netmask(self, opts, vals):
916         #opts must be (netmask, ip) options
917         if None in vals:
918             return
919         self.__cons_netmask(opts, vals[0], vals[1], True)
920
921     def __cons_netmask(self, opts, val_netmask, val_ipnetwork, make_net):
922         if len(opts) != 2:
923             raise ConfigError(_('invalid len for opts'))
924         msg = None
925         try:
926             ip = IP('{0}/{1}'.format(val_ipnetwork, val_netmask),
927                     make_net=make_net)
928             #if cidr == 32, ip same has network
929             if ip.prefixlen() != 32:
930                 try:
931                     IP('{0}/{1}'.format(val_ipnetwork, val_netmask),
932                         make_net=not make_net)
933                 except ValueError:
934                     if not make_net:
935                         msg = _("invalid network {0} ({1}) "
936                                 "with netmask {2},"
937                                 " this network is an IP")
938                 else:
939                     if make_net:
940                         msg = _("invalid IP {0} ({1}) with netmask {2},"
941                                 " this IP is a network")
942
943         except ValueError:
944             if make_net:
945                 msg = _('invalid IP {0} ({1}) with netmask {2}')
946             else:
947                 msg = _('invalid network {0} ({1}) with netmask {2}')
948         if msg is not None:
949             raise ValueError(msg.format(val_ipnetwork, opts[1]._name,
950                                         val_netmask))
951
952
953 class BroadcastOption(Option):
954     __slots__ = tuple()
955     _opt_type = 'broadcast'
956
957     def _validate(self, value):
958         try:
959             IP('{0}/32'.format(value))
960         except ValueError:
961             raise ValueError(_('invalid broadcast address'))
962
963     def _cons_broadcast(self, opts, vals):
964         if len(vals) != 3:
965             raise ConfigError(_('invalid len for vals'))
966         if None in vals:
967             return
968         broadcast, network, netmask = vals
969         if IP('{0}/{1}'.format(network, netmask)).broadcast() != IP(broadcast):
970             raise ValueError(_('invalid broadcast {0} ({1}) with network {2} '
971                                '({3}) and netmask {4} ({5})').format(
972                                    broadcast, opts[0]._name, network,
973                                    opts[1]._name, netmask, opts[2]._name))
974
975
976 class DomainnameOption(Option):
977     """represents the choice of a domain name
978     netbios: for MS domain
979     hostname: to identify the device
980     domainname:
981     fqdn: with tld, not supported yet
982     """
983     __slots__ = ('_type', '_allow_ip', '_allow_without_dot', '_domain_re')
984     _opt_type = 'domainname'
985
986     def __init__(self, name, doc, default=None, default_multi=None,
987                  requires=None, multi=False, callback=None,
988                  callback_params=None, validator=None, validator_params=None,
989                  properties=None, allow_ip=False, type_='domainname',
990                  warnings_only=False, allow_without_dot=False):
991         if type_ not in ['netbios', 'hostname', 'domainname']:
992             raise ValueError(_('unknown type_ {0} for hostname').format(type_))
993         self._type = type_
994         if allow_ip not in [True, False]:
995             raise ValueError(_('allow_ip must be a boolean'))
996         if allow_without_dot not in [True, False]:
997             raise ValueError(_('allow_without_dot must be a boolean'))
998         self._allow_ip = allow_ip
999         self._allow_without_dot = allow_without_dot
1000         end = ''
1001         extrachar = ''
1002         extrachar_mandatory = ''
1003         if self._type == 'netbios':
1004             length = 14
1005         elif self._type == 'hostname':
1006             length = 62
1007         elif self._type == 'domainname':
1008             length = 62
1009             if allow_without_dot is False:
1010                 extrachar_mandatory = '\.'
1011             else:
1012                 extrachar = '\.'
1013             end = '+[a-z]*'
1014         self._domain_re = re.compile(r'^(?:[a-z][a-z\d\-{0}]{{,{1}}}{2}){3}$'
1015                                      ''.format(extrachar, length, extrachar_mandatory, end))
1016         super(DomainnameOption, self).__init__(name, doc, default=default,
1017                                                default_multi=default_multi,
1018                                                callback=callback,
1019                                                callback_params=callback_params,
1020                                                requires=requires,
1021                                                multi=multi,
1022                                                validator=validator,
1023                                                validator_params=validator_params,
1024                                                properties=properties,
1025                                                warnings_only=warnings_only)
1026
1027     def _validate(self, value):
1028         if self._allow_ip is True:
1029             try:
1030                 IP('{0}/32'.format(value))
1031                 return
1032             except ValueError:
1033                 pass
1034         if self._type == 'domainname' and not self._allow_without_dot and \
1035                 '.' not in value:
1036             raise ValueError(_("invalid domainname, must have dot"))
1037             if len(value) > 255:
1038                 raise ValueError(_("invalid domainname's length (max 255)"))
1039         if len(value) < 2:
1040             raise ValueError(_("invalid domainname's length (min 2)"))
1041         if not self._domain_re.search(value):
1042             raise ValueError(_('invalid domainname'))
1043
1044
1045 class EmailOption(DomainnameOption):
1046     __slots__ = tuple()
1047     _opt_type = 'email'
1048     username_re = re.compile(r"^[\w!#$%&'*+\-/=?^`{|}~.]+$")
1049
1050     def _validate(self, value):
1051         splitted = value.split('@', 1)
1052         try:
1053             username, domain = splitted
1054         except ValueError:
1055             raise ValueError(_('invalid email address, should contains one @'
1056                                ))
1057         if not self.username_re.search(username):
1058             raise ValueError(_('invalid username in email address'))
1059         super(EmailOption, self)._validate(domain)
1060
1061
1062 class URLOption(DomainnameOption):
1063     __slots__ = tuple()
1064     _opt_type = 'url'
1065     proto_re = re.compile(r'(http|https)://')
1066     path_re = re.compile(r"^[a-z0-9\-\._~:/\?#\[\]@!%\$&\'\(\)\*\+,;=]+$")
1067
1068     def _validate(self, value):
1069         match = self.proto_re.search(value)
1070         if not match:
1071             raise ValueError(_('invalid url, should start with http:// or '
1072                                'https://'))
1073         value = value[len(match.group(0)):]
1074         # get domain/files
1075         splitted = value.split('/', 1)
1076         try:
1077             domain, files = splitted
1078         except ValueError:
1079             domain = value
1080             files = None
1081         # if port in domain
1082         splitted = domain.split(':', 1)
1083         try:
1084             domain, port = splitted
1085
1086         except ValueError:
1087             domain = splitted[0]
1088             port = 0
1089         if not 0 <= int(port) <= 65535:
1090             raise ValueError(_('invalid url, port must be an between 0 and '
1091                                '65536'))
1092         # validate domainname
1093         super(URLOption, self)._validate(domain)
1094         # validate file
1095         if files is not None and files != '' and not self.path_re.search(files):
1096             raise ValueError(_('invalid url, should ends with filename'))
1097
1098
1099 class FilenameOption(Option):
1100     __slots__ = tuple()
1101     _opt_type = 'file'
1102     path_re = re.compile(r"^[a-zA-Z0-9\-\._~/+]+$")
1103
1104     def _validate(self, value):
1105         match = self.path_re.search(value)
1106         if not match:
1107             raise ValueError(_('invalid filename'))
1108
1109
1110 class OptionDescription(BaseOption):
1111     """Config's schema (organisation, group) and container of Options
1112     The `OptionsDescription` objects lives in the `tiramisu.config.Config`.
1113     """
1114     __slots__ = ('_name', '_requires', '_cache_paths', '_group_type',
1115                  '_state_group_type', '_properties', '_children',
1116                  '_cache_consistencies', '_calc_properties', '__weakref__',
1117                  '_readonly', '_impl_informations', '_state_requires',
1118                  '_stated', '_state_readonly')
1119     _opt_type = 'optiondescription'
1120
1121     def __init__(self, name, doc, children, requires=None, properties=None):
1122         """
1123         :param children: a list of options (including optiondescriptions)
1124
1125         """
1126         super(OptionDescription, self).__init__(name, doc, requires, properties)
1127         child_names = [child._name for child in children]
1128         #better performance like this
1129         valid_child = copy(child_names)
1130         valid_child.sort()
1131         old = None
1132         for child in valid_child:
1133             if child == old:
1134                 raise ConflictError(_('duplicate option name: '
1135                                       '{0}').format(child))
1136             old = child
1137         self._children = (tuple(child_names), tuple(children))
1138         self._cache_paths = None
1139         self._cache_consistencies = None
1140         # the group_type is useful for filtering OptionDescriptions in a config
1141         self._group_type = groups.default
1142
1143     def impl_getdoc(self):
1144         return self.impl_get_information('doc')
1145
1146     def __getattr__(self, name):
1147         if name in self.__slots__:
1148             return object.__getattribute__(self, name)
1149         try:
1150             return self._children[1][self._children[0].index(name)]
1151         except ValueError:
1152             raise AttributeError(_('unknown Option {0} '
1153                                    'in OptionDescription {1}'
1154                                    '').format(name, self._name))
1155
1156     def impl_getkey(self, config):
1157         return tuple([child.impl_getkey(getattr(config, child._name))
1158                       for child in self.impl_getchildren()])
1159
1160     def impl_getpaths(self, include_groups=False, _currpath=None):
1161         """returns a list of all paths in self, recursively
1162            _currpath should not be provided (helps with recursion)
1163         """
1164         if _currpath is None:
1165             _currpath = []
1166         paths = []
1167         for option in self.impl_getchildren():
1168             attr = option._name
1169             if isinstance(option, OptionDescription):
1170                 if include_groups:
1171                     paths.append('.'.join(_currpath + [attr]))
1172                 paths += option.impl_getpaths(include_groups=include_groups,
1173                                               _currpath=_currpath + [attr])
1174             else:
1175                 paths.append('.'.join(_currpath + [attr]))
1176         return paths
1177
1178     def impl_getchildren(self):
1179         return self._children[1]
1180
1181     def impl_build_cache(self,
1182                          cache_path=None,
1183                          cache_option=None,
1184                          _currpath=None,
1185                          _consistencies=None,
1186                          force_no_consistencies=False):
1187         if _currpath is None and self._cache_paths is not None:
1188             # cache already set
1189             return
1190         if _currpath is None:
1191             save = True
1192             _currpath = []
1193             if not force_no_consistencies:
1194                 _consistencies = {}
1195         else:
1196             save = False
1197         if cache_path is None:
1198             cache_path = []
1199             cache_option = []
1200         for option in self.impl_getchildren():
1201             attr = option._name
1202             if option in cache_option:
1203                 raise ConflictError(_('duplicate option: {0}').format(option))
1204
1205             cache_option.append(option)
1206             if not force_no_consistencies:
1207                 option._readonly = True
1208             cache_path.append(str('.'.join(_currpath + [attr])))
1209             if not isinstance(option, OptionDescription):
1210                 if not force_no_consistencies and \
1211                         option._consistencies is not None:
1212                     for consistency in option._consistencies:
1213                         func, all_cons_opts = consistency
1214                         for opt in all_cons_opts:
1215                             _consistencies.setdefault(opt,
1216                                                       []).append((func,
1217                                                                   all_cons_opts))
1218             else:
1219                 _currpath.append(attr)
1220                 option.impl_build_cache(cache_path,
1221                                         cache_option,
1222                                         _currpath,
1223                                         _consistencies,
1224                                         force_no_consistencies)
1225                 _currpath.pop()
1226         if save:
1227             self._cache_paths = (tuple(cache_option), tuple(cache_path))
1228             if not force_no_consistencies:
1229                 if _consistencies != {}:
1230                     self._cache_consistencies = {}
1231                     for opt, cons in _consistencies.items():
1232                         if opt not in cache_option:
1233                             raise ConfigError(_('consistency with option {0} which is not in Config').format(opt._name))
1234                         self._cache_consistencies[opt] = tuple(cons)
1235                 self._readonly = True
1236
1237     def impl_get_opt_by_path(self, path):
1238         try:
1239             return self._cache_paths[0][self._cache_paths[1].index(path)]
1240         except ValueError:
1241             raise AttributeError(_('no option for path {0}').format(path))
1242
1243     def impl_get_path_by_opt(self, opt):
1244         try:
1245             return self._cache_paths[1][self._cache_paths[0].index(opt)]
1246         except ValueError:
1247             raise AttributeError(_('no option {0} found').format(opt))
1248
1249     # ____________________________________________________________
1250     def impl_set_group_type(self, group_type):
1251         """sets a given group object to an OptionDescription
1252
1253         :param group_type: an instance of `GroupType` or `MasterGroupType`
1254                               that lives in `setting.groups`
1255         """
1256         if self._group_type != groups.default:
1257             raise TypeError(_('cannot change group_type if already set '
1258                             '(old {0}, new {1})').format(self._group_type,
1259                                                          group_type))
1260         if isinstance(group_type, groups.GroupType):
1261             self._group_type = group_type
1262             if isinstance(group_type, groups.MasterGroupType):
1263                 #if master (same name has group) is set
1264                 #for collect all slaves
1265                 slaves = []
1266                 master = None
1267                 for child in self.impl_getchildren():
1268                     if isinstance(child, OptionDescription):
1269                         raise ValueError(_("master group {0} shall not have "
1270                                          "a subgroup").format(self._name))
1271                     if isinstance(child, SymLinkOption):
1272                         raise ValueError(_("master group {0} shall not have "
1273                                          "a symlinkoption").format(self._name))
1274                     if not child.impl_is_multi():
1275                         raise ValueError(_("not allowed option {0} "
1276                                          "in group {1}"
1277                                          ": this option is not a multi"
1278                                          "").format(child._name, self._name))
1279                     if child._name == self._name:
1280                         child._multitype = multitypes.master
1281                         master = child
1282                     else:
1283                         slaves.append(child)
1284                 if master is None:
1285                     raise ValueError(_('master group with wrong'
1286                                        ' master name for {0}'
1287                                        ).format(self._name))
1288                 if master._callback is not None and master._callback[1] is not None:
1289                     for key, callbacks in master._callback[1].items():
1290                         for callbk in callbacks:
1291                             if isinstance(callbk, tuple):
1292                                 if callbk[0] in slaves:
1293                                     raise ValueError(_("callback of master's option shall "
1294                                                        "not refered a slave's ones"))
1295                 master._master_slaves = tuple(slaves)
1296                 for child in self.impl_getchildren():
1297                     if child != master:
1298                         child._master_slaves = master
1299                         child._multitype = multitypes.slave
1300         else:
1301             raise ValueError(_('group_type: {0}'
1302                                ' not allowed').format(group_type))
1303
1304     def impl_get_group_type(self):
1305         return self._group_type
1306
1307     def _valid_consistency(self, option, value, context, index):
1308         if self._cache_consistencies is None:
1309             return True
1310         #consistencies is something like [('_cons_not_equal', (opt1, opt2))]
1311         consistencies = self._cache_consistencies.get(option)
1312         if consistencies is not None:
1313             for func, all_cons_opts in consistencies:
1314                 #all_cons_opts[0] is the option where func is set
1315                 ret = all_cons_opts[0]._launch_consistency(func, option,
1316                                                            value,
1317                                                            context, index,
1318                                                            all_cons_opts)
1319                 if ret is False:
1320                     return False
1321         return True
1322
1323     def _impl_getstate(self, descr=None):
1324         """enables us to export into a dict
1325         :param descr: parent :class:`tiramisu.option.OptionDescription`
1326         """
1327         if descr is None:
1328             self.impl_build_cache()
1329             descr = self
1330         super(OptionDescription, self)._impl_getstate(descr)
1331         self._state_group_type = str(self._group_type)
1332         for option in self.impl_getchildren():
1333             option._impl_getstate(descr)
1334
1335     def __getstate__(self):
1336         """special method to enable the serialization with pickle
1337         """
1338         stated = True
1339         try:
1340             # the `_state` attribute is a flag that which tells us if
1341             # the serialization can be performed
1342             self._stated
1343         except AttributeError:
1344             # if cannot delete, _impl_getstate never launch
1345             # launch it recursivement
1346             # _stated prevent __getstate__ launch more than one time
1347             # _stated is delete, if re-serialize, re-lauch _impl_getstate
1348             self._impl_getstate()
1349             stated = False
1350         return super(OptionDescription, self).__getstate__(stated)
1351
1352     def _impl_setstate(self, descr=None):
1353         """enables us to import from a dict
1354         :param descr: parent :class:`tiramisu.option.OptionDescription`
1355         """
1356         if descr is None:
1357             self._cache_paths = None
1358             self._cache_consistencies = None
1359             self.impl_build_cache(force_no_consistencies=True)
1360             descr = self
1361         self._group_type = getattr(groups, self._state_group_type)
1362         del(self._state_group_type)
1363         super(OptionDescription, self)._impl_setstate(descr)
1364         for option in self.impl_getchildren():
1365             option._impl_setstate(descr)
1366
1367     def __setstate__(self, state):
1368         super(OptionDescription, self).__setstate__(state)
1369         try:
1370             self._stated
1371         except AttributeError:
1372             self._impl_setstate()
1373
1374
1375 def validate_requires_arg(requires, name):
1376     """check malformed requirements
1377     and tranform dict to internal tuple
1378
1379     :param requires: have a look at the
1380                      :meth:`tiramisu.setting.Settings.apply_requires` method to
1381                      know more about
1382                      the description of the requires dictionary
1383     """
1384     if requires is None:
1385         return None, None
1386     ret_requires = {}
1387     config_action = {}
1388
1389     # start parsing all requires given by user (has dict)
1390     # transforme it to a tuple
1391     for require in requires:
1392         if not type(require) == dict:
1393             raise ValueError(_("malformed requirements type for option:"
1394                                " {0}, must be a dict").format(name))
1395         valid_keys = ('option', 'expected', 'action', 'inverse', 'transitive',
1396                       'same_action')
1397         unknown_keys = frozenset(require.keys()) - frozenset(valid_keys)
1398         if unknown_keys != frozenset():
1399             raise ValueError('malformed requirements for option: {0}'
1400                              ' unknown keys {1}, must only '
1401                              '{2}'.format(name,
1402                                           unknown_keys,
1403                                           valid_keys))
1404         # prepare all attributes
1405         try:
1406             option = require['option']
1407             expected = require['expected']
1408             action = require['action']
1409         except KeyError:
1410             raise ValueError(_("malformed requirements for option: {0}"
1411                                " require must have option, expected and"
1412                                " action keys").format(name))
1413         inverse = require.get('inverse', False)
1414         if inverse not in [True, False]:
1415             raise ValueError(_('malformed requirements for option: {0}'
1416                                ' inverse must be boolean'))
1417         transitive = require.get('transitive', True)
1418         if transitive not in [True, False]:
1419             raise ValueError(_('malformed requirements for option: {0}'
1420                                ' transitive must be boolean'))
1421         same_action = require.get('same_action', True)
1422         if same_action not in [True, False]:
1423             raise ValueError(_('malformed requirements for option: {0}'
1424                                ' same_action must be boolean'))
1425
1426         if not isinstance(option, Option):
1427             raise ValueError(_('malformed requirements '
1428                                'must be an option in option {0}').format(name))
1429         if option.impl_is_multi():
1430             raise ValueError(_('malformed requirements option {0} '
1431                                'should not be a multi').format(name))
1432         if expected is not None:
1433             try:
1434                 option._validate(expected)
1435             except ValueError as err:
1436                 raise ValueError(_('malformed requirements second argument '
1437                                    'must be valid for option {0}'
1438                                    ': {1}').format(name, err))
1439         if action in config_action:
1440             if inverse != config_action[action]:
1441                 raise ValueError(_("inconsistency in action types"
1442                                    " for option: {0}"
1443                                    " action: {1}").format(name, action))
1444         else:
1445             config_action[action] = inverse
1446         if action not in ret_requires:
1447             ret_requires[action] = {}
1448         if option not in ret_requires[action]:
1449             ret_requires[action][option] = (option, [expected], action,
1450                                             inverse, transitive, same_action)
1451         else:
1452             ret_requires[action][option][1].append(expected)
1453     # transform dict to tuple
1454     ret = []
1455     for opt_requires in ret_requires.values():
1456         ret_action = []
1457         for require in opt_requires.values():
1458             ret_action.append((require[0], tuple(require[1]), require[2],
1459                                require[3], require[4], require[5]))
1460         ret.append(tuple(ret_action))
1461     return frozenset(config_action.keys()), tuple(ret)
1462
1463
1464 def validate_callback(callback, callback_params, type_):
1465     if type(callback) != FunctionType:
1466         raise ValueError(_('{0} should be a function').format(type_))
1467     if callback_params is not None:
1468         if not isinstance(callback_params, dict):
1469             raise ValueError(_('{0}_params should be a dict').format(type_))
1470         for key, callbacks in callback_params.items():
1471             if key != '' and len(callbacks) != 1:
1472                 raise ValueError(_('{0}_params with key {1} should not have '
1473                                    'length different to 1').format(type_,
1474                                                                    key))
1475             if not isinstance(callbacks, tuple):
1476                 raise ValueError(_('{0}_params should be tuple for key "{1}"'
1477                                    ).format(type_, key))
1478             for callbk in callbacks:
1479                 if isinstance(callbk, tuple):
1480                     option, force_permissive = callbk
1481                     if type_ == 'validator' and not force_permissive:
1482                         raise ValueError(_('validator not support tuple'))
1483                     if not isinstance(option, Option) and not \
1484                             isinstance(option, SymLinkOption):
1485                         raise ValueError(_('{0}_params should have an option '
1486                                            'not a {0} for first argument'
1487                                            ).format(type_, type(option)))
1488                     if force_permissive not in [True, False]:
1489                         raise ValueError(_('{0}_params should have a boolean'
1490                                            ' not a {0} for second argument'
1491                                            ).format(type_, type(
1492                                                force_permissive)))