add more tests
[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._open_values and not value in self._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         for val in value.split('.'):
786             if val.startswith("0") and len(val) > 1:
787                 raise ValueError(_('invalid IP'))
788         # 'standard' validation
789         try:
790             IP('{0}/32'.format(value))
791         except ValueError:
792             raise ValueError(_('invalid IP'))
793
794     def _second_level_validation(self, value):
795         ip = IP('{0}/32'.format(value))
796         if not self._allow_reserved and ip.iptype() == 'RESERVED':
797             raise ValueError(_("invalid IP, mustn't not be in reserved class"))
798         if self._private_only and not ip.iptype() == 'PRIVATE':
799             raise ValueError(_("invalid IP, must be in private class"))
800
801
802 class PortOption(Option):
803     """represents the choice of a port
804     The port numbers are divided into three ranges:
805     the well-known ports,
806     the registered ports,
807     and the dynamic or private ports.
808     You can actived this three range.
809     Port number 0 is reserved and can't be used.
810     see: http://en.wikipedia.org/wiki/Port_numbers
811     """
812     __slots__ = ('_allow_range', '_allow_zero', '_min_value', '_max_value')
813     _opt_type = 'port'
814
815     def __init__(self, name, doc, default=None, default_multi=None,
816                  requires=None, multi=False, callback=None,
817                  callback_params=None, validator=None, validator_params=None,
818                  properties=None, allow_range=False, allow_zero=False,
819                  allow_wellknown=True, allow_registred=True,
820                  allow_private=False, warnings_only=False):
821         self._allow_range = allow_range
822         self._min_value = None
823         self._max_value = None
824         ports_min = [0, 1, 1024, 49152]
825         ports_max = [0, 1023, 49151, 65535]
826         is_finally = False
827         for index, allowed in enumerate([allow_zero,
828                                          allow_wellknown,
829                                          allow_registred,
830                                          allow_private]):
831             if self._min_value is None:
832                 if allowed:
833                     self._min_value = ports_min[index]
834             elif not allowed:
835                 is_finally = True
836             elif allowed and is_finally:
837                 raise ValueError(_('inconsistency in allowed range'))
838             if allowed:
839                 self._max_value = ports_max[index]
840
841         if self._max_value is None:
842             raise ValueError(_('max value is empty'))
843
844         super(PortOption, self).__init__(name, doc, default=default,
845                                          default_multi=default_multi,
846                                          callback=callback,
847                                          callback_params=callback_params,
848                                          requires=requires,
849                                          multi=multi,
850                                          validator=validator,
851                                          validator_params=validator_params,
852                                          properties=properties,
853                                          warnings_only=warnings_only)
854
855     def _validate(self, value):
856         if self._allow_range and ":" in str(value):
857             value = str(value).split(':')
858             if len(value) != 2:
859                 raise ValueError('invalid part, range must have two values '
860                                  'only')
861             if not value[0] < value[1]:
862                 raise ValueError('invalid port, first port in range must be'
863                                  ' smaller than the second one')
864         else:
865             value = [value]
866
867         for val in value:
868             if not self._min_value <= int(val) <= self._max_value:
869                 raise ValueError('invalid port, must be an between {0} and {1}'
870                                  ''.format(self._min_value, self._max_value))
871
872
873 class NetworkOption(Option):
874     "represents the choice of a network"
875     __slots__ = tuple()
876     _opt_type = 'network'
877
878     def _validate(self, value):
879         try:
880             IP(value)
881         except ValueError:
882             raise ValueError(_('invalid network address'))
883
884     def _second_level_validation(self, value):
885         ip = IP(value)
886         if ip.iptype() == 'RESERVED':
887             raise ValueError(_("invalid network address, must not be in reserved class"))
888
889
890 class NetmaskOption(Option):
891     "represents the choice of a netmask"
892     __slots__ = tuple()
893     _opt_type = 'netmask'
894
895     def _validate(self, value):
896         try:
897             IP('0.0.0.0/{0}'.format(value))
898         except ValueError:
899             raise ValueError(_('invalid netmask address'))
900
901     def _cons_network_netmask(self, opts, vals):
902         #opts must be (netmask, network) options
903         if None in vals:
904             return
905         self.__cons_netmask(opts, vals[0], vals[1], False)
906
907     def _cons_ip_netmask(self, opts, vals):
908         #opts must be (netmask, ip) options
909         if None in vals:
910             return
911         self.__cons_netmask(opts, vals[0], vals[1], True)
912
913     def __cons_netmask(self, opts, val_netmask, val_ipnetwork, make_net):
914         if len(opts) != 2:
915             raise ConfigError(_('invalid len for opts'))
916         msg = None
917         try:
918             ip = IP('{0}/{1}'.format(val_ipnetwork, val_netmask),
919                     make_net=make_net)
920             #if cidr == 32, ip same has network
921             if ip.prefixlen() != 32:
922                 try:
923                     IP('{0}/{1}'.format(val_ipnetwork, val_netmask),
924                         make_net=not make_net)
925                 except ValueError:
926                     if not make_net:
927                         msg = _("invalid network {0} ({1}) "
928                                 "with netmask {2},"
929                                 " this network is an IP")
930                 else:
931                     if make_net:
932                         msg = _("invalid IP {0} ({1}) with netmask {2},"
933                                 " this IP is a network")
934
935         except ValueError:
936             if make_net:
937                 msg = _('invalid IP {0} ({1}) with netmask {2}')
938             else:
939                 msg = _('invalid network {0} ({1}) with netmask {2}')
940         if msg is not None:
941             raise ValueError(msg.format(val_ipnetwork, opts[1]._name,
942                                         val_netmask))
943
944
945 class BroadcastOption(Option):
946     __slots__ = tuple()
947     _opt_type = 'broadcast'
948
949     def _validate(self, value):
950         try:
951             IP('{0}/32'.format(value))
952         except ValueError:
953             raise ValueError(_('invalid broadcast address'))
954
955     def _cons_broadcast(self, opts, vals):
956         if len(vals) != 3:
957             raise ConfigError(_('invalid len for vals'))
958         if None in vals:
959             return
960         broadcast, network, netmask = vals
961         if IP('{0}/{1}'.format(network, netmask)).broadcast() != IP(broadcast):
962             raise ValueError(_('invalid broadcast {0} ({1}) with network {2} '
963                                '({3}) and netmask {4} ({5})').format(
964                                    broadcast, opts[0]._name, network,
965                                    opts[1]._name, netmask, opts[2]._name))
966
967
968 class DomainnameOption(Option):
969     """represents the choice of a domain name
970     netbios: for MS domain
971     hostname: to identify the device
972     domainname:
973     fqdn: with tld, not supported yet
974     """
975     __slots__ = ('_type', '_allow_ip', '_allow_without_dot', '_domain_re')
976     _opt_type = 'domainname'
977
978     def __init__(self, name, doc, default=None, default_multi=None,
979                  requires=None, multi=False, callback=None,
980                  callback_params=None, validator=None, validator_params=None,
981                  properties=None, allow_ip=False, type_='domainname',
982                  warnings_only=False, allow_without_dot=False):
983         if type_ not in ['netbios', 'hostname', 'domainname']:
984             raise ValueError(_('unknown type_ {0} for hostname').format(type_))
985         self._type = type_
986         if allow_ip not in [True, False]:
987             raise ValueError(_('allow_ip must be a boolean'))
988         if allow_without_dot not in [True, False]:
989             raise ValueError(_('allow_without_dot must be a boolean'))
990         self._allow_ip = allow_ip
991         self._allow_without_dot = allow_without_dot
992         end = ''
993         extrachar = ''
994         extrachar_mandatory = ''
995         if self._type == 'netbios':
996             length = 14
997         elif self._type == 'hostname':
998             length = 62
999         elif self._type == 'domainname':
1000             length = 62
1001             if allow_without_dot is False:
1002                 extrachar_mandatory = '\.'
1003             else:
1004                 extrachar = '\.'
1005             end = '+[a-z]*'
1006         self._domain_re = re.compile(r'^(?:[a-z][a-z\d\-{0}]{{,{1}}}{2}){3}$'
1007                                      ''.format(extrachar, length, extrachar_mandatory, end))
1008         super(DomainnameOption, self).__init__(name, doc, default=default,
1009                                                default_multi=default_multi,
1010                                                callback=callback,
1011                                                callback_params=callback_params,
1012                                                requires=requires,
1013                                                multi=multi,
1014                                                validator=validator,
1015                                                validator_params=validator_params,
1016                                                properties=properties,
1017                                                warnings_only=warnings_only)
1018
1019     def _validate(self, value):
1020         if self._allow_ip is True:
1021             try:
1022                 IP('{0}/32'.format(value))
1023                 return
1024             except ValueError:
1025                 pass
1026         if self._type == 'domainname' and not self._allow_without_dot and \
1027                 '.' not in value:
1028             raise ValueError(_("invalid domainname, must have dot"))
1029             if len(value) > 255:
1030                 raise ValueError(_("invalid domainname's length (max 255)"))
1031         if len(value) < 2:
1032             raise ValueError(_("invalid domainname's length (min 2)"))
1033         if not self._domain_re.search(value):
1034             raise ValueError(_('invalid domainname'))
1035
1036
1037 class EmailOption(DomainnameOption):
1038     __slots__ = tuple()
1039     _opt_type = 'email'
1040     username_re = re.compile(r"^[\w!#$%&'*+\-/=?^`{|}~.]+$")
1041
1042     def _validate(self, value):
1043         splitted = value.split('@', 1)
1044         try:
1045             username, domain = splitted
1046         except ValueError:
1047             raise ValueError(_('invalid email address, should contains one @'
1048                                ))
1049         if not self.username_re.search(username):
1050             raise ValueError(_('invalid username in email address'))
1051         super(EmailOption, self)._validate(domain)
1052
1053
1054 class URLOption(DomainnameOption):
1055     __slots__ = tuple()
1056     _opt_type = 'url'
1057     proto_re = re.compile(r'(http|https)://')
1058     path_re = re.compile(r"^[a-z0-9\-\._~:/\?#\[\]@!%\$&\'\(\)\*\+,;=]+$")
1059
1060     def _validate(self, value):
1061         match = self.proto_re.search(value)
1062         if not match:
1063             raise ValueError(_('invalid url, should start with http:// or '
1064                                'https://'))
1065         value = value[len(match.group(0)):]
1066         # get domain/files
1067         splitted = value.split('/', 1)
1068         try:
1069             domain, files = splitted
1070         except ValueError:
1071             domain = value
1072             files = None
1073         # if port in domain
1074         splitted = domain.split(':', 1)
1075         try:
1076             domain, port = splitted
1077
1078         except ValueError:
1079             domain = splitted[0]
1080             port = 0
1081         if not 0 <= int(port) <= 65535:
1082             raise ValueError(_('invalid url, port must be an between 0 and '
1083                                '65536'))
1084         # validate domainname
1085         super(URLOption, self)._validate(domain)
1086         # validate file
1087         if files is not None and files != '' and not self.path_re.search(files):
1088             raise ValueError(_('invalid url, should ends with filename'))
1089
1090
1091 class FilenameOption(Option):
1092     __slots__ = tuple()
1093     _opt_type = 'file'
1094     path_re = re.compile(r"^[a-zA-Z0-9\-\._~/+]+$")
1095
1096     def _validate(self, value):
1097         match = self.path_re.search(value)
1098         if not match:
1099             raise ValueError(_('invalid filename'))
1100
1101
1102 class OptionDescription(BaseOption):
1103     """Config's schema (organisation, group) and container of Options
1104     The `OptionsDescription` objects lives in the `tiramisu.config.Config`.
1105     """
1106     __slots__ = ('_name', '_requires', '_cache_paths', '_group_type',
1107                  '_state_group_type', '_properties', '_children',
1108                  '_cache_consistencies', '_calc_properties', '__weakref__',
1109                  '_readonly', '_impl_informations', '_state_requires',
1110                  '_stated', '_state_readonly')
1111     _opt_type = 'optiondescription'
1112
1113     def __init__(self, name, doc, children, requires=None, properties=None):
1114         """
1115         :param children: a list of options (including optiondescriptions)
1116
1117         """
1118         super(OptionDescription, self).__init__(name, doc, requires, properties)
1119         child_names = [child._name for child in children]
1120         #better performance like this
1121         valid_child = copy(child_names)
1122         valid_child.sort()
1123         old = None
1124         for child in valid_child:
1125             if child == old:
1126                 raise ConflictError(_('duplicate option name: '
1127                                       '{0}').format(child))
1128             old = child
1129         self._children = (tuple(child_names), tuple(children))
1130         self._cache_paths = None
1131         self._cache_consistencies = None
1132         # the group_type is useful for filtering OptionDescriptions in a config
1133         self._group_type = groups.default
1134
1135     def impl_getdoc(self):
1136         return self.impl_get_information('doc')
1137
1138     def __getattr__(self, name):
1139         if name in self.__slots__:
1140             return object.__getattribute__(self, name)
1141         try:
1142             return self._children[1][self._children[0].index(name)]
1143         except ValueError:
1144             raise AttributeError(_('unknown Option {0} '
1145                                    'in OptionDescription {1}'
1146                                    '').format(name, self._name))
1147
1148     def impl_getkey(self, config):
1149         return tuple([child.impl_getkey(getattr(config, child._name))
1150                       for child in self.impl_getchildren()])
1151
1152     def impl_getpaths(self, include_groups=False, _currpath=None):
1153         """returns a list of all paths in self, recursively
1154            _currpath should not be provided (helps with recursion)
1155         """
1156         if _currpath is None:
1157             _currpath = []
1158         paths = []
1159         for option in self.impl_getchildren():
1160             attr = option._name
1161             if isinstance(option, OptionDescription):
1162                 if include_groups:
1163                     paths.append('.'.join(_currpath + [attr]))
1164                 paths += option.impl_getpaths(include_groups=include_groups,
1165                                               _currpath=_currpath + [attr])
1166             else:
1167                 paths.append('.'.join(_currpath + [attr]))
1168         return paths
1169
1170     def impl_getchildren(self):
1171         return self._children[1]
1172
1173     def impl_build_cache(self,
1174                          cache_path=None,
1175                          cache_option=None,
1176                          _currpath=None,
1177                          _consistencies=None,
1178                          force_no_consistencies=False):
1179         if _currpath is None and self._cache_paths is not None:
1180             # cache already set
1181             return
1182         if _currpath is None:
1183             save = True
1184             _currpath = []
1185             if not force_no_consistencies:
1186                 _consistencies = {}
1187         else:
1188             save = False
1189         if cache_path is None:
1190             cache_path = []
1191             cache_option = []
1192         for option in self.impl_getchildren():
1193             attr = option._name
1194             if option in cache_option:
1195                 raise ConflictError(_('duplicate option: {0}').format(option))
1196
1197             cache_option.append(option)
1198             if not force_no_consistencies:
1199                 option._readonly = True
1200             cache_path.append(str('.'.join(_currpath + [attr])))
1201             if not isinstance(option, OptionDescription):
1202                 if not force_no_consistencies and \
1203                         option._consistencies is not None:
1204                     for consistency in option._consistencies:
1205                         func, all_cons_opts = consistency
1206                         for opt in all_cons_opts:
1207                             _consistencies.setdefault(opt,
1208                                                       []).append((func,
1209                                                                   all_cons_opts))
1210             else:
1211                 _currpath.append(attr)
1212                 option.impl_build_cache(cache_path,
1213                                         cache_option,
1214                                         _currpath,
1215                                         _consistencies,
1216                                         force_no_consistencies)
1217                 _currpath.pop()
1218         if save:
1219             self._cache_paths = (tuple(cache_option), tuple(cache_path))
1220             if not force_no_consistencies:
1221                 if _consistencies != {}:
1222                     self._cache_consistencies = {}
1223                     for opt, cons in _consistencies.items():
1224                         if opt not in cache_option:
1225                             raise ConfigError(_('consistency with option {0} which is not in Config').format(opt._name))
1226                         self._cache_consistencies[opt] = tuple(cons)
1227                 self._readonly = True
1228
1229     def impl_get_opt_by_path(self, path):
1230         try:
1231             return self._cache_paths[0][self._cache_paths[1].index(path)]
1232         except ValueError:
1233             raise AttributeError(_('no option for path {0}').format(path))
1234
1235     def impl_get_path_by_opt(self, opt):
1236         try:
1237             return self._cache_paths[1][self._cache_paths[0].index(opt)]
1238         except ValueError:
1239             raise AttributeError(_('no option {0} found').format(opt))
1240
1241     # ____________________________________________________________
1242     def impl_set_group_type(self, group_type):
1243         """sets a given group object to an OptionDescription
1244
1245         :param group_type: an instance of `GroupType` or `MasterGroupType`
1246                               that lives in `setting.groups`
1247         """
1248         if self._group_type != groups.default:
1249             raise TypeError(_('cannot change group_type if already set '
1250                             '(old {0}, new {1})').format(self._group_type,
1251                                                          group_type))
1252         if isinstance(group_type, groups.GroupType):
1253             self._group_type = group_type
1254             if isinstance(group_type, groups.MasterGroupType):
1255                 #if master (same name has group) is set
1256                 #for collect all slaves
1257                 slaves = []
1258                 master = None
1259                 for child in self.impl_getchildren():
1260                     if isinstance(child, OptionDescription):
1261                         raise ValueError(_("master group {0} shall not have "
1262                                          "a subgroup").format(self._name))
1263                     if isinstance(child, SymLinkOption):
1264                         raise ValueError(_("master group {0} shall not have "
1265                                          "a symlinkoption").format(self._name))
1266                     if not child.impl_is_multi():
1267                         raise ValueError(_("not allowed option {0} "
1268                                          "in group {1}"
1269                                          ": this option is not a multi"
1270                                          "").format(child._name, self._name))
1271                     if child._name == self._name:
1272                         child._multitype = multitypes.master
1273                         master = child
1274                     else:
1275                         slaves.append(child)
1276                 if master is None:
1277                     raise ValueError(_('master group with wrong'
1278                                        ' master name for {0}'
1279                                        ).format(self._name))
1280                 if master._callback is not None and master._callback[1] is not None:
1281                     for key, callbacks in master._callback[1].items():
1282                         for callbk in callbacks:
1283                             if isinstance(callbk, tuple):
1284                                 if callbk[0] in slaves:
1285                                     raise ValueError(_("callback of master's option shall "
1286                                                        "not refered a slave's ones"))
1287                 master._master_slaves = tuple(slaves)
1288                 for child in self.impl_getchildren():
1289                     if child != master:
1290                         child._master_slaves = master
1291                         child._multitype = multitypes.slave
1292         else:
1293             raise ValueError(_('group_type: {0}'
1294                                ' not allowed').format(group_type))
1295
1296     def impl_get_group_type(self):
1297         return self._group_type
1298
1299     def _valid_consistency(self, option, value, context, index):
1300         if self._cache_consistencies is None:
1301             return True
1302         #consistencies is something like [('_cons_not_equal', (opt1, opt2))]
1303         consistencies = self._cache_consistencies.get(option)
1304         if consistencies is not None:
1305             for func, all_cons_opts in consistencies:
1306                 #all_cons_opts[0] is the option where func is set
1307                 ret = all_cons_opts[0]._launch_consistency(func, option,
1308                                                            value,
1309                                                            context, index,
1310                                                            all_cons_opts)
1311                 if ret is False:
1312                     return False
1313         return True
1314
1315     def _impl_getstate(self, descr=None):
1316         """enables us to export into a dict
1317         :param descr: parent :class:`tiramisu.option.OptionDescription`
1318         """
1319         if descr is None:
1320             self.impl_build_cache()
1321             descr = self
1322         super(OptionDescription, self)._impl_getstate(descr)
1323         self._state_group_type = str(self._group_type)
1324         for option in self.impl_getchildren():
1325             option._impl_getstate(descr)
1326
1327     def __getstate__(self):
1328         """special method to enable the serialization with pickle
1329         """
1330         stated = True
1331         try:
1332             # the `_state` attribute is a flag that which tells us if
1333             # the serialization can be performed
1334             self._stated
1335         except AttributeError:
1336             # if cannot delete, _impl_getstate never launch
1337             # launch it recursivement
1338             # _stated prevent __getstate__ launch more than one time
1339             # _stated is delete, if re-serialize, re-lauch _impl_getstate
1340             self._impl_getstate()
1341             stated = False
1342         return super(OptionDescription, self).__getstate__(stated)
1343
1344     def _impl_setstate(self, descr=None):
1345         """enables us to import from a dict
1346         :param descr: parent :class:`tiramisu.option.OptionDescription`
1347         """
1348         if descr is None:
1349             self._cache_paths = None
1350             self._cache_consistencies = None
1351             self.impl_build_cache(force_no_consistencies=True)
1352             descr = self
1353         self._group_type = getattr(groups, self._state_group_type)
1354         del(self._state_group_type)
1355         super(OptionDescription, self)._impl_setstate(descr)
1356         for option in self.impl_getchildren():
1357             option._impl_setstate(descr)
1358
1359     def __setstate__(self, state):
1360         super(OptionDescription, self).__setstate__(state)
1361         try:
1362             self._stated
1363         except AttributeError:
1364             self._impl_setstate()
1365
1366
1367 def validate_requires_arg(requires, name):
1368     """check malformed requirements
1369     and tranform dict to internal tuple
1370
1371     :param requires: have a look at the
1372                      :meth:`tiramisu.setting.Settings.apply_requires` method to
1373                      know more about
1374                      the description of the requires dictionary
1375     """
1376     if requires is None:
1377         return None, None
1378     ret_requires = {}
1379     config_action = {}
1380
1381     # start parsing all requires given by user (has dict)
1382     # transforme it to a tuple
1383     for require in requires:
1384         if not type(require) == dict:
1385             raise ValueError(_("malformed requirements type for option:"
1386                                " {0}, must be a dict").format(name))
1387         valid_keys = ('option', 'expected', 'action', 'inverse', 'transitive',
1388                       'same_action')
1389         unknown_keys = frozenset(require.keys()) - frozenset(valid_keys)
1390         if unknown_keys != frozenset():
1391             raise ValueError('malformed requirements for option: {0}'
1392                              ' unknown keys {1}, must only '
1393                              '{2}'.format(name,
1394                                           unknown_keys,
1395                                           valid_keys))
1396         # prepare all attributes
1397         try:
1398             option = require['option']
1399             expected = require['expected']
1400             action = require['action']
1401         except KeyError:
1402             raise ValueError(_("malformed requirements for option: {0}"
1403                                " require must have option, expected and"
1404                                " action keys").format(name))
1405         inverse = require.get('inverse', False)
1406         if inverse not in [True, False]:
1407             raise ValueError(_('malformed requirements for option: {0}'
1408                                ' inverse must be boolean'))
1409         transitive = require.get('transitive', True)
1410         if transitive not in [True, False]:
1411             raise ValueError(_('malformed requirements for option: {0}'
1412                                ' transitive must be boolean'))
1413         same_action = require.get('same_action', True)
1414         if same_action not in [True, False]:
1415             raise ValueError(_('malformed requirements for option: {0}'
1416                                ' same_action must be boolean'))
1417
1418         if not isinstance(option, Option):
1419             raise ValueError(_('malformed requirements '
1420                                'must be an option in option {0}').format(name))
1421         if option.impl_is_multi():
1422             raise ValueError(_('malformed requirements option {0} '
1423                                'should not be a multi').format(name))
1424         if expected is not None:
1425             try:
1426                 option._validate(expected)
1427             except ValueError as err:
1428                 raise ValueError(_('malformed requirements second argument '
1429                                    'must be valid for option {0}'
1430                                    ': {1}').format(name, err))
1431         if action in config_action:
1432             if inverse != config_action[action]:
1433                 raise ValueError(_("inconsistency in action types"
1434                                    " for option: {0}"
1435                                    " action: {1}").format(name, action))
1436         else:
1437             config_action[action] = inverse
1438         if action not in ret_requires:
1439             ret_requires[action] = {}
1440         if option not in ret_requires[action]:
1441             ret_requires[action][option] = (option, [expected], action,
1442                                             inverse, transitive, same_action)
1443         else:
1444             ret_requires[action][option][1].append(expected)
1445     # transform dict to tuple
1446     ret = []
1447     for opt_requires in ret_requires.values():
1448         ret_action = []
1449         for require in opt_requires.values():
1450             ret_action.append((require[0], tuple(require[1]), require[2],
1451                                require[3], require[4], require[5]))
1452         ret.append(tuple(ret_action))
1453     return frozenset(config_action.keys()), tuple(ret)
1454
1455
1456 def validate_callback(callback, callback_params, type_):
1457     if type(callback) != FunctionType:
1458         raise ValueError(_('{0} should be a function').format(type_))
1459     if callback_params is not None:
1460         if not isinstance(callback_params, dict):
1461             raise ValueError(_('{0}_params should be a dict').format(type_))
1462         for key, callbacks in callback_params.items():
1463             if key != '' and len(callbacks) != 1:
1464                 raise ValueError(_('{0}_params with key {1} should not have '
1465                                    'length different to 1').format(type_,
1466                                                                    key))
1467             if not isinstance(callbacks, tuple):
1468                 raise ValueError(_('{0}_params should be tuple for key "{1}"'
1469                                    ).format(type_, key))
1470             for callbk in callbacks:
1471                 if isinstance(callbk, tuple):
1472                     option, force_permissive = callbk
1473                     if type_ == 'validator' and not force_permissive:
1474                         raise ValueError(_('validator not support tuple'))
1475                     if not isinstance(option, Option) and not \
1476                             isinstance(option, SymLinkOption):
1477                         raise ValueError(_('{0}_params should have an option '
1478                                            'not a {0} for first argument'
1479                                            ).format(type_, type(option)))
1480                     if force_permissive not in [True, False]:
1481                         raise ValueError(_('{0}_params should have a boolean'
1482                                            ' not a {0} for second argument'
1483                                            ).format(type_, type(
1484                                                force_permissive)))