better name's validation
[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, default_multi=False):
457         "accessing the default value"
458         if not default_multi or not self.impl_is_multi():
459             return self._default
460         else:
461             return self.getdefault_multi()
462
463     def impl_getdefault_multi(self):
464         "accessing the default value for a multi"
465         return self._default_multi
466
467     def impl_get_multitype(self):
468         return self._multitype
469
470     def impl_get_master_slaves(self):
471         return self._master_slaves
472
473     def impl_is_empty_by_default(self):
474         "no default value has been set yet"
475         if ((not self.impl_is_multi() and self._default is None) or
476                 (self.impl_is_multi() and (self._default == []
477                                            or None in self._default))):
478             return True
479         return False
480
481     def impl_getdoc(self):
482         "accesses the Option's doc"
483         return self.impl_get_information('doc')
484
485     def impl_has_callback(self):
486         "to know if a callback has been defined or not"
487         if self._callback is None:
488             return False
489         else:
490             return True
491
492     def impl_getkey(self, value):
493         return value
494
495     def impl_is_multi(self):
496         return self._multi
497
498     def impl_add_consistency(self, func, *other_opts):
499         """Add consistency means that value will be validate with other_opts
500         option's values.
501
502         :param func: function's name
503         :type func: `str`
504         :param other_opts: options used to validate value
505         :type other_opts: `list` of `tiramisu.option.Option`
506         """
507         if self._consistencies is None:
508             self._consistencies = []
509         for opt in other_opts:
510             if not isinstance(opt, Option):
511                 raise ConfigError(_('consistency should be set with an option'))
512             if self is opt:
513                 raise ConfigError(_('cannot add consistency with itself'))
514             if self.impl_is_multi() != opt.impl_is_multi():
515                 raise ConfigError(_('every options in consistency should be '
516                                     'multi or none'))
517         func = '_cons_{0}'.format(func)
518         all_cons_opts = tuple([self] + list(other_opts))
519         value = self.impl_getdefault()
520         if value is not None:
521             if self.impl_is_multi():
522                 for idx, val in enumerate(value):
523                     self._launch_consistency(func, self, val, None,
524                                              idx, all_cons_opts)
525             else:
526                 self._launch_consistency(func, self, value, None,
527                                          None, all_cons_opts)
528         self._consistencies.append((func, all_cons_opts))
529         self.impl_validate(self.impl_getdefault())
530
531     def _cons_not_equal(self, opts, vals):
532         for idx_inf, val_inf in enumerate(vals):
533             for idx_sup, val_sup in enumerate(vals[idx_inf + 1:]):
534                 if val_inf == val_sup is not None:
535                     raise ValueError(_("same value for {0} and {1}").format(
536                         opts[idx_inf]._name, opts[idx_inf + idx_sup + 1]._name))
537
538     def _impl_convert_callbacks(self, descr, load=False):
539         if not load and self._callback is None:
540             self._state_callback = None
541         elif load and self._state_callback is None:
542             self._callback = None
543             del(self._state_callback)
544         else:
545             if load:
546                 callback, callback_params = self._state_callback
547             else:
548                 callback, callback_params = self._callback
549             if callback_params is not None:
550                 cllbck_prms = {}
551                 for key, values in callback_params.items():
552                     vls = []
553                     for value in values:
554                         if isinstance(value, tuple):
555                             if load:
556                                 value = (descr.impl_get_opt_by_path(value[0]),
557                                          value[1])
558                             else:
559                                 value = (descr.impl_get_path_by_opt(value[0]),
560                                          value[1])
561                         vls.append(value)
562                     cllbck_prms[key] = tuple(vls)
563             else:
564                 cllbck_prms = None
565
566             if load:
567                 del(self._state_callback)
568                 self._callback = (callback, cllbck_prms)
569             else:
570                 self._state_callback = (callback, cllbck_prms)
571
572     # serialize/unserialize
573     def _impl_convert_consistencies(self, descr, load=False):
574         """during serialization process, many things have to be done.
575         one of them is the localisation of the options.
576         The paths are set once for all.
577
578         :type descr: :class:`tiramisu.option.OptionDescription`
579         :param load: `True` if we are at the init of the option description
580         :type load: bool
581         """
582         if not load and self._consistencies is None:
583             self._state_consistencies = None
584         elif load and self._state_consistencies is None:
585             self._consistencies = None
586             del(self._state_consistencies)
587         else:
588             if load:
589                 consistencies = self._state_consistencies
590             else:
591                 consistencies = self._consistencies
592             if isinstance(consistencies, list):
593                 new_value = []
594                 for consistency in consistencies:
595                     values = []
596                     for obj in consistency[1]:
597                         if load:
598                             values.append(descr.impl_get_opt_by_path(obj))
599                         else:
600                             values.append(descr.impl_get_path_by_opt(obj))
601                     new_value.append((consistency[0], tuple(values)))
602
603             else:
604                 new_value = {}
605                 for key, _consistencies in consistencies.items():
606                     new_value[key] = []
607                     for key_cons, _cons in _consistencies:
608                         _list_cons = []
609                         for _con in _cons:
610                             if load:
611                                 _list_cons.append(
612                                     descr.impl_get_opt_by_path(_con))
613                             else:
614                                 _list_cons.append(
615                                     descr.impl_get_path_by_opt(_con))
616                         new_value[key].append((key_cons, tuple(_list_cons)))
617             if load:
618                 del(self._state_consistencies)
619                 self._consistencies = new_value
620             else:
621                 self._state_consistencies = new_value
622
623     def _second_level_validation(self, value):
624         pass
625
626
627 class ChoiceOption(Option):
628     """represents a choice out of several objects.
629
630     The option can also have the value ``None``
631     """
632
633     __slots__ = ('_values', '_open_values')
634     _opt_type = 'string'
635
636     def __init__(self, name, doc, values, default=None, default_multi=None,
637                  requires=None, multi=False, callback=None,
638                  callback_params=None, open_values=False, validator=None,
639                  validator_params=None, properties=None, warnings_only=False):
640         """
641         :param values: is a list of values the option can possibly take
642         """
643         if not isinstance(values, tuple):
644             raise TypeError(_('values must be a tuple for {0}').format(name))
645         self._values = values
646         if open_values not in (True, False):
647             raise TypeError(_('open_values must be a boolean for '
648                             '{0}').format(name))
649         self._open_values = open_values
650         super(ChoiceOption, self).__init__(name, doc, default=default,
651                                            default_multi=default_multi,
652                                            callback=callback,
653                                            callback_params=callback_params,
654                                            requires=requires,
655                                            multi=multi,
656                                            validator=validator,
657                                            validator_params=validator_params,
658                                            properties=properties,
659                                            warnings_only=warnings_only)
660
661     def impl_get_values(self):
662         return self._values
663
664     def impl_is_openvalues(self):
665         return self._open_values
666
667     def _validate(self, value):
668         if not self._open_values and not value in self._values:
669             raise ValueError(_('value {0} is not permitted, '
670                                'only {1} is allowed'
671                                '').format(value, self._values))
672
673
674 class BoolOption(Option):
675     "represents a choice between ``True`` and ``False``"
676     __slots__ = tuple()
677     _opt_type = 'bool'
678
679     def _validate(self, value):
680         if not isinstance(value, bool):
681             raise ValueError(_('invalid boolean'))
682
683
684 class IntOption(Option):
685     "represents a choice of an integer"
686     __slots__ = tuple()
687     _opt_type = 'int'
688
689     def _validate(self, value):
690         if not isinstance(value, int):
691             raise ValueError(_('invalid integer'))
692
693
694 class FloatOption(Option):
695     "represents a choice of a floating point number"
696     __slots__ = tuple()
697     _opt_type = 'float'
698
699     def _validate(self, value):
700         if not isinstance(value, float):
701             raise ValueError(_('invalid float'))
702
703
704 class StrOption(Option):
705     "represents the choice of a string"
706     __slots__ = tuple()
707     _opt_type = 'string'
708
709     def _validate(self, value):
710         if not isinstance(value, str):
711             raise ValueError(_('invalid string'))
712
713
714 if sys.version_info[0] >= 3:
715     #UnicodeOption is same as StrOption in python 3+
716     class UnicodeOption(StrOption):
717         __slots__ = tuple()
718         pass
719 else:
720     class UnicodeOption(Option):
721         "represents the choice of a unicode string"
722         __slots__ = tuple()
723         _opt_type = 'unicode'
724         _empty = u''
725
726         def _validate(self, value):
727             if not isinstance(value, unicode):
728                 raise ValueError(_('invalid unicode'))
729
730
731 class SymLinkOption(BaseOption):
732     __slots__ = ('_name', '_opt', '_state_opt')
733     _opt_type = 'symlink'
734     #not return _opt consistencies
735     _consistencies = None
736
737     def __init__(self, name, opt):
738         self._name = name
739         if not isinstance(opt, Option):
740             raise ValueError(_('malformed symlinkoption '
741                                'must be an option '
742                                'for symlink {0}').format(name))
743         self._opt = opt
744         self._readonly = True
745
746     def __getattr__(self, name):
747         if name in ('_name', '_opt', '_opt_type', '_readonly'):
748             return object.__getattr__(self, name)
749         else:
750             return getattr(self._opt, name)
751
752     def _impl_getstate(self, descr):
753         super(SymLinkOption, self)._impl_getstate(descr)
754         self._state_opt = descr.impl_get_path_by_opt(self._opt)
755
756     def _impl_setstate(self, descr):
757         self._opt = descr.impl_get_opt_by_path(self._state_opt)
758         del(self._state_opt)
759         super(SymLinkOption, self)._impl_setstate(descr)
760
761
762 class IPOption(Option):
763     "represents the choice of an ip"
764     __slots__ = ('_private_only', '_allow_reserved')
765     _opt_type = 'ip'
766
767     def __init__(self, name, doc, default=None, default_multi=None,
768                  requires=None, multi=False, callback=None,
769                  callback_params=None, validator=None, validator_params=None,
770                  properties=None, private_only=False, allow_reserved=False,
771                  warnings_only=False):
772         self._private_only = private_only
773         self._allow_reserved = allow_reserved
774         super(IPOption, self).__init__(name, doc, default=default,
775                                        default_multi=default_multi,
776                                        callback=callback,
777                                        callback_params=callback_params,
778                                        requires=requires,
779                                        multi=multi,
780                                        validator=validator,
781                                        validator_params=validator_params,
782                                        properties=properties,
783                                        warnings_only=warnings_only)
784
785     def _validate(self, value):
786         # sometimes an ip term starts with a zero
787         # but this does not fit in some case, for example bind does not like it
788         for val in value.split('.'):
789             if val.startswith("0") and len(val) > 1:
790                 raise ValueError(_('invalid IP'))
791         # 'standard' validation
792         try:
793             IP('{0}/32'.format(value))
794         except ValueError:
795             raise ValueError(_('invalid IP'))
796
797     def _second_level_validation(self, value):
798         ip = IP('{0}/32'.format(value))
799         if not self._allow_reserved and ip.iptype() == 'RESERVED':
800             raise ValueError(_("invalid IP, mustn't not be in reserved class"))
801         if self._private_only and not ip.iptype() == 'PRIVATE':
802             raise ValueError(_("invalid IP, must be in private class"))
803
804
805 class PortOption(Option):
806     """represents the choice of a port
807     The port numbers are divided into three ranges:
808     the well-known ports,
809     the registered ports,
810     and the dynamic or private ports.
811     You can actived this three range.
812     Port number 0 is reserved and can't be used.
813     see: http://en.wikipedia.org/wiki/Port_numbers
814     """
815     __slots__ = ('_allow_range', '_allow_zero', '_min_value', '_max_value')
816     _opt_type = 'port'
817
818     def __init__(self, name, doc, default=None, default_multi=None,
819                  requires=None, multi=False, callback=None,
820                  callback_params=None, validator=None, validator_params=None,
821                  properties=None, allow_range=False, allow_zero=False,
822                  allow_wellknown=True, allow_registred=True,
823                  allow_private=False, warnings_only=False):
824         self._allow_range = allow_range
825         self._min_value = None
826         self._max_value = None
827         ports_min = [0, 1, 1024, 49152]
828         ports_max = [0, 1023, 49151, 65535]
829         is_finally = False
830         for index, allowed in enumerate([allow_zero,
831                                          allow_wellknown,
832                                          allow_registred,
833                                          allow_private]):
834             if self._min_value is None:
835                 if allowed:
836                     self._min_value = ports_min[index]
837             elif not allowed:
838                 is_finally = True
839             elif allowed and is_finally:
840                 raise ValueError(_('inconsistency in allowed range'))
841             if allowed:
842                 self._max_value = ports_max[index]
843
844         if self._max_value is None:
845             raise ValueError(_('max value is empty'))
846
847         super(PortOption, self).__init__(name, doc, default=default,
848                                          default_multi=default_multi,
849                                          callback=callback,
850                                          callback_params=callback_params,
851                                          requires=requires,
852                                          multi=multi,
853                                          validator=validator,
854                                          validator_params=validator_params,
855                                          properties=properties,
856                                          warnings_only=warnings_only)
857
858     def _validate(self, value):
859         if self._allow_range and ":" in str(value):
860             value = str(value).split(':')
861             if len(value) != 2:
862                 raise ValueError('invalid part, range must have two values '
863                                  'only')
864             if not value[0] < value[1]:
865                 raise ValueError('invalid port, first port in range must be'
866                                  ' smaller than the second one')
867         else:
868             value = [value]
869
870         for val in value:
871             if not self._min_value <= int(val) <= self._max_value:
872                 raise ValueError('invalid port, must be an between {0} and {1}'
873                                  ''.format(self._min_value, self._max_value))
874
875
876 class NetworkOption(Option):
877     "represents the choice of a network"
878     __slots__ = tuple()
879     _opt_type = 'network'
880
881     def _validate(self, value):
882         try:
883             IP(value)
884         except ValueError:
885             raise ValueError(_('invalid network address'))
886
887     def _second_level_validation(self, value):
888         ip = IP(value)
889         if ip.iptype() == 'RESERVED':
890             raise ValueError(_("invalid network address, must not be in reserved class"))
891
892
893 class NetmaskOption(Option):
894     "represents the choice of a netmask"
895     __slots__ = tuple()
896     _opt_type = 'netmask'
897
898     def _validate(self, value):
899         try:
900             IP('0.0.0.0/{0}'.format(value))
901         except ValueError:
902             raise ValueError(_('invalid netmask address'))
903
904     def _cons_network_netmask(self, opts, vals):
905         #opts must be (netmask, network) options
906         if None in vals:
907             return
908         self.__cons_netmask(opts, vals[0], vals[1], False)
909
910     def _cons_ip_netmask(self, opts, vals):
911         #opts must be (netmask, ip) options
912         if None in vals:
913             return
914         self.__cons_netmask(opts, vals[0], vals[1], True)
915
916     def __cons_netmask(self, opts, val_netmask, val_ipnetwork, make_net):
917         if len(opts) != 2:
918             raise ConfigError(_('invalid len for opts'))
919         msg = None
920         try:
921             ip = IP('{0}/{1}'.format(val_ipnetwork, val_netmask),
922                     make_net=make_net)
923             #if cidr == 32, ip same has network
924             if ip.prefixlen() != 32:
925                 try:
926                     IP('{0}/{1}'.format(val_ipnetwork, val_netmask),
927                         make_net=not make_net)
928                 except ValueError:
929                     if not make_net:
930                         msg = _("invalid network {0} ({1}) "
931                                 "with netmask {2},"
932                                 " this network is an IP")
933                 else:
934                     if make_net:
935                         msg = _("invalid IP {0} ({1}) with netmask {2},"
936                                 " this IP is a network")
937
938         except ValueError:
939             if make_net:
940                 msg = _('invalid IP {0} ({1}) with netmask {2}')
941             else:
942                 msg = _('invalid network {0} ({1}) with netmask {2}')
943         if msg is not None:
944             raise ValueError(msg.format(val_ipnetwork, opts[1]._name,
945                                         val_netmask))
946
947
948 class BroadcastOption(Option):
949     __slots__ = tuple()
950     _opt_type = 'broadcast'
951
952     def _validate(self, value):
953         try:
954             IP('{0}/32'.format(value))
955         except ValueError:
956             raise ValueError(_('invalid broadcast address'))
957
958     def _cons_broadcast(self, opts, vals):
959         if len(vals) != 3:
960             raise ConfigError(_('invalid len for vals'))
961         if None in vals:
962             return
963         broadcast, network, netmask = vals
964         if IP('{0}/{1}'.format(network, netmask)).broadcast() != IP(broadcast):
965             raise ValueError(_('invalid broadcast {0} ({1}) with network {2} '
966                                '({3}) and netmask {4} ({5})').format(
967                                    broadcast, opts[0]._name, network,
968                                    opts[1]._name, netmask, opts[2]._name))
969
970
971 class DomainnameOption(Option):
972     """represents the choice of a domain name
973     netbios: for MS domain
974     hostname: to identify the device
975     domainname:
976     fqdn: with tld, not supported yet
977     """
978     __slots__ = ('_type', '_allow_ip', '_allow_without_dot', '_domain_re')
979     _opt_type = 'domainname'
980
981     def __init__(self, name, doc, default=None, default_multi=None,
982                  requires=None, multi=False, callback=None,
983                  callback_params=None, validator=None, validator_params=None,
984                  properties=None, allow_ip=False, type_='domainname',
985                  warnings_only=False, allow_without_dot=False):
986         if type_ not in ['netbios', 'hostname', 'domainname']:
987             raise ValueError(_('unknown type_ {0} for hostname').format(type_))
988         self._type = type_
989         if allow_ip not in [True, False]:
990             raise ValueError(_('allow_ip must be a boolean'))
991         if allow_without_dot not in [True, False]:
992             raise ValueError(_('allow_without_dot must be a boolean'))
993         self._allow_ip = allow_ip
994         self._allow_without_dot = allow_without_dot
995         end = ''
996         extrachar = ''
997         extrachar_mandatory = ''
998         if self._type == 'netbios':
999             length = 14
1000         elif self._type == 'hostname':
1001             length = 62
1002         elif self._type == 'domainname':
1003             length = 62
1004             if allow_without_dot is False:
1005                 extrachar_mandatory = '\.'
1006             else:
1007                 extrachar = '\.'
1008             end = '+[a-z]*'
1009         self._domain_re = re.compile(r'^(?:[a-z][a-z\d\-{0}]{{,{1}}}{2}){3}$'
1010                                      ''.format(extrachar, length, extrachar_mandatory, end))
1011         super(DomainnameOption, self).__init__(name, doc, default=default,
1012                                                default_multi=default_multi,
1013                                                callback=callback,
1014                                                callback_params=callback_params,
1015                                                requires=requires,
1016                                                multi=multi,
1017                                                validator=validator,
1018                                                validator_params=validator_params,
1019                                                properties=properties,
1020                                                warnings_only=warnings_only)
1021
1022     def _validate(self, value):
1023         if self._allow_ip is True:
1024             try:
1025                 IP('{0}/32'.format(value))
1026                 return
1027             except ValueError:
1028                 pass
1029         if self._type == 'domainname' and not self._allow_without_dot and \
1030                 '.' not in value:
1031             raise ValueError(_("invalid domainname, must have dot"))
1032             if len(value) > 255:
1033                 raise ValueError(_("invalid domainname's length (max 255)"))
1034         if len(value) < 2:
1035             raise ValueError(_("invalid domainname's length (min 2)"))
1036         if not self._domain_re.search(value):
1037             raise ValueError(_('invalid domainname'))
1038
1039
1040 class EmailOption(DomainnameOption):
1041     __slots__ = tuple()
1042     _opt_type = 'email'
1043     username_re = re.compile(r"^[\w!#$%&'*+\-/=?^`{|}~.]+$")
1044
1045     def _validate(self, value):
1046         splitted = value.split('@', 1)
1047         try:
1048             username, domain = splitted
1049         except ValueError:
1050             raise ValueError(_('invalid email address, should contains one @'
1051                                ))
1052         if not self.username_re.search(username):
1053             raise ValueError(_('invalid username in email address'))
1054         super(EmailOption, self)._validate(domain)
1055
1056
1057 class URLOption(DomainnameOption):
1058     __slots__ = tuple()
1059     _opt_type = 'url'
1060     proto_re = re.compile(r'(http|https)://')
1061     path_re = re.compile(r"^[a-z0-9\-\._~:/\?#\[\]@!%\$&\'\(\)\*\+,;=]+$")
1062
1063     def _validate(self, value):
1064         match = self.proto_re.search(value)
1065         if not match:
1066             raise ValueError(_('invalid url, should start with http:// or '
1067                                'https://'))
1068         value = value[len(match.group(0)):]
1069         # get domain/files
1070         splitted = value.split('/', 1)
1071         try:
1072             domain, files = splitted
1073         except ValueError:
1074             domain = value
1075             files = None
1076         # if port in domain
1077         splitted = domain.split(':', 1)
1078         try:
1079             domain, port = splitted
1080
1081         except ValueError:
1082             domain = splitted[0]
1083             port = 0
1084         if not 0 <= int(port) <= 65535:
1085             raise ValueError(_('invalid url, port must be an between 0 and '
1086                                '65536'))
1087         # validate domainname
1088         super(URLOption, self)._validate(domain)
1089         # validate file
1090         if files is not None and files != '' and not self.path_re.search(files):
1091             raise ValueError(_('invalid url, should ends with filename'))
1092
1093
1094 class FilenameOption(Option):
1095     __slots__ = tuple()
1096     _opt_type = 'file'
1097     path_re = re.compile(r"^[a-zA-Z0-9\-\._~/+]+$")
1098
1099     def _validate(self, value):
1100         match = self.path_re.search(value)
1101         if not match:
1102             raise ValueError(_('invalid filename'))
1103
1104
1105 class OptionDescription(BaseOption):
1106     """Config's schema (organisation, group) and container of Options
1107     The `OptionsDescription` objects lives in the `tiramisu.config.Config`.
1108     """
1109     __slots__ = ('_name', '_requires', '_cache_paths', '_group_type',
1110                  '_state_group_type', '_properties', '_children',
1111                  '_cache_consistencies', '_calc_properties', '__weakref__',
1112                  '_readonly', '_impl_informations', '_state_requires',
1113                  '_stated', '_state_readonly')
1114     _opt_type = 'optiondescription'
1115
1116     def __init__(self, name, doc, children, requires=None, properties=None):
1117         """
1118         :param children: a list of options (including optiondescriptions)
1119
1120         """
1121         super(OptionDescription, self).__init__(name, doc, requires, properties)
1122         child_names = [child._name for child in children]
1123         #better performance like this
1124         valid_child = copy(child_names)
1125         valid_child.sort()
1126         old = None
1127         for child in valid_child:
1128             if child == old:
1129                 raise ConflictError(_('duplicate option name: '
1130                                       '{0}').format(child))
1131             old = child
1132         self._children = (tuple(child_names), tuple(children))
1133         self._cache_paths = None
1134         self._cache_consistencies = None
1135         # the group_type is useful for filtering OptionDescriptions in a config
1136         self._group_type = groups.default
1137
1138     def impl_getdoc(self):
1139         return self.impl_get_information('doc')
1140
1141     def __getattr__(self, name):
1142         if name in self.__slots__:
1143             return object.__getattribute__(self, name)
1144         try:
1145             return self._children[1][self._children[0].index(name)]
1146         except ValueError:
1147             raise AttributeError(_('unknown Option {0} '
1148                                    'in OptionDescription {1}'
1149                                    '').format(name, self._name))
1150
1151     def impl_getkey(self, config):
1152         return tuple([child.impl_getkey(getattr(config, child._name))
1153                       for child in self.impl_getchildren()])
1154
1155     def impl_getpaths(self, include_groups=False, _currpath=None):
1156         """returns a list of all paths in self, recursively
1157            _currpath should not be provided (helps with recursion)
1158         """
1159         if _currpath is None:
1160             _currpath = []
1161         paths = []
1162         for option in self.impl_getchildren():
1163             attr = option._name
1164             if isinstance(option, OptionDescription):
1165                 if include_groups:
1166                     paths.append('.'.join(_currpath + [attr]))
1167                 paths += option.impl_getpaths(include_groups=include_groups,
1168                                               _currpath=_currpath + [attr])
1169             else:
1170                 paths.append('.'.join(_currpath + [attr]))
1171         return paths
1172
1173     def impl_getchildren(self):
1174         return self._children[1]
1175
1176     def impl_build_cache(self,
1177                          cache_path=None,
1178                          cache_option=None,
1179                          _currpath=None,
1180                          _consistencies=None,
1181                          force_no_consistencies=False):
1182         if _currpath is None and self._cache_paths is not None:
1183             # cache already set
1184             return
1185         if _currpath is None:
1186             save = True
1187             _currpath = []
1188             if not force_no_consistencies:
1189                 _consistencies = {}
1190         else:
1191             save = False
1192         if cache_path is None:
1193             cache_path = []
1194             cache_option = []
1195         for option in self.impl_getchildren():
1196             attr = option._name
1197             if option in cache_option:
1198                 raise ConflictError(_('duplicate option: {0}').format(option))
1199
1200             cache_option.append(option)
1201             if not force_no_consistencies:
1202                 option._readonly = True
1203             cache_path.append(str('.'.join(_currpath + [attr])))
1204             if not isinstance(option, OptionDescription):
1205                 if not force_no_consistencies and \
1206                         option._consistencies is not None:
1207                     for consistency in option._consistencies:
1208                         func, all_cons_opts = consistency
1209                         for opt in all_cons_opts:
1210                             _consistencies.setdefault(opt,
1211                                                       []).append((func,
1212                                                                   all_cons_opts))
1213             else:
1214                 _currpath.append(attr)
1215                 option.impl_build_cache(cache_path,
1216                                         cache_option,
1217                                         _currpath,
1218                                         _consistencies,
1219                                         force_no_consistencies)
1220                 _currpath.pop()
1221         if save:
1222             self._cache_paths = (tuple(cache_option), tuple(cache_path))
1223             if not force_no_consistencies:
1224                 if _consistencies != {}:
1225                     self._cache_consistencies = {}
1226                     for opt, cons in _consistencies.items():
1227                         if opt not in cache_option:
1228                             raise ConfigError(_('consistency with option {0} which is not in Config').format(opt._name))
1229                         self._cache_consistencies[opt] = tuple(cons)
1230                 self._readonly = True
1231
1232     def impl_get_opt_by_path(self, path):
1233         try:
1234             return self._cache_paths[0][self._cache_paths[1].index(path)]
1235         except ValueError:
1236             raise AttributeError(_('no option for path {0}').format(path))
1237
1238     def impl_get_path_by_opt(self, opt):
1239         try:
1240             return self._cache_paths[1][self._cache_paths[0].index(opt)]
1241         except ValueError:
1242             raise AttributeError(_('no option {0} found').format(opt))
1243
1244     # ____________________________________________________________
1245     def impl_set_group_type(self, group_type):
1246         """sets a given group object to an OptionDescription
1247
1248         :param group_type: an instance of `GroupType` or `MasterGroupType`
1249                               that lives in `setting.groups`
1250         """
1251         if self._group_type != groups.default:
1252             raise TypeError(_('cannot change group_type if already set '
1253                             '(old {0}, new {1})').format(self._group_type,
1254                                                          group_type))
1255         if isinstance(group_type, groups.GroupType):
1256             self._group_type = group_type
1257             if isinstance(group_type, groups.MasterGroupType):
1258                 #if master (same name has group) is set
1259                 identical_master_child_name = False
1260                 #for collect all slaves
1261                 slaves = []
1262                 master = None
1263                 for child in self.impl_getchildren():
1264                     if isinstance(child, OptionDescription):
1265                         raise ValueError(_("master group {0} shall not have "
1266                                          "a subgroup").format(self._name))
1267                     if isinstance(child, SymLinkOption):
1268                         raise ValueError(_("master group {0} shall not have "
1269                                          "a symlinkoption").format(self._name))
1270                     if not child.impl_is_multi():
1271                         raise ValueError(_("not allowed option {0} "
1272                                          "in group {1}"
1273                                          ": this option is not a multi"
1274                                          "").format(child._name, self._name))
1275                     if child._name == self._name:
1276                         identical_master_child_name = True
1277                         child._multitype = multitypes.master
1278                         master = child
1279                     else:
1280                         slaves.append(child)
1281                 if master is None:
1282                     raise ValueError(_('master group with wrong'
1283                                        ' master name for {0}'
1284                                        ).format(self._name))
1285                 if master._callback is not None and master._callback[1] is not None:
1286                     for key, callbacks in master._callback[1].items():
1287                         for callbk in callbacks:
1288                             if isinstance(callbk, tuple):
1289                                 if callbk[0] in slaves:
1290                                     raise ValueError(_("callback of master's option shall "
1291                                                        "not refered a slave's ones"))
1292                 master._master_slaves = tuple(slaves)
1293                 for child in self.impl_getchildren():
1294                     if child != master:
1295                         child._master_slaves = master
1296                         child._multitype = multitypes.slave
1297                 if not identical_master_child_name:
1298                     raise ValueError(_("no child has same name has master group"
1299                                        " for: {0}").format(self._name))
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)))