Merge modification made for 1.0's branch
[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
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                 if self._warnings_only:
434                     warning = error
435                     error = None
436             except ValueWarning as warning:
437                 pass
438             if error is None and warning is None:
439                 try:
440                     # if context launch consistency validation
441                     if context is not None:
442                         descr._valid_consistency(self, _value, context, _index)
443                 except ValueError as error:
444                     pass
445                 except ValueWarning as warning:
446                     pass
447             if warning:
448                 msg = _("warning on the value of the option {0}: {1}").format(
449                     self._name, warning)
450                 warnings.warn_explicit(ValueWarning(msg, self),
451                                        ValueWarning,
452                                        self.__class__.__name__, 0)
453             elif error:
454                 raise ValueError(_("invalid value for option {0}: {1}").format(
455                     self._name, error))
456
457         # generic calculation
458         if context is not None:
459             descr = context.cfgimpl_get_description()
460
461         if not self._multi or force_index is not None:
462             do_validation(value, force_index)
463         else:
464             if not isinstance(value, list):
465                 raise ValueError(_("invalid value {0} for option {1} which must be a list").format(value, self._name))
466             for index, val in enumerate(value):
467                 do_validation(val, index)
468
469     def impl_getdefault(self):
470         "accessing the default value"
471         return self._default
472
473     def impl_getdefault_multi(self):
474         "accessing the default value for a multi"
475         return self._default_multi
476
477     def impl_get_multitype(self):
478         return self._multitype
479
480     def impl_get_master_slaves(self):
481         return self._master_slaves
482
483     def impl_is_empty_by_default(self):
484         "no default value has been set yet"
485         if ((not self.impl_is_multi() and self._default is None) or
486                 (self.impl_is_multi() and (self._default == []
487                                            or None in self._default))):
488             return True
489         return False
490
491     def impl_getdoc(self):
492         "accesses the Option's doc"
493         return self.impl_get_information('doc')
494
495     def impl_has_callback(self):
496         "to know if a callback has been defined or not"
497         if self._callback is None:
498             return False
499         else:
500             return True
501
502     def impl_getkey(self, value):
503         return value
504
505     def impl_is_multi(self):
506         return self._multi
507
508     def impl_add_consistency(self, func, *other_opts, **params):
509         """Add consistency means that value will be validate with other_opts
510         option's values.
511
512         :param func: function's name
513         :type func: `str`
514         :param other_opts: options used to validate value
515         :type other_opts: `list` of `tiramisu.option.Option`
516         :param params: extra params (only warnings_only are allowed)
517         """
518         if self._consistencies is None:
519             self._consistencies = []
520         warnings_only = params.get('warnings_only', False)
521         for opt in other_opts:
522             if not isinstance(opt, Option):
523                 raise ConfigError(_('consistency must be set with an option'))
524             if self is opt:
525                 raise ConfigError(_('cannot add consistency with itself'))
526             if self.impl_is_multi() != opt.impl_is_multi():
527                 raise ConfigError(_('every options in consistency must be '
528                                     'multi or none'))
529         func = '_cons_{0}'.format(func)
530         all_cons_opts = tuple([self] + list(other_opts))
531         value = self.impl_getdefault()
532         if value is not None:
533             if self.impl_is_multi():
534                 for idx, val in enumerate(value):
535                     self._launch_consistency(func, self, val, None,
536                                              idx, all_cons_opts, warnings_only)
537             else:
538                 self._launch_consistency(func, self, value, None,
539                                          None, all_cons_opts, warnings_only)
540         self._consistencies.append((func, all_cons_opts, params))
541         self.impl_validate(self.impl_getdefault())
542
543     def _cons_not_equal(self, opts, vals, warnings_only):
544         for idx_inf, val_inf in enumerate(vals):
545             for idx_sup, val_sup in enumerate(vals[idx_inf + 1:]):
546                 if val_inf == val_sup is not None:
547                     if warnings_only:
548                         msg = _("same value for {0} and {1}, should be different")
549                     else:
550                         msg = _("same value for {0} and {1}, must be different")
551                     raise ValueError(msg.format(opts[idx_inf]._name,
552                                                 opts[idx_inf + idx_sup + 1]._name))
553
554     def _impl_convert_callbacks(self, descr, load=False):
555         if not load and self._callback is None:
556             self._state_callback = None
557         elif load and self._state_callback is None:
558             self._callback = None
559             del(self._state_callback)
560         else:
561             if load:
562                 callback, callback_params = self._state_callback
563             else:
564                 callback, callback_params = self._callback
565             if callback_params is not None:
566                 cllbck_prms = {}
567                 for key, values in callback_params.items():
568                     vls = []
569                     for value in values:
570                         if isinstance(value, tuple):
571                             if load:
572                                 value = (descr.impl_get_opt_by_path(value[0]),
573                                          value[1])
574                             else:
575                                 value = (descr.impl_get_path_by_opt(value[0]),
576                                          value[1])
577                         vls.append(value)
578                     cllbck_prms[key] = tuple(vls)
579             else:
580                 cllbck_prms = None
581
582             if load:
583                 del(self._state_callback)
584                 self._callback = (callback, cllbck_prms)
585             else:
586                 self._state_callback = (callback, cllbck_prms)
587
588     # serialize/unserialize
589     def _impl_convert_consistencies(self, descr, load=False):
590         """during serialization process, many things have to be done.
591         one of them is the localisation of the options.
592         The paths are set once for all.
593
594         :type descr: :class:`tiramisu.option.OptionDescription`
595         :param load: `True` if we are at the init of the option description
596         :type load: bool
597         """
598         if not load and self._consistencies is None:
599             self._state_consistencies = None
600         elif load and self._state_consistencies is None:
601             self._consistencies = None
602             del(self._state_consistencies)
603         else:
604             if load:
605                 consistencies = self._state_consistencies
606             else:
607                 consistencies = self._consistencies
608             new_value = []
609             for consistency in consistencies:
610                 values = []
611                 for obj in consistency[1]:
612                     if load:
613                         values.append(descr.impl_get_opt_by_path(obj))
614                     else:
615                         values.append(descr.impl_get_path_by_opt(obj))
616                 new_value.append((consistency[0], tuple(values), consistency[2]))
617             if load:
618                 del(self._state_consistencies)
619                 self._consistencies = new_value
620             else:
621                 self._state_consistencies = new_value
622
623     def _second_level_validation(self, value, warnings_only):
624         pass
625
626
627 class ChoiceOption(Option):
628     """represents a choice out of several objects.
629
630     The option can also have the value ``None``
631     """
632
633     __slots__ = ('_values', '_open_values')
634     _opt_type = 'string'
635
636     def __init__(self, name, doc, values, default=None, default_multi=None,
637                  requires=None, multi=False, callback=None,
638                  callback_params=None, open_values=False, validator=None,
639                  validator_params=None, properties=None, warnings_only=False):
640         """
641         :param values: is a list of values the option can possibly take
642         """
643         if not isinstance(values, tuple):
644             raise TypeError(_('values must be a tuple for {0}').format(name))
645         self._values = values
646         if open_values not in (True, False):
647             raise TypeError(_('open_values must be a boolean for '
648                             '{0}').format(name))
649         self._open_values = open_values
650         super(ChoiceOption, self).__init__(name, doc, default=default,
651                                            default_multi=default_multi,
652                                            callback=callback,
653                                            callback_params=callback_params,
654                                            requires=requires,
655                                            multi=multi,
656                                            validator=validator,
657                                            validator_params=validator_params,
658                                            properties=properties,
659                                            warnings_only=warnings_only)
660
661     def impl_get_values(self):
662         return self._values
663
664     def impl_is_openvalues(self):
665         return self._open_values
666
667     def _validate(self, value):
668         if not self.impl_is_openvalues() and not value in self.impl_get_values():
669             raise ValueError(_('value {0} is not permitted, '
670                                'only {1} is allowed'
671                                '').format(value, self._values))
672
673
674 class BoolOption(Option):
675     "represents a choice between ``True`` and ``False``"
676     __slots__ = tuple()
677     _opt_type = 'bool'
678
679     def _validate(self, value):
680         if not isinstance(value, bool):
681             raise ValueError(_('invalid boolean'))
682
683
684 class IntOption(Option):
685     "represents a choice of an integer"
686     __slots__ = tuple()
687     _opt_type = 'int'
688
689     def _validate(self, value):
690         if not isinstance(value, int):
691             raise ValueError(_('invalid integer'))
692
693
694 class FloatOption(Option):
695     "represents a choice of a floating point number"
696     __slots__ = tuple()
697     _opt_type = 'float'
698
699     def _validate(self, value):
700         if not isinstance(value, float):
701             raise ValueError(_('invalid float'))
702
703
704 class StrOption(Option):
705     "represents the choice of a string"
706     __slots__ = tuple()
707     _opt_type = 'string'
708
709     def _validate(self, value):
710         if not isinstance(value, str):
711             raise ValueError(_('invalid string'))
712
713
714 if sys.version_info[0] >= 3:
715     #UnicodeOption is same as StrOption in python 3+
716     class UnicodeOption(StrOption):
717         __slots__ = tuple()
718         pass
719 else:
720     class UnicodeOption(Option):
721         "represents the choice of a unicode string"
722         __slots__ = tuple()
723         _opt_type = 'unicode'
724         _empty = u''
725
726         def _validate(self, value):
727             if not isinstance(value, unicode):
728                 raise ValueError(_('invalid unicode'))
729
730
731 class SymLinkOption(BaseOption):
732     __slots__ = ('_name', '_opt', '_state_opt')
733     _opt_type = 'symlink'
734     #not return _opt consistencies
735     _consistencies = None
736
737     def __init__(self, name, opt):
738         self._name = name
739         if not isinstance(opt, Option):
740             raise ValueError(_('malformed symlinkoption '
741                                'must be an option '
742                                'for symlink {0}').format(name))
743         self._opt = opt
744         self._readonly = True
745
746     def __getattr__(self, name):
747         if name in ('_name', '_opt', '_opt_type', '_readonly'):
748             return object.__getattr__(self, name)
749         else:
750             return getattr(self._opt, name)
751
752     def _impl_getstate(self, descr):
753         super(SymLinkOption, self)._impl_getstate(descr)
754         self._state_opt = descr.impl_get_path_by_opt(self._opt)
755
756     def _impl_setstate(self, descr):
757         self._opt = descr.impl_get_opt_by_path(self._state_opt)
758         del(self._state_opt)
759         super(SymLinkOption, self)._impl_setstate(descr)
760
761
762 class IPOption(Option):
763     "represents the choice of an ip"
764     __slots__ = ('_private_only', '_allow_reserved')
765     _opt_type = 'ip'
766
767     def __init__(self, name, doc, default=None, default_multi=None,
768                  requires=None, multi=False, callback=None,
769                  callback_params=None, validator=None, validator_params=None,
770                  properties=None, private_only=False, allow_reserved=False,
771                  warnings_only=False):
772         self._private_only = private_only
773         self._allow_reserved = allow_reserved
774         super(IPOption, self).__init__(name, doc, default=default,
775                                        default_multi=default_multi,
776                                        callback=callback,
777                                        callback_params=callback_params,
778                                        requires=requires,
779                                        multi=multi,
780                                        validator=validator,
781                                        validator_params=validator_params,
782                                        properties=properties,
783                                        warnings_only=warnings_only)
784
785     def _validate(self, value):
786         # sometimes an ip term starts with a zero
787         # but this does not fit in some case, for example bind does not like it
788         try:
789             for val in value.split('.'):
790                 if val.startswith("0") and len(val) > 1:
791                     raise ValueError(_('invalid IP'))
792         except AttributeError:
793             #if integer for example
794             raise ValueError(_('invalid IP'))
795         # 'standard' validation
796         try:
797             IP('{0}/32'.format(value))
798         except ValueError:
799             raise ValueError(_('invalid IP'))
800
801     def _second_level_validation(self, value, warnings_only):
802         ip = IP('{0}/32'.format(value))
803         if not self._allow_reserved and ip.iptype() == 'RESERVED':
804             if warnings_only:
805                 msg = _("IP is in reserved class")
806             else:
807                 msg = _("invalid IP, mustn't be in reserved class")
808             raise ValueError(msg)
809         if self._private_only and not ip.iptype() == 'PRIVATE':
810             if warnings_only:
811                 msg = _("IP is not in private class")
812             else:
813                 msg = _("invalid IP, must be in private class")
814             raise ValueError(msg)
815
816     def _cons_in_network(self, opts, vals, warnings_only):
817         if len(vals) != 3:
818             raise ConfigError(_('invalid len for vals'))
819         if None in vals:
820             return
821         ip, network, netmask = vals
822         if IP(ip) not in IP('{0}/{1}'.format(network, netmask)):
823             if warnings_only:
824                 msg = _('IP {0} ({1}) not in network {2} ({3}) with netmask {4}'
825                         ' ({5})')
826             else:
827                 msg = _('invalid IP {0} ({1}) not in network {2} ({3}) with '
828                         'netmask {4} ({5})')
829             raise ValueError(msg.format(ip, opts[0]._name, network,
830                              opts[1]._name, netmask, opts[2]._name))
831
832
833 class PortOption(Option):
834     """represents the choice of a port
835     The port numbers are divided into three ranges:
836     the well-known ports,
837     the registered ports,
838     and the dynamic or private ports.
839     You can actived this three range.
840     Port number 0 is reserved and can't be used.
841     see: http://en.wikipedia.org/wiki/Port_numbers
842     """
843     __slots__ = ('_allow_range', '_allow_zero', '_min_value', '_max_value')
844     _opt_type = 'port'
845
846     def __init__(self, name, doc, default=None, default_multi=None,
847                  requires=None, multi=False, callback=None,
848                  callback_params=None, validator=None, validator_params=None,
849                  properties=None, allow_range=False, allow_zero=False,
850                  allow_wellknown=True, allow_registred=True,
851                  allow_private=False, warnings_only=False):
852         self._allow_range = allow_range
853         self._min_value = None
854         self._max_value = None
855         ports_min = [0, 1, 1024, 49152]
856         ports_max = [0, 1023, 49151, 65535]
857         is_finally = False
858         for index, allowed in enumerate([allow_zero,
859                                          allow_wellknown,
860                                          allow_registred,
861                                          allow_private]):
862             if self._min_value is None:
863                 if allowed:
864                     self._min_value = ports_min[index]
865             elif not allowed:
866                 is_finally = True
867             elif allowed and is_finally:
868                 raise ValueError(_('inconsistency in allowed range'))
869             if allowed:
870                 self._max_value = ports_max[index]
871
872         if self._max_value is None:
873             raise ValueError(_('max value is empty'))
874
875         super(PortOption, self).__init__(name, doc, default=default,
876                                          default_multi=default_multi,
877                                          callback=callback,
878                                          callback_params=callback_params,
879                                          requires=requires,
880                                          multi=multi,
881                                          validator=validator,
882                                          validator_params=validator_params,
883                                          properties=properties,
884                                          warnings_only=warnings_only)
885
886     def _validate(self, value):
887         if self._allow_range and ":" in str(value):
888             value = str(value).split(':')
889             if len(value) != 2:
890                 raise ValueError(_('invalid port, range must have two values '
891                                  'only'))
892             if not value[0] < value[1]:
893                 raise ValueError(_('invalid port, first port in range must be'
894                                  ' smaller than the second one'))
895         else:
896             value = [value]
897
898         for val in value:
899             try:
900                 int(val)
901             except ValueError:
902                 raise ValueError(_('invalid port'))
903             if not self._min_value <= int(val) <= self._max_value:
904                 raise ValueError(_('invalid port, must be an between {0} '
905                                    'and {1}').format(self._min_value,
906                                                      self._max_value))
907
908
909 class NetworkOption(Option):
910     "represents the choice of a network"
911     __slots__ = tuple()
912     _opt_type = 'network'
913
914     def _validate(self, value):
915         try:
916             IP(value)
917         except ValueError:
918             raise ValueError(_('invalid network address'))
919
920     def _second_level_validation(self, value, warnings_only):
921         ip = IP(value)
922         if ip.iptype() == 'RESERVED':
923             if warnings_only:
924                 msg = _("network address is in reserved class")
925             else:
926                 msg = _("invalid network address, mustn't be in reserved class")
927             raise ValueError(msg)
928
929
930 class NetmaskOption(Option):
931     "represents the choice of a netmask"
932     __slots__ = tuple()
933     _opt_type = 'netmask'
934
935     def _validate(self, value):
936         try:
937             IP('0.0.0.0/{0}'.format(value))
938         except ValueError:
939             raise ValueError(_('invalid netmask address'))
940
941     def _cons_network_netmask(self, opts, vals, warnings_only):
942         #opts must be (netmask, network) options
943         if None in vals:
944             return
945         self.__cons_netmask(opts, vals[0], vals[1], False, warnings_only)
946
947     def _cons_ip_netmask(self, opts, vals, warnings_only):
948         #opts must be (netmask, ip) options
949         if None in vals:
950             return
951         self.__cons_netmask(opts, vals[0], vals[1], True, warnings_only)
952
953     def __cons_netmask(self, opts, val_netmask, val_ipnetwork, make_net,
954                        warnings_only):
955         if len(opts) != 2:
956             raise ConfigError(_('invalid len for opts'))
957         msg = None
958         try:
959             ip = IP('{0}/{1}'.format(val_ipnetwork, val_netmask),
960                     make_net=make_net)
961             #if cidr == 32, ip same has network
962             if ip.prefixlen() != 32:
963                 try:
964                     IP('{0}/{1}'.format(val_ipnetwork, val_netmask),
965                         make_net=not make_net)
966                 except ValueError:
967                     pass
968                 else:
969                     if make_net:
970                         msg = _("invalid IP {0} ({1}) with netmask {2},"
971                                 " this IP is a network")
972
973         except ValueError:
974             if not make_net:
975                 msg = _('invalid network {0} ({1}) with netmask {2}')
976         if msg is not None:
977             raise ValueError(msg.format(val_ipnetwork, opts[1]._name,
978                                         val_netmask))
979
980
981 class BroadcastOption(Option):
982     __slots__ = tuple()
983     _opt_type = 'broadcast'
984
985     def _validate(self, value):
986         try:
987             IP('{0}/32'.format(value))
988         except ValueError:
989             raise ValueError(_('invalid broadcast address'))
990
991     def _cons_broadcast(self, opts, vals, warnings_only):
992         if len(vals) != 3:
993             raise ConfigError(_('invalid len for vals'))
994         if None in vals:
995             return
996         broadcast, network, netmask = vals
997         if IP('{0}/{1}'.format(network, netmask)).broadcast() != IP(broadcast):
998             raise ValueError(_('invalid broadcast {0} ({1}) with network {2} '
999                                '({3}) and netmask {4} ({5})').format(
1000                                    broadcast, opts[0]._name, network,
1001                                    opts[1]._name, netmask, opts[2]._name))
1002
1003
1004 class DomainnameOption(Option):
1005     """represents the choice of a domain name
1006     netbios: for MS domain
1007     hostname: to identify the device
1008     domainname:
1009     fqdn: with tld, not supported yet
1010     """
1011     __slots__ = ('_type', '_allow_ip', '_allow_without_dot', '_domain_re')
1012     _opt_type = 'domainname'
1013
1014     def __init__(self, name, doc, default=None, default_multi=None,
1015                  requires=None, multi=False, callback=None,
1016                  callback_params=None, validator=None, validator_params=None,
1017                  properties=None, allow_ip=False, type_='domainname',
1018                  warnings_only=False, allow_without_dot=False):
1019         if type_ not in ['netbios', 'hostname', 'domainname']:
1020             raise ValueError(_('unknown type_ {0} for hostname').format(type_))
1021         self._type = type_
1022         if allow_ip not in [True, False]:
1023             raise ValueError(_('allow_ip must be a boolean'))
1024         if allow_without_dot not in [True, False]:
1025             raise ValueError(_('allow_without_dot must be a boolean'))
1026         self._allow_ip = allow_ip
1027         self._allow_without_dot = allow_without_dot
1028         end = ''
1029         extrachar = ''
1030         extrachar_mandatory = ''
1031         if self._type != 'netbios':
1032             allow_number = '\d'
1033         else:
1034             allow_number = ''
1035         if self._type == 'netbios':
1036             length = 14
1037         elif self._type == 'hostname':
1038             length = 62
1039         elif self._type == 'domainname':
1040             length = 62
1041             if allow_without_dot is False:
1042                 extrachar_mandatory = '\.'
1043             else:
1044                 extrachar = '\.'
1045             end = '+[a-z]*'
1046         self._domain_re = re.compile(r'^(?:[a-z{0}][a-z\d\-{1}]{{,{2}}}{3}){4}$'
1047                                      ''.format(allow_number, extrachar, length,
1048                                                extrachar_mandatory, end))
1049         super(DomainnameOption, self).__init__(name, doc, default=default,
1050                                                default_multi=default_multi,
1051                                                callback=callback,
1052                                                callback_params=callback_params,
1053                                                requires=requires,
1054                                                multi=multi,
1055                                                validator=validator,
1056                                                validator_params=validator_params,
1057                                                properties=properties,
1058                                                warnings_only=warnings_only)
1059
1060     def _validate(self, value):
1061         if self._allow_ip is True:
1062             try:
1063                 IP('{0}/32'.format(value))
1064                 return
1065             except ValueError:
1066                 pass
1067         if self._type == 'domainname' and not self._allow_without_dot and \
1068                 '.' not in value:
1069             raise ValueError(_("invalid domainname, must have dot"))
1070         if len(value) > 255:
1071             raise ValueError(_("invalid domainname's length (max 255)"))
1072         if len(value) < 2:
1073             raise ValueError(_("invalid domainname's length (min 2)"))
1074         if not self._domain_re.search(value):
1075             raise ValueError(_('invalid domainname'))
1076
1077
1078 class EmailOption(DomainnameOption):
1079     __slots__ = tuple()
1080     _opt_type = 'email'
1081     username_re = re.compile(r"^[\w!#$%&'*+\-/=?^`{|}~.]+$")
1082
1083     def _validate(self, value):
1084         splitted = value.split('@', 1)
1085         try:
1086             username, domain = splitted
1087         except ValueError:
1088             raise ValueError(_('invalid email address, must contains one @'
1089                                ))
1090         if not self.username_re.search(username):
1091             raise ValueError(_('invalid username in email address'))
1092         super(EmailOption, self)._validate(domain)
1093
1094
1095 class URLOption(DomainnameOption):
1096     __slots__ = tuple()
1097     _opt_type = 'url'
1098     proto_re = re.compile(r'(http|https)://')
1099     path_re = re.compile(r"^[a-z0-9\-\._~:/\?#\[\]@!%\$&\'\(\)\*\+,;=]+$")
1100
1101     def _validate(self, value):
1102         match = self.proto_re.search(value)
1103         if not match:
1104             raise ValueError(_('invalid url, must start with http:// or '
1105                                'https://'))
1106         value = value[len(match.group(0)):]
1107         # get domain/files
1108         splitted = value.split('/', 1)
1109         try:
1110             domain, files = splitted
1111         except ValueError:
1112             domain = value
1113             files = None
1114         # if port in domain
1115         splitted = domain.split(':', 1)
1116         try:
1117             domain, port = splitted
1118
1119         except ValueError:
1120             domain = splitted[0]
1121             port = 0
1122         if not 0 <= int(port) <= 65535:
1123             raise ValueError(_('invalid url, port must be an between 0 and '
1124                                '65536'))
1125         # validate domainname
1126         super(URLOption, self)._validate(domain)
1127         # validate file
1128         if files is not None and files != '' and not self.path_re.search(files):
1129             raise ValueError(_('invalid url, must ends with filename'))
1130
1131
1132 class UsernameOption(Option):
1133     __slots__ = tuple()
1134     _opt_type = 'username'
1135     #regexp build with 'man 8 adduser' informations
1136     username_re = re.compile(r"^[a-z_][a-z0-9_-]{0,30}[$a-z0-9_-]{0,1}$")
1137
1138     def _validate(self, value):
1139         match = self.username_re.search(value)
1140         if not match:
1141             raise ValueError(_('invalid username'))
1142
1143
1144 class FilenameOption(Option):
1145     __slots__ = tuple()
1146     _opt_type = 'file'
1147     path_re = re.compile(r"^[a-zA-Z0-9\-\._~/+]+$")
1148
1149     def _validate(self, value):
1150         match = self.path_re.search(value)
1151         if not match:
1152             raise ValueError(_('invalid filename'))
1153
1154
1155 class OptionDescription(BaseOption):
1156     """Config's schema (organisation, group) and container of Options
1157     The `OptionsDescription` objects lives in the `tiramisu.config.Config`.
1158     """
1159     __slots__ = ('_name', '_requires', '_cache_paths', '_group_type',
1160                  '_state_group_type', '_properties', '_children',
1161                  '_cache_consistencies', '_calc_properties', '__weakref__',
1162                  '_readonly', '_impl_informations', '_state_requires',
1163                  '_stated', '_state_readonly')
1164     _opt_type = 'optiondescription'
1165
1166     def __init__(self, name, doc, children, requires=None, properties=None):
1167         """
1168         :param children: a list of options (including optiondescriptions)
1169
1170         """
1171         super(OptionDescription, self).__init__(name, doc, requires, properties)
1172         child_names = [child._name for child in children]
1173         #better performance like this
1174         valid_child = copy(child_names)
1175         valid_child.sort()
1176         old = None
1177         for child in valid_child:
1178             if child == old:
1179                 raise ConflictError(_('duplicate option name: '
1180                                       '{0}').format(child))
1181             old = child
1182         self._children = (tuple(child_names), tuple(children))
1183         self._cache_paths = None
1184         self._cache_consistencies = None
1185         # the group_type is useful for filtering OptionDescriptions in a config
1186         self._group_type = groups.default
1187
1188     def impl_getdoc(self):
1189         return self.impl_get_information('doc')
1190
1191     def __getattr__(self, name):
1192         if name in self.__slots__:
1193             return object.__getattribute__(self, name)
1194         try:
1195             return self._children[1][self._children[0].index(name)]
1196         except ValueError:
1197             raise AttributeError(_('unknown Option {0} '
1198                                    'in OptionDescription {1}'
1199                                    '').format(name, self._name))
1200
1201     def impl_getkey(self, config):
1202         return tuple([child.impl_getkey(getattr(config, child._name))
1203                       for child in self.impl_getchildren()])
1204
1205     def impl_getpaths(self, include_groups=False, _currpath=None):
1206         """returns a list of all paths in self, recursively
1207            _currpath should not be provided (helps with recursion)
1208         """
1209         if _currpath is None:
1210             _currpath = []
1211         paths = []
1212         for option in self.impl_getchildren():
1213             attr = option._name
1214             if isinstance(option, OptionDescription):
1215                 if include_groups:
1216                     paths.append('.'.join(_currpath + [attr]))
1217                 paths += option.impl_getpaths(include_groups=include_groups,
1218                                               _currpath=_currpath + [attr])
1219             else:
1220                 paths.append('.'.join(_currpath + [attr]))
1221         return paths
1222
1223     def impl_getchildren(self):
1224         return self._children[1]
1225
1226     def impl_build_cache(self,
1227                          cache_path=None,
1228                          cache_option=None,
1229                          _currpath=None,
1230                          _consistencies=None,
1231                          force_no_consistencies=False):
1232         if _currpath is None and self._cache_paths is not None:
1233             # cache already set
1234             return
1235         if _currpath is None:
1236             save = True
1237             _currpath = []
1238             if not force_no_consistencies:
1239                 _consistencies = {}
1240         else:
1241             save = False
1242         if cache_path is None:
1243             cache_path = []
1244             cache_option = []
1245         for option in self.impl_getchildren():
1246             attr = option._name
1247             if option in cache_option:
1248                 raise ConflictError(_('duplicate option: {0}').format(option))
1249
1250             cache_option.append(option)
1251             if not force_no_consistencies:
1252                 option._readonly = True
1253             cache_path.append(str('.'.join(_currpath + [attr])))
1254             if not isinstance(option, OptionDescription):
1255                 if not force_no_consistencies and \
1256                         option._consistencies is not None:
1257                     for consistency in option._consistencies:
1258                         func, all_cons_opts, params = consistency
1259                         for opt in all_cons_opts:
1260                             _consistencies.setdefault(opt,
1261                                                       []).append((func,
1262                                                                   all_cons_opts,
1263                                                                   params))
1264             else:
1265                 _currpath.append(attr)
1266                 option.impl_build_cache(cache_path,
1267                                         cache_option,
1268                                         _currpath,
1269                                         _consistencies,
1270                                         force_no_consistencies)
1271                 _currpath.pop()
1272         if save:
1273             self._cache_paths = (tuple(cache_option), tuple(cache_path))
1274             if not force_no_consistencies:
1275                 if _consistencies != {}:
1276                     self._cache_consistencies = {}
1277                     for opt, cons in _consistencies.items():
1278                         if opt not in cache_option:
1279                             raise ConfigError(_('consistency with option {0} which is not in Config').format(opt._name))
1280                         self._cache_consistencies[opt] = tuple(cons)
1281                 self._readonly = True
1282
1283     def impl_get_opt_by_path(self, path):
1284         try:
1285             return self._cache_paths[0][self._cache_paths[1].index(path)]
1286         except ValueError:
1287             raise AttributeError(_('no option for path {0}').format(path))
1288
1289     def impl_get_path_by_opt(self, opt):
1290         try:
1291             return self._cache_paths[1][self._cache_paths[0].index(opt)]
1292         except ValueError:
1293             raise AttributeError(_('no option {0} found').format(opt))
1294
1295     # ____________________________________________________________
1296     def impl_set_group_type(self, group_type):
1297         """sets a given group object to an OptionDescription
1298
1299         :param group_type: an instance of `GroupType` or `MasterGroupType`
1300                               that lives in `setting.groups`
1301         """
1302         if self._group_type != groups.default:
1303             raise TypeError(_('cannot change group_type if already set '
1304                             '(old {0}, new {1})').format(self._group_type,
1305                                                          group_type))
1306         if isinstance(group_type, groups.GroupType):
1307             self._group_type = group_type
1308             if isinstance(group_type, groups.MasterGroupType):
1309                 #if master (same name has group) is set
1310                 #for collect all slaves
1311                 slaves = []
1312                 master = None
1313                 for child in self.impl_getchildren():
1314                     if isinstance(child, OptionDescription):
1315                         raise ValueError(_("master group {0} shall not have "
1316                                          "a subgroup").format(self._name))
1317                     if isinstance(child, SymLinkOption):
1318                         raise ValueError(_("master group {0} shall not have "
1319                                          "a symlinkoption").format(self._name))
1320                     if not child.impl_is_multi():
1321                         raise ValueError(_("not allowed option {0} "
1322                                          "in group {1}"
1323                                          ": this option is not a multi"
1324                                          "").format(child._name, self._name))
1325                     if child._name == self._name:
1326                         child._multitype = multitypes.master
1327                         master = child
1328                     else:
1329                         slaves.append(child)
1330                 if master is None:
1331                     raise ValueError(_('master group with wrong'
1332                                        ' master name for {0}'
1333                                        ).format(self._name))
1334                 if master._callback is not None and master._callback[1] is not None:
1335                     for key, callbacks in master._callback[1].items():
1336                         for callbk in callbacks:
1337                             if isinstance(callbk, tuple):
1338                                 if callbk[0] in slaves:
1339                                     raise ValueError(_("callback of master's option shall "
1340                                                        "not refered a slave's ones"))
1341                 master._master_slaves = tuple(slaves)
1342                 for child in self.impl_getchildren():
1343                     if child != master:
1344                         child._master_slaves = master
1345                         child._multitype = multitypes.slave
1346         else:
1347             raise ValueError(_('group_type: {0}'
1348                                ' not allowed').format(group_type))
1349
1350     def impl_get_group_type(self):
1351         return self._group_type
1352
1353     def _valid_consistency(self, option, value, context, index):
1354         if self._cache_consistencies is None:
1355             return True
1356         #consistencies is something like [('_cons_not_equal', (opt1, opt2))]
1357         consistencies = self._cache_consistencies.get(option)
1358         if consistencies is not None:
1359             for func, all_cons_opts, params in consistencies:
1360                 warnings_only = params.get('warnings_only', False)
1361                 #all_cons_opts[0] is the option where func is set
1362                 try:
1363                     all_cons_opts[0]._launch_consistency(func, option,
1364                                                          value,
1365                                                          context, index,
1366                                                          all_cons_opts,
1367                                                          warnings_only)
1368                 except ValueError as err:
1369                     if warnings_only:
1370                         raise ValueWarning(err.message, option)
1371                     else:
1372                         raise err
1373
1374     def _impl_getstate(self, descr=None):
1375         """enables us to export into a dict
1376         :param descr: parent :class:`tiramisu.option.OptionDescription`
1377         """
1378         if descr is None:
1379             self.impl_build_cache()
1380             descr = self
1381         super(OptionDescription, self)._impl_getstate(descr)
1382         self._state_group_type = str(self._group_type)
1383         for option in self.impl_getchildren():
1384             option._impl_getstate(descr)
1385
1386     def __getstate__(self):
1387         """special method to enable the serialization with pickle
1388         """
1389         stated = True
1390         try:
1391             # the `_state` attribute is a flag that which tells us if
1392             # the serialization can be performed
1393             self._stated
1394         except AttributeError:
1395             # if cannot delete, _impl_getstate never launch
1396             # launch it recursivement
1397             # _stated prevent __getstate__ launch more than one time
1398             # _stated is delete, if re-serialize, re-lauch _impl_getstate
1399             self._impl_getstate()
1400             stated = False
1401         return super(OptionDescription, self).__getstate__(stated)
1402
1403     def _impl_setstate(self, descr=None):
1404         """enables us to import from a dict
1405         :param descr: parent :class:`tiramisu.option.OptionDescription`
1406         """
1407         if descr is None:
1408             self._cache_paths = None
1409             self._cache_consistencies = None
1410             self.impl_build_cache(force_no_consistencies=True)
1411             descr = self
1412         self._group_type = getattr(groups, self._state_group_type)
1413         del(self._state_group_type)
1414         super(OptionDescription, self)._impl_setstate(descr)
1415         for option in self.impl_getchildren():
1416             option._impl_setstate(descr)
1417
1418     def __setstate__(self, state):
1419         super(OptionDescription, self).__setstate__(state)
1420         try:
1421             self._stated
1422         except AttributeError:
1423             self._impl_setstate()
1424
1425
1426 def validate_requires_arg(requires, name):
1427     """check malformed requirements
1428     and tranform dict to internal tuple
1429
1430     :param requires: have a look at the
1431                      :meth:`tiramisu.setting.Settings.apply_requires` method to
1432                      know more about
1433                      the description of the requires dictionary
1434     """
1435     if requires is None:
1436         return None, None
1437     ret_requires = {}
1438     config_action = {}
1439
1440     # start parsing all requires given by user (has dict)
1441     # transforme it to a tuple
1442     for require in requires:
1443         if not type(require) == dict:
1444             raise ValueError(_("malformed requirements type for option:"
1445                                " {0}, must be a dict").format(name))
1446         valid_keys = ('option', 'expected', 'action', 'inverse', 'transitive',
1447                       'same_action')
1448         unknown_keys = frozenset(require.keys()) - frozenset(valid_keys)
1449         if unknown_keys != frozenset():
1450             raise ValueError('malformed requirements for option: {0}'
1451                              ' unknown keys {1}, must only '
1452                              '{2}'.format(name,
1453                                           unknown_keys,
1454                                           valid_keys))
1455         # prepare all attributes
1456         try:
1457             option = require['option']
1458             expected = require['expected']
1459             action = require['action']
1460         except KeyError:
1461             raise ValueError(_("malformed requirements for option: {0}"
1462                                " require must have option, expected and"
1463                                " action keys").format(name))
1464         if action == 'force_store_value':
1465             raise ValueError(_("malformed requirements for option: {0}"
1466                                " action cannot be force_store_value"
1467                                ).format(name))
1468         inverse = require.get('inverse', False)
1469         if inverse not in [True, False]:
1470             raise ValueError(_('malformed requirements for option: {0}'
1471                                ' inverse must be boolean'))
1472         transitive = require.get('transitive', True)
1473         if transitive not in [True, False]:
1474             raise ValueError(_('malformed requirements for option: {0}'
1475                                ' transitive must be boolean'))
1476         same_action = require.get('same_action', True)
1477         if same_action not in [True, False]:
1478             raise ValueError(_('malformed requirements for option: {0}'
1479                                ' same_action must be boolean'))
1480
1481         if not isinstance(option, Option):
1482             raise ValueError(_('malformed requirements '
1483                                'must be an option in option {0}').format(name))
1484         if option.impl_is_multi():
1485             raise ValueError(_('malformed requirements option {0} '
1486                                'must not be a multi').format(name))
1487         if expected is not None:
1488             try:
1489                 option._validate(expected)
1490             except ValueError as err:
1491                 raise ValueError(_('malformed requirements second argument '
1492                                    'must be valid for option {0}'
1493                                    ': {1}').format(name, err))
1494         if action in config_action:
1495             if inverse != config_action[action]:
1496                 raise ValueError(_("inconsistency in action types"
1497                                    " for option: {0}"
1498                                    " action: {1}").format(name, action))
1499         else:
1500             config_action[action] = inverse
1501         if action not in ret_requires:
1502             ret_requires[action] = {}
1503         if option not in ret_requires[action]:
1504             ret_requires[action][option] = (option, [expected], action,
1505                                             inverse, transitive, same_action)
1506         else:
1507             ret_requires[action][option][1].append(expected)
1508     # transform dict to tuple
1509     ret = []
1510     for opt_requires in ret_requires.values():
1511         ret_action = []
1512         for require in opt_requires.values():
1513             ret_action.append((require[0], tuple(require[1]), require[2],
1514                                require[3], require[4], require[5]))
1515         ret.append(tuple(ret_action))
1516     return frozenset(config_action.keys()), tuple(ret)
1517
1518
1519 def validate_callback(callback, callback_params, type_):
1520     if type(callback) != FunctionType:
1521         raise ValueError(_('{0} must be a function').format(type_))
1522     if callback_params is not None:
1523         if not isinstance(callback_params, dict):
1524             raise ValueError(_('{0}_params must be a dict').format(type_))
1525         for key, callbacks in callback_params.items():
1526             if key != '' and len(callbacks) != 1:
1527                 raise ValueError(_("{0}_params with key {1} mustn't have "
1528                                    "length different to 1").format(type_,
1529                                                                    key))
1530             if not isinstance(callbacks, tuple):
1531                 raise ValueError(_('{0}_params must be tuple for key "{1}"'
1532                                    ).format(type_, key))
1533             for callbk in callbacks:
1534                 if isinstance(callbk, tuple):
1535                     if len(callbk) == 1:
1536                         if callbk != (None,):
1537                             raise ValueError(_('{0}_params with length of '
1538                                                'tuple as 1 must only have '
1539                                                'None as first value'))
1540                     elif len(callbk) != 2:
1541                         raise ValueError(_('{0}_params must only have 1 or 2 '
1542                                            'as length'))
1543                     else:
1544                         option, force_permissive = callbk
1545                         if type_ == 'validator' and not force_permissive:
1546                             raise ValueError(_('validator not support tuple'))
1547                         if not isinstance(option, Option) and not \
1548                                 isinstance(option, SymLinkOption):
1549                             raise ValueError(_('{0}_params must have an option'
1550                                                ' not a {0} for first argument'
1551                                                ).format(type_, type(option)))
1552                         if force_permissive not in [True, False]:
1553                             raise ValueError(_('{0}_params must have a boolean'
1554                                                ' not a {0} for second argument'
1555                                                ).format(type_, type(
1556                                                    force_permissive)))