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